Merge branch 'master' into feature/console

This commit is contained in:
Spencer 2016-03-31 12:53:48 -07:00
commit 3ec3006d38
118 changed files with 1252 additions and 871 deletions

View file

@ -24,18 +24,19 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"author": "Rashid Khan <rashid.khan@elastic.co>", "author": "Rashid Khan <rashid.khan@elastic.co>",
"contributors": [ "contributors": [
"Spencer Alger <spencer.alger@elastic.co>",
"Matt Bargar <matt.bargar@elastic.co>",
"Jon Budzenski <jonathan.budzenski@elastic.co>",
"Chris Cowan <chris.cowan@elastic.co>", "Chris Cowan <chris.cowan@elastic.co>",
"Court Ewing <court@elastic.co>", "Court Ewing <court@elastic.co>",
"Jim Unger <jim.unger@elastic.co>",
"Joe Fleming <joe.fleming@elastic.co>", "Joe Fleming <joe.fleming@elastic.co>",
"Jon Budzenski <jonathan.budzenski@elastic.co>",
"Juan Thomassie <juan.thomassie@elastic.co>",
"Khalah Jones-Golden <khalah.jones@elastic.co>", "Khalah Jones-Golden <khalah.jones@elastic.co>",
"Lukas Olson <lukas.olson@elastic.co>", "Lukas Olson <lukas.olson@elastic.co>",
"Juan Thomassie <juan.thomassie@elastic.co>", "Matt Bargar <matt.bargar@elastic.co>",
"Nicolás Bevacqua <nico@elastic.co>",
"Shelby Sturgis <shelby@elastic.co>", "Shelby Sturgis <shelby@elastic.co>",
"Tim Sullivan <tim@elastic.co>", "Spencer Alger <spencer.alger@elastic.co>",
"Jim Unger <jim.unger@elastic.co>" "Tim Sullivan <tim@elastic.co>"
], ],
"scripts": { "scripts": {
"test": "grunt test", "test": "grunt test",

View file

@ -0,0 +1,44 @@
import EventEmitter from 'events';
import { assign, random } from 'lodash';
import sinon from 'sinon';
import cluster from 'cluster';
import { delay } from 'bluebird';
export default class MockClusterFork extends EventEmitter {
constructor() {
super();
let dead = true;
function wait() {
return delay(random(10, 250));
}
assign(this, {
process: {
kill: sinon.spy(() => {
(async () => {
await wait();
this.emit('disconnect');
await wait();
dead = true;
this.emit('exit');
cluster.emit('exit', this, this.exitCode || 0);
}());
}),
},
isDead: sinon.spy(() => dead),
send: sinon.stub()
});
sinon.spy(this, 'on');
sinon.spy(this, 'removeListener');
sinon.spy(this, 'emit');
(async () => {
await wait();
dead = false;
this.emit('online');
}());
}
}

View file

@ -0,0 +1,59 @@
import expect from 'expect.js';
import sinon from 'auto-release-sinon';
import cluster from 'cluster';
import { ChildProcess } from 'child_process';
import { sample, difference } from 'lodash';
import ClusterManager from '../cluster_manager';
import Worker from '../worker';
describe('CLI cluster manager', function () {
function setup() {
sinon.stub(cluster, 'fork', function () {
return {
process: {
kill: sinon.stub(),
},
isDead: sinon.stub().returns(false),
removeListener: sinon.stub(),
on: sinon.stub(),
send: sinon.stub()
};
});
const manager = new ClusterManager({});
return manager;
}
it('has two workers', function () {
const manager = setup();
expect(manager.workers).to.have.length(2);
for (const worker of manager.workers) expect(worker).to.be.a(Worker);
expect(manager.optimizer).to.be.a(Worker);
expect(manager.server).to.be.a(Worker);
});
it('delivers broadcast messages to other workers', function () {
const manager = setup();
for (const worker of manager.workers) {
Worker.prototype.start.call(worker);// bypass the debounced start method
worker.onOnline();
}
const football = {};
const messenger = sample(manager.workers);
messenger.emit('broadcast', football);
for (const worker of manager.workers) {
if (worker === messenger) {
expect(worker.fork.send.callCount).to.be(0);
} else {
expect(worker.fork.send.firstCall.args[0]).to.be(football);
}
}
});
});

View file

@ -0,0 +1,198 @@
import expect from 'expect.js';
import sinon from 'auto-release-sinon';
import cluster from 'cluster';
import { ChildProcess } from 'child_process';
import { difference, findIndex, sample } from 'lodash';
import { fromNode as fn } from 'bluebird';
import MockClusterFork from './_mock_cluster_fork';
import Worker from '../worker';
const workersToShutdown = [];
function assertListenerAdded(emitter, event) {
sinon.assert.calledWith(emitter.on, event);
}
function assertListenerRemoved(emitter, event) {
sinon.assert.calledWith(
emitter.removeListener,
event,
emitter.on.args[findIndex(emitter.on.args, { 0: event })][1]
);
}
function setup(opts = {}) {
sinon.stub(cluster, 'fork', function () {
return new MockClusterFork();
});
const worker = new Worker(opts);
workersToShutdown.push(worker);
return worker;
}
describe('CLI cluster manager', function () {
afterEach(async function () {
for (const worker of workersToShutdown) {
if (worker.shutdown.restore) {
// if the shutdown method was stubbed, restore it first
worker.shutdown.restore();
}
await worker.shutdown();
}
});
describe('#onChange', function () {
context('opts.watch = true', function () {
it('restarts the fork', function () {
const worker = setup({ watch: true });
sinon.stub(worker, 'start');
worker.onChange('/some/path');
expect(worker.changes).to.eql(['/some/path']);
sinon.assert.calledOnce(worker.start);
});
});
context('opts.watch = false', function () {
it('does not restart the fork', function () {
const worker = setup({ watch: false });
sinon.stub(worker, 'start');
worker.onChange('/some/path');
expect(worker.changes).to.eql([]);
sinon.assert.notCalled(worker.start);
});
});
});
describe('#shutdown', function () {
context('after starting()', function () {
it('kills the worker and unbinds from message, online, and disconnect events', async function () {
const worker = setup();
await worker.start();
expect(worker).to.have.property('online', true);
const fork = worker.fork;
sinon.assert.notCalled(fork.process.kill);
assertListenerAdded(fork, 'message');
assertListenerAdded(fork, 'online');
assertListenerAdded(fork, 'disconnect');
worker.shutdown();
sinon.assert.calledOnce(fork.process.kill);
assertListenerRemoved(fork, 'message');
assertListenerRemoved(fork, 'online');
assertListenerRemoved(fork, 'disconnect');
});
});
context('before being started', function () {
it('does nothing', function () {
const worker = setup();
worker.shutdown();
});
});
});
describe('#parseIncomingMessage()', function () {
context('on a started worker', function () {
it(`is bound to fork's message event`, async function () {
const worker = setup();
await worker.start();
sinon.assert.calledWith(worker.fork.on, 'message');
});
});
it('ignores non-array messsages', function () {
const worker = setup();
worker.parseIncomingMessage('some string thing');
worker.parseIncomingMessage(0);
worker.parseIncomingMessage(null);
worker.parseIncomingMessage(undefined);
worker.parseIncomingMessage({ like: 'an object' });
worker.parseIncomingMessage(/weird/);
});
it('calls #onMessage with message parts', function () {
const worker = setup();
const stub = sinon.stub(worker, 'onMessage');
worker.parseIncomingMessage([10, 100, 1000, 10000]);
sinon.assert.calledWith(stub, 10, 100, 1000, 10000);
});
});
describe('#onMessage', function () {
context('when sent WORKER_BROADCAST message', function () {
it('emits the data to be broadcasted', function () {
const worker = setup();
const data = {};
const stub = sinon.stub(worker, 'emit');
worker.onMessage('WORKER_BROADCAST', data);
sinon.assert.calledWithExactly(stub, 'broadcast', data);
});
});
context('when sent WORKER_LISTENING message', function () {
it('sets the listening flag and emits the listening event', function () {
const worker = setup();
const data = {};
const stub = sinon.stub(worker, 'emit');
expect(worker).to.have.property('listening', false);
worker.onMessage('WORKER_LISTENING');
expect(worker).to.have.property('listening', true);
sinon.assert.calledWithExactly(stub, 'listening');
});
});
context('when passed an unkown message', function () {
it('does nothing', function () {
const worker = setup();
worker.onMessage('asdlfkajsdfahsdfiohuasdofihsdoif');
worker.onMessage({});
worker.onMessage(23049283094);
});
});
});
describe('#start', function () {
context('when not started', function () {
it('creates a fork and waits for it to come online', async function () {
const worker = setup();
sinon.spy(worker, 'on');
await worker.start();
sinon.assert.calledOnce(cluster.fork);
sinon.assert.calledWith(worker.on, 'fork:online');
});
it('listens for cluster and process "exit" events', async function () {
const worker = setup();
sinon.spy(process, 'on');
sinon.spy(cluster, 'on');
await worker.start();
sinon.assert.calledOnce(cluster.on);
sinon.assert.calledWith(cluster.on, 'exit');
sinon.assert.calledOnce(process.on);
sinon.assert.calledWith(process.on, 'exit');
});
});
context('when already started', function () {
it('calls shutdown and waits for the graceful shutdown to cause a restart', async function () {
const worker = setup();
await worker.start();
sinon.spy(worker, 'shutdown');
sinon.spy(worker, 'on');
worker.start();
sinon.assert.calledOnce(worker.shutdown);
sinon.assert.calledWith(worker.on, 'online');
});
});
});
});

View file

@ -11,7 +11,7 @@ import BasePathProxy from './base_path_proxy';
process.env.kbnWorkerType = 'managr'; process.env.kbnWorkerType = 'managr';
module.exports = class ClusterManager { module.exports = class ClusterManager {
constructor(opts, settings) { constructor(opts = {}, settings = {}) {
this.log = new Log(opts.quiet, opts.silent); this.log = new Log(opts.quiet, opts.silent);
this.addedCount = 0; this.addedCount = 0;

View file

@ -1,9 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import cluster from 'cluster'; import cluster from 'cluster';
let { resolve } = require('path'); import { resolve } from 'path';
let { EventEmitter } = require('events'); import { EventEmitter } from 'events';
import fromRoot from '../../utils/from_root'; import { BinderFor, fromRoot } from '../../utils';
let cliPath = fromRoot('src/cli'); let cliPath = fromRoot('src/cli');
let baseArgs = _.difference(process.argv.slice(2), ['--no-watch']); let baseArgs = _.difference(process.argv.slice(2), ['--no-watch']);
@ -18,13 +18,6 @@ let dead = fork => {
return fork.isDead() || fork.killed; return fork.isDead() || fork.killed;
}; };
let kill = fork => {
// fork.kill() waits for process to disconnect, but causes occasional
// "ipc disconnected" errors and is too slow for the proc's "exit" event
fork.process.kill();
fork.killed = true;
};
module.exports = class Worker extends EventEmitter { module.exports = class Worker extends EventEmitter {
constructor(opts) { constructor(opts) {
opts = opts || {}; opts = opts || {};
@ -36,26 +29,33 @@ module.exports = class Worker extends EventEmitter {
this.watch = (opts.watch !== false); this.watch = (opts.watch !== false);
this.startCount = 0; this.startCount = 0;
this.online = false; this.online = false;
this.listening = false;
this.changes = []; this.changes = [];
this.forkBinder = null; // defined when the fork is
this.clusterBinder = new BinderFor(cluster);
this.processBinder = new BinderFor(process);
let argv = _.union(baseArgv, opts.argv || []); let argv = _.union(baseArgv, opts.argv || []);
this.env = { this.env = {
kbnWorkerType: this.type, kbnWorkerType: this.type,
kbnWorkerArgv: JSON.stringify(argv) kbnWorkerArgv: JSON.stringify(argv)
}; };
_.bindAll(this, ['onExit', 'onMessage', 'onOnline', 'onDisconnect', 'shutdown', 'start']);
this.start = _.debounce(this.start, 25);
cluster.on('exit', this.onExit);
process.on('exit', this.shutdown);
} }
onExit(fork, code) { onExit(fork, code) {
if (this.fork !== fork) return; if (this.fork !== fork) return;
// we have our fork's exit, so stop listening for others
this.clusterBinder.destroy();
// our fork is gone, clear our ref so we don't try to talk to it anymore // our fork is gone, clear our ref so we don't try to talk to it anymore
this.fork = null; this.fork = null;
this.forkBinder = null;
this.online = false;
this.listening = false;
this.emit('fork:exit');
if (code) { if (code) {
this.log.bad(`${this.title} crashed`, 'with status code', code); this.log.bad(`${this.title} crashed`, 'with status code', code);
@ -72,26 +72,48 @@ module.exports = class Worker extends EventEmitter {
this.start(); this.start();
} }
shutdown() { async shutdown() {
if (this.fork && !dead(this.fork)) { if (this.fork && !dead(this.fork)) {
kill(this.fork); // kill the fork
this.fork.removeListener('message', this.onMessage); this.fork.process.kill();
this.fork.removeListener('online', this.onOnline); this.fork.killed = true;
this.fork.removeListener('disconnect', this.onDisconnect);
// stop listening to the fork, it's just going to die
this.forkBinder.destroy();
// we don't need to react to process.exit anymore
this.processBinder.destroy();
// wait until the cluster reports this fork has exitted, then resolve
await new Promise(cb => this.once('fork:exit', cb));
} }
} }
onMessage(msg) { parseIncomingMessage(msg) {
if (!_.isArray(msg) || msg[0] !== 'WORKER_BROADCAST') return; if (!_.isArray(msg)) return;
this.emit('broadcast', msg[1]); this.onMessage(...msg);
}
onMessage(type, data) {
switch (type) {
case 'WORKER_BROADCAST':
this.emit('broadcast', data);
break;
case 'WORKER_LISTENING':
this.listening = true;
this.emit('listening');
break;
}
} }
onOnline() { onOnline() {
this.online = true; this.online = true;
this.emit('fork:online');
} }
onDisconnect() { onDisconnect() {
this.online = false; this.online = false;
this.listening = false;
} }
flushChangeBuffer() { flushChangeBuffer() {
@ -102,9 +124,13 @@ module.exports = class Worker extends EventEmitter {
}, ''); }, '');
} }
start() { async start() {
if (this.fork) {
// once "exit" event is received with 0 status, start() is called again // once "exit" event is received with 0 status, start() is called again
if (this.fork) return this.shutdown(); this.shutdown();
await new Promise(cb => this.once('online', cb));
return;
}
if (this.changes.length) { if (this.changes.length) {
this.log.warn(`restarting ${this.title}`, `due to changes in ${this.flushChangeBuffer()}`); this.log.warn(`restarting ${this.title}`, `due to changes in ${this.flushChangeBuffer()}`);
@ -114,8 +140,20 @@ module.exports = class Worker extends EventEmitter {
} }
this.fork = cluster.fork(this.env); this.fork = cluster.fork(this.env);
this.fork.on('message', this.onMessage); this.forkBinder = new BinderFor(this.fork);
this.fork.on('online', this.onOnline);
this.fork.on('disconnect', this.onDisconnect); // when the fork sends a message, comes online, or looses it's connection, then react
this.forkBinder.on('message', (msg) => this.parseIncomingMessage(msg));
this.forkBinder.on('online', () => this.onOnline());
this.forkBinder.on('disconnect', () => this.onDisconnect());
// when the cluster says a fork has exitted, check if it is ours
this.clusterBinder.on('exit', (fork, code) => this.onExit(fork, code));
// when the process exits, make sure we kill our workers
this.processBinder.on('exit', () => this.shutdown());
// wait for the fork to report it is online before resolving
await new Promise(cb => this.once('fork:online', cb));
} }
}; };

View file

@ -82,7 +82,7 @@ Command.prototype.parseOptions = _.wrap(Command.prototype.parseOptions, function
Command.prototype.action = _.wrap(Command.prototype.action, function (action, fn) { Command.prototype.action = _.wrap(Command.prototype.action, function (action, fn) {
return action.call(this, function (...args) { return action.call(this, function (...args) {
var ret = fn.apply(this, args); let ret = fn.apply(this, args);
if (ret && typeof ret.then === 'function') { if (ret && typeof ret.then === 'function') {
ret.then(null, function (e) { ret.then(null, function (e) {
console.log('FATAL CLI ERROR', e.stack); console.log('FATAL CLI ERROR', e.stack);

View file

@ -69,6 +69,6 @@ ${indent(cmd.optionHelp(), 2)}
} }
function humanReadableArgName(arg) { function humanReadableArgName(arg) {
var nameOutput = arg.name + (arg.variadic === true ? '...' : ''); let nameOutput = arg.name + (arg.variadic === true ? '...' : '');
return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']'; return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']';
} }

View file

@ -2,7 +2,7 @@ import _ from 'lodash';
import fs from 'fs'; import fs from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
let legacySettingMap = { let legacySettingMap = {
// server // server

View file

@ -3,7 +3,7 @@ const { isWorker } = require('cluster');
const { resolve } = require('path'); const { resolve } = require('path');
const cwd = process.cwd(); const cwd = process.cwd();
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
let canCluster; let canCluster;
try { try {

View file

@ -23,7 +23,7 @@ program
.command('help <command>') .command('help <command>')
.description('get the help for a specific command') .description('get the help for a specific command')
.action(function (cmdName) { .action(function (cmdName) {
var cmd = _.find(program.commands, { _name: cmdName }); let cmd = _.find(program.commands, { _name: cmdName });
if (!cmd) return program.error(`unknown command ${cmdName}`); if (!cmd) return program.error(`unknown command ${cmdName}`);
cmd.help(); cmd.help();
}); });
@ -35,7 +35,7 @@ program
}); });
// check for no command name // check for no command name
var subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//); let subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
if (!subCommand) { if (!subCommand) {
program.defaultHelp(); program.defaultHelp();
} }

View file

@ -1,6 +1,6 @@
import path from 'path'; import path from 'path';
import expect from 'expect.js'; import expect from 'expect.js';
import fromRoot from '../../../utils/from_root'; import { fromRoot } from '../../../utils';
import { resolve } from 'path'; import { resolve } from 'path';
import { parseMilliseconds, parse, getPlatform } from '../settings'; import { parseMilliseconds, parse, getPlatform } from '../settings';

View file

@ -1,4 +1,4 @@
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
import install from './install'; import install from './install';
import Logger from '../lib/logger'; import Logger from '../lib/logger';
import pkg from '../../utils/package_json'; import pkg from '../../utils/package_json';

View file

@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
import KbnServer from '../../server/kbn_server'; import KbnServer from '../../server/kbn_server';
import readYamlConfig from '../../cli/serve/read_yaml_config'; import readYamlConfig from '../../cli/serve/read_yaml_config';
import { versionSatisfies, cleanVersion } from './version'; import { versionSatisfies, cleanVersion } from './version';

View file

@ -1,4 +1,4 @@
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
import list from './list'; import list from './list';
import Logger from '../lib/logger'; import Logger from '../lib/logger';
import { parse } from './settings'; import { parse } from './settings';

View file

@ -1,4 +1,4 @@
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
import remove from './remove'; import remove from './remove';
import Logger from '../lib/logger'; import Logger from '../lib/logger';
import { parse } from './settings'; import { parse } from './settings';

View file

@ -17,23 +17,23 @@ export default function GeoHashGridAggResponseFixture() {
// }, // },
// }); // });
var geoHashCharts = _.union( let geoHashCharts = _.union(
_.range(48, 57), // 0-9 _.range(48, 57), // 0-9
_.range(65, 90), // A-Z _.range(65, 90), // A-Z
_.range(97, 122) // a-z _.range(97, 122) // a-z
); );
var totalDocCount = 0; let totalDocCount = 0;
var tags = _.times(_.random(4, 20), function (i) { let tags = _.times(_.random(4, 20), function (i) {
// random number of tags // random number of tags
var docCount = 0; let docCount = 0;
var buckets = _.times(_.random(40, 200), function () { let buckets = _.times(_.random(40, 200), function () {
return _.sample(geoHashCharts, 3).join(''); return _.sample(geoHashCharts, 3).join('');
}) })
.sort() .sort()
.map(function (geoHash) { .map(function (geoHash) {
var count = _.random(1, 5000); let count = _.random(1, 5000);
totalDocCount += count; totalDocCount += count;
docCount += count; docCount += count;

View file

@ -1,4 +1,4 @@
var results = {}; let results = {};
results.timeSeries = { results.timeSeries = {
data: { data: {

View file

@ -1,4 +1,4 @@
var data = { }; let data = { };
data.metricOnly = { data.metricOnly = {
hits: { total: 1000, hits: [], max_score: 0 }, hits: { total: 1000, hits: [], max_score: 0 },

View file

@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
var longString = Array(200).join('_'); let longString = Array(200).join('_');
export default function (id, mapping) { export default function (id, mapping) {
function fakeVals(type) { function fakeVals(type) {

View file

@ -1,5 +1,5 @@
function stubbedLogstashFields() { function stubbedLogstashFields() {
var sourceData = [ let sourceData = [
{ name: 'bytes', type: 'number', indexed: true, analyzed: true, sortable: true, filterable: true, count: 10 }, { name: 'bytes', type: 'number', indexed: true, analyzed: true, sortable: true, filterable: true, count: 10 },
{ name: 'ssl', type: 'boolean', indexed: true, analyzed: true, sortable: true, filterable: true, count: 20 }, { name: 'ssl', type: 'boolean', indexed: true, analyzed: true, sortable: true, filterable: true, count: 20 },
{ name: '@timestamp', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 }, { name: '@timestamp', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 },

View file

@ -3,11 +3,11 @@ import sinon from 'auto-release-sinon';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
export default function (Private, Promise) { export default function (Private, Promise) {
var indexPatterns = Private(FixturesStubbedLogstashIndexPatternProvider); let indexPatterns = Private(FixturesStubbedLogstashIndexPatternProvider);
var getIndexPatternStub = sinon.stub(); let getIndexPatternStub = sinon.stub();
getIndexPatternStub.returns(Promise.resolve(indexPatterns)); getIndexPatternStub.returns(Promise.resolve(indexPatterns));
var courier = { let courier = {
indexPatterns: { get: getIndexPatternStub }, indexPatterns: { get: getIndexPatternStub },
getStub: getIndexPatternStub getStub: getIndexPatternStub
}; };

View file

@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
var keys = {}; let keys = {};
export default { export default {
get: function (path, def) { get: function (path, def) {
return keys[path] == null ? def : keys[path]; return keys[path] == null ? def : keys[path];

View file

@ -1,7 +1,7 @@
import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields';
function stubbedDocSourceResponse(Private) { function stubbedDocSourceResponse(Private) {
var mockLogstashFields = Private(FixturesLogstashFieldsProvider); let mockLogstashFields = Private(FixturesLogstashFieldsProvider);
return function (id, index) { return function (id, index) {
index = index || '.kibana'; index = index || '.kibana';

View file

@ -3,21 +3,21 @@ import TestUtilsStubIndexPatternProvider from 'test_utils/stub_index_pattern';
import IndexPatternsFieldTypesProvider from 'ui/index_patterns/_field_types'; import IndexPatternsFieldTypesProvider from 'ui/index_patterns/_field_types';
import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields'; import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields';
export default function stubbedLogstashIndexPatternService(Private) { export default function stubbedLogstashIndexPatternService(Private) {
var StubIndexPattern = Private(TestUtilsStubIndexPatternProvider); let StubIndexPattern = Private(TestUtilsStubIndexPatternProvider);
var fieldTypes = Private(IndexPatternsFieldTypesProvider); let fieldTypes = Private(IndexPatternsFieldTypesProvider);
var mockLogstashFields = Private(FixturesLogstashFieldsProvider); let mockLogstashFields = Private(FixturesLogstashFieldsProvider);
var fields = mockLogstashFields.map(function (field) { let fields = mockLogstashFields.map(function (field) {
field.displayName = field.name; field.displayName = field.name;
var type = fieldTypes.byName[field.type]; let type = fieldTypes.byName[field.type];
if (!type) throw new TypeError('unknown type ' + field.type); if (!type) throw new TypeError('unknown type ' + field.type);
if (!_.has(field, 'sortable')) field.sortable = type.sortable; if (!_.has(field, 'sortable')) field.sortable = type.sortable;
if (!_.has(field, 'filterable')) field.filterable = type.filterable; if (!_.has(field, 'filterable')) field.filterable = type.filterable;
return field; return field;
}); });
var indexPattern = new StubIndexPattern('logstash-*', 'time', fields); let indexPattern = new StubIndexPattern('logstash-*', 'time', fields);
indexPattern.id = 'logstash-*'; indexPattern.id = 'logstash-*';
return indexPattern; return indexPattern;

View file

@ -3,8 +3,8 @@ import searchResponse from 'fixtures/search_response';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
export default function stubSearchSource(Private, $q, Promise) { export default function stubSearchSource(Private, $q, Promise) {
var deferedResult = $q.defer(); let deferedResult = $q.defer();
var indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); let indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
return { return {
sort: sinon.spy(), sort: sinon.spy(),

View file

@ -2,7 +2,7 @@ import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import VislibVisProvider from 'ui/vislib/vis'; import VislibVisProvider from 'ui/vislib/vis';
var $visCanvas = $('<div>') let $visCanvas = $('<div>')
.attr('id', 'vislib-vis-fixtures') .attr('id', 'vislib-vis-fixtures')
.css({ .css({
height: '500px', height: '500px',
@ -15,8 +15,8 @@ var $visCanvas = $('<div>')
}) })
.appendTo('body'); .appendTo('body');
var count = 0; let count = 0;
var visHeight = $visCanvas.height(); let visHeight = $visCanvas.height();
$visCanvas.new = function () { $visCanvas.new = function () {
count += 1; count += 1;
@ -32,7 +32,7 @@ afterEach(function () {
module.exports = function VislibFixtures(Private) { module.exports = function VislibFixtures(Private) {
return function (visLibParams) { return function (visLibParams) {
var Vis = Private(VislibVisProvider); let Vis = Private(VislibVisProvider);
return new Vis($visCanvas.new(), _.defaults({}, visLibParams || {}, { return new Vis($visCanvas.new(), _.defaults({}, visLibParams || {}, {
shareYAxis: true, shareYAxis: true,
addTooltip: true, addTooltip: true,

View file

@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
var fromRoot = require('path').resolve.bind(null, __dirname, '../../'); let fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) { if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json'); process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');

View file

@ -1,5 +1,8 @@
var cloneDeep = require('lodash').cloneDeep; // this file is not transpiled
var fromRoot = require('path').resolve.bind(null, __dirname, '../../'); 'use strict'; // eslint-disable-line strict
let cloneDeep = require('lodash').cloneDeep;
let fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) { if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json'); process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');

View file

@ -1,6 +1,6 @@
import LazyServer from './lazy_server'; import LazyServer from './lazy_server';
import LazyOptimizer from './lazy_optimizer'; import LazyOptimizer from './lazy_optimizer';
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
export default async (kbnServer, kibanaHapiServer, config) => { export default async (kbnServer, kibanaHapiServer, config) => {
let server = new LazyServer( let server = new LazyServer(

View file

@ -26,7 +26,7 @@ docViewsRegistry.register(function () {
}; };
$scope.showArrayInObjectsWarning = function (row, field) { $scope.showArrayInObjectsWarning = function (row, field) {
var value = $scope.flattened[field]; let value = $scope.flattened[field];
return _.isArray(value) && typeof value[0] === 'object'; return _.isArray(value) && typeof value[0] === 'object';
}; };
} }

View file

@ -5,6 +5,7 @@ import VisSchemasProvider from 'ui/vis/schemas';
import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json'; import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json';
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter'; import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html'; import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html';
export default function TileMapVisType(Private, getAppState, courier, config) { export default function TileMapVisType(Private, getAppState, courier, config) {
const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider);
const Schemas = Private(VisSchemasProvider); const Schemas = Private(VisSchemasProvider);
@ -120,6 +121,8 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
group: 'buckets', group: 'buckets',
name: 'split', name: 'split',
title: 'Split Chart', title: 'Split Chart',
deprecate: true,
deprecateMessage: 'The Split Chart feature for Tile Maps has been deprecated.',
min: 0, min: 0,
max: 1 max: 1
} }

View file

@ -1,60 +1,11 @@
<div dashboard-app class="app-container dashboard-container"> <div dashboard-app class="app-container dashboard-container">
<navbar name="dashboard-options" class="kibana-nav-options"> <kbn-top-nav name="dashboard" config="topNavMenu">
<div class="kibana-nav-info"> <div class="kibana-nav-info">
<span ng-show="dash.id" class="kibana-nav-info-title"> <span ng-show="dash.id" class="kibana-nav-info-title">
<span ng-bind="::dash.title"></span> <span ng-bind="::dash.title"></span>
</span> </span>
</div> </div>
</kbn-top-nav>
<div class="button-group kibana-nav-actions" role="toolbar">
<button ng-click="newDashboard()"
aria-label="New Dashboard">
<span>New</span>
</button>
<button
aria-label="Save Dashboard"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('save') }}"
ng-class="{active: configTemplate.is('save')}"
ng-click="configTemplate.toggle('save');">
<span>Save</span>
</button>
<button
aria-label="Load Saved Dashboard"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('load') }}"
ng-class="{active: configTemplate.is('load')}"
ng-click="configTemplate.toggle('load');">
<span>Open</span>
</button>
<button
aria-label="Share Dashboard"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('share') }}"
ng-class="{active: configTemplate.is('share')}"
ng-click="configTemplate.toggle('share');">
<span>Share</span>
</button>
<button
aria-label="Add Visualization"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('pickVis') }}"
ng-class="{active: configTemplate.is('pickVis')}"
ng-click="configTemplate.toggle('pickVis');">
<span>Add visualization</span>
</button>
<button
aria-label="Options"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('options') }}"
ng-class="{active: configTemplate.is('options')}"
ng-click="configTemplate.toggle('options');">
<span>Options</span>
</button>
<div class="chrome-actions"kbn-chrome-append-nav-controls></div>
</div>
</navbar>
<config config-template="configTemplate" config-object="opts"></config>
<navbar ng-show="chrome.getVisible()" name="dashboard-search"> <navbar ng-show="chrome.getVisible()" name="dashboard-search">
<form name="queryInput" <form name="queryInput"
@ -91,7 +42,7 @@
<div ng-show="!state.panels.length" class="text-center start-screen"> <div ng-show="!state.panels.length" class="text-center start-screen">
<h2>Ready to get started?</h2> <h2>Ready to get started?</h2>
<p>Click the <a class="btn btn-xs navbtn-inverse" ng-click="configTemplate.open('pickVis'); toggleAddVisualization = !toggleAddVisualization" aria-label="Add visualization"><i aria-hidden="true" class="fa fa-plus-circle"></i></a> button in the menu bar above to add a visualization to the dashboard. <br/>If you haven't setup a visualization yet visit the <a href="#/visualize" title="Visualize">"Visualize"</a> tab to create your first visualization.</p> <p>Click the <a class="btn btn-xs navbtn-inverse" ng-click="kbnTopNav.open('add'); toggleAddVisualization = !toggleAddVisualization" aria-label="Add visualization">Add</a> button in the menu bar above to add a visualization to the dashboard. <br/>If you haven't setup a visualization yet visit the <a href="#/visualize" title="Visualize">"Visualize"</a> tab to create your first visualization.</p>
</div> </div>
<dashboard-grid></dashboard-grid> <dashboard-grid></dashboard-grid>

View file

@ -1,14 +1,13 @@
import _ from 'lodash'; import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import angular from 'angular'; import angular from 'angular';
import ConfigTemplate from 'ui/config_template';
import chrome from 'ui/chrome'; import chrome from 'ui/chrome';
import 'ui/directives/config'; import 'ui/directives/kbn_top_nav';
import 'ui/courier'; import 'ui/courier';
import 'ui/config'; import 'ui/config';
import 'ui/notify'; import 'ui/notify';
import 'ui/typeahead'; import 'ui/typeahead';
import 'ui/navbar'; import 'ui/navbar_extensions';
import 'ui/share'; import 'ui/share';
import 'plugins/kibana/dashboard/directives/grid'; import 'plugins/kibana/dashboard/directives/grid';
import 'plugins/kibana/dashboard/components/panel/panel'; import 'plugins/kibana/dashboard/components/panel/panel';
@ -100,15 +99,27 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
}); });
$scope.$watch('state.options.darkTheme', setDarkTheme); $scope.$watch('state.options.darkTheme', setDarkTheme);
$scope.configTemplate = new ConfigTemplate({ $scope.topNavMenu = [{
save: require('plugins/kibana/dashboard/partials/save_dashboard.html'), key: 'new',
load: require('plugins/kibana/dashboard/partials/load_dashboard.html'), description: 'New Dashboard',
share: require('plugins/kibana/dashboard/partials/share.html'), run: function () { kbnUrl.change('/dashboard', {}); },
pickVis: require('plugins/kibana/dashboard/partials/pick_visualization.html'), }, {
options: require('plugins/kibana/dashboard/partials/options.html'), key: 'add',
filter: require('ui/chrome/config/filter.html'), description: 'Add a panel to the dashboard',
interval: require('ui/chrome/config/interval.html') template: require('plugins/kibana/dashboard/partials/pick_visualization.html')
}); }, {
key: 'save',
description: 'Save Dashboard',
template: require('plugins/kibana/dashboard/partials/save_dashboard.html')
}, {
key: 'open',
description: 'Load Saved Dashboard',
template: require('plugins/kibana/dashboard/partials/load_dashboard.html')
}, {
key: 'share',
description: 'Share Dashboard',
template: require('plugins/kibana/dashboard/partials/share.html')
}];
$scope.refresh = _.bindKey(courier, 'fetch'); $scope.refresh = _.bindKey(courier, 'fetch');
@ -198,7 +209,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
dash.save() dash.save()
.then(function (id) { .then(function (id) {
$scope.configTemplate.close('save'); $scope.kbnTopNav.close('save');
if (id) { if (id) {
notify.info('Saved Dashboard as "' + dash.title + '"'); notify.info('Saved Dashboard as "' + dash.title + '"');
if (dash.id !== $routeParams.id) { if (dash.id !== $routeParams.id) {

View file

@ -1,7 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import angular from 'angular'; import angular from 'angular';
import moment from 'moment'; import moment from 'moment';
import ConfigTemplate from 'ui/config_template';
import getSort from 'ui/doc_table/lib/get_sort'; import getSort from 'ui/doc_table/lib/get_sort';
import rison from 'ui/utils/rison'; import rison from 'ui/utils/rison';
import dateMath from 'ui/utils/date_math'; import dateMath from 'ui/utils/date_math';
@ -96,14 +95,23 @@ app.controller('discover', function ($scope, config, courier, $route, $window, N
$scope.toggleInterval = function () { $scope.toggleInterval = function () {
$scope.showInterval = !$scope.showInterval; $scope.showInterval = !$scope.showInterval;
}; };
// config panel templates $scope.topNavMenu = [{
$scope.configTemplate = new ConfigTemplate({ key: 'new',
load: require('plugins/kibana/discover/partials/load_search.html'), description: 'New Search',
save: require('plugins/kibana/discover/partials/save_search.html'), run: function () { kbnUrl.change('/discover'); }
share: require('plugins/kibana/discover/partials/share_search.html'), }, {
filter: require('ui/chrome/config/filter.html'), key: 'save',
interval: require('ui/chrome/config/interval.html') description: 'Save Search',
}); template: require('plugins/kibana/discover/partials/save_search.html')
}, {
key: 'open',
description: 'Load Saved Search',
template: require('plugins/kibana/discover/partials/load_search.html')
}, {
key: 'share',
description: 'Share Search',
template: require('plugins/kibana/discover/partials/share_search.html')
}];
$scope.timefilter = timefilter; $scope.timefilter = timefilter;
@ -287,7 +295,7 @@ app.controller('discover', function ($scope, config, courier, $route, $window, N
return savedSearch.save() return savedSearch.save()
.then(function (id) { .then(function (id) {
$scope.configTemplate.close('save'); $scope.kbnTopNav.close('save');
if (id) { if (id) {
notify.info('Saved Data Source "' + savedSearch.title + '"'); notify.info('Saved Data Source "' + savedSearch.title + '"');

View file

@ -1,5 +1,5 @@
<div ng-controller="discover" class="app-container"> <div ng-controller="discover" class="app-container">
<navbar name="discover-options" class="kibana-nav-options"> <kbn-top-nav name="discover" config="topNavMenu">
<div class="kibana-nav-info"> <div class="kibana-nav-info">
<span ng-show="opts.savedSearch.id" class="kibana-nav-info-title"> <span ng-show="opts.savedSearch.id" class="kibana-nav-info-title">
<span ng-bind="::opts.savedSearch.title"></span> <span ng-bind="::opts.savedSearch.title"></span>
@ -9,44 +9,7 @@
<strong class="discover-info-hits">{{(hits || 0) | number:0}}</strong> <strong class="discover-info-hits">{{(hits || 0) | number:0}}</strong>
<ng-pluralize count="hits" when="{'1':'hit', 'other':'hits'}"></ng-pluralize> <ng-pluralize count="hits" when="{'1':'hit', 'other':'hits'}"></ng-pluralize>
</div> </div>
<div class="kibana-nav-actions button-group" role="toolbar"> </kbn-top-nav>
<button
ng-click="newQuery()"
aria-label="New Search">
<span>New</span>
</button>
<button
ng-click="configTemplate.toggle('save');"
ng-class="{active: configTemplate.is('save')}"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('save') }}"
aria-label="Save Search">
<span>Save</span>
</button>
<button
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('load') }}"
ng-click="configTemplate.toggle('load');"
ng-class="{active: configTemplate.is('load')}"
aria-label="Load Saved Search">
<span>Open</span>
</button>
<button
aria-label="Share Search"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('share') }}"
ng-class="{active: configTemplate.is('share')}"
ng-click="configTemplate.toggle('share');">
<span>Share</span>
</button>
<div class="chrome-actions" kbn-chrome-append-nav-controls></div>
</div>
</navbar>
<config
config-template="configTemplate"
config-object="opts"
config-close="configClose"
></config>
<navbar name="discover-search"> <navbar name="discover-search">
<form role="form" class="fill inline-form" ng-submit="fetch()" name="discoverSearch"> <form role="form" class="fill inline-form" ng-submit="fetch()" name="discoverSearch">
<div class="typeahead" kbn-typeahead="discover"> <div class="typeahead" kbn-typeahead="discover">
@ -116,7 +79,7 @@
<div ng-show="opts.timefield"> <div ng-show="opts.timefield">
<p> <p>
<h3>Expand your time range</h3> <h3>Expand your time range</h3>
<p>I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Click the button below to open the time picker. For future reference you can open the time picker by clicking the <a class="btn btn-xs navbtn" ng-click="toggleTimepicker(); toggledTimepicker = !toggledTimepicker" aria-expanded="{{toggledTimepicker}}" aria-label="time picker">time picker <i aria-hidden="true" class="fa fa-clock-o"></i></a> in the top right corner of your screen. <p>I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Try selecting a wider time range by opening the time picker <i aria-hidden="true" class="fa fa-clock-o"></i> in the top right corner of your screen.
</p> </p>
</div> </div>

View file

@ -1,6 +1,6 @@
import 'plugins/kibana/discover/saved_searches/saved_searches'; import 'plugins/kibana/discover/saved_searches/saved_searches';
import 'plugins/kibana/discover/directives/timechart'; import 'plugins/kibana/discover/directives/timechart';
import 'ui/navbar'; import 'ui/navbar_extensions';
import 'ui/collapsible_sidebar'; import 'ui/collapsible_sidebar';
import 'plugins/kibana/discover/components/field_chooser/field_chooser'; import 'plugins/kibana/discover/components/field_chooser/field_chooser';
import 'plugins/kibana/discover/controllers/discover'; import 'plugins/kibana/discover/controllers/discover';

View file

@ -7,7 +7,7 @@
<input id="SaveSearch" ng-model="opts.savedSearch.title" input-focus="select" class="form-control" placeholder="Name this search..."> <input id="SaveSearch" ng-model="opts.savedSearch.title" input-focus="select" class="form-control" placeholder="Name this search...">
</div> </div>
<div class="form-group"> <div class="form-group">
<button ng-disabled="!opts.savedSearch.title" type="submit" class="btn btn-primary"> <button ng-disabled="!opts.savedSearch.title" data-test-subj="discover-save-search-btn" type="submit" class="btn btn-primary">
Save Save
</button> </button>
</div> </div>

View file

@ -40,12 +40,11 @@ uiModules
}, },
link: function ($scope, $el) { link: function ($scope, $el) {
timefilter.enabled = false; timefilter.enabled = false;
$scope.sections = sections;
$scope.sections = sections.inOrder; $scope.sections = sections.inOrder;
$scope.section = _.find($scope.sections, { name: $scope.sectionName }); $scope.section = _.find($scope.sections, { name: $scope.sectionName });
$scope.sections.forEach(function (section) { $scope.sections.forEach(section => {
section.class = (section === $scope.section) ? 'active' : void 0; section.class = section === $scope.section ? 'active' : undefined;
}); });
} }
}; };

View file

@ -2,6 +2,7 @@
<label>Select {{ groupName }} type</label> <label>Select {{ groupName }} type</label>
<ul class="form-group list-group list-group-menu"> <ul class="form-group list-group list-group-menu">
<li <li
ng-hide="schema.deprecate"
ng-repeat="schema in availableSchema" ng-repeat="schema in availableSchema"
ng-click="add.submit(schema)" ng-click="add.submit(schema)"
class="list-group-item list-group-menu-item"> class="list-group-item list-group-menu-item">
@ -18,10 +19,10 @@
class="vis-editor-agg-wide-btn"> class="vis-editor-agg-wide-btn">
<div ng-if="!add.form"> <div ng-if="!add.form">
<div class="btn btn-sm btn-primary" ng-if="groupName !== 'buckets' || !stats.count"> <div class="btn btn-sm btn-primary" ng-if="groupName !== 'buckets' || !stats.count && !stats.deprecate">
Add {{ groupName }} Add {{ groupName }}
</div> </div>
<div class="btn btn-sm btn-primary" ng-if="groupName === 'buckets' && stats.count > 0"> <div class="btn btn-sm btn-primary" ng-if="groupName === 'buckets' && stats.count > 0 && !stats.deprecate">
Add sub-{{ groupName }} Add sub-{{ groupName }}
</div> </div>
</div> </div>

View file

@ -33,6 +33,7 @@ uiModules
$scope.schemas.forEach(function (schema) { $scope.schemas.forEach(function (schema) {
stats.min += schema.min; stats.min += schema.min;
stats.max += schema.max; stats.max += schema.max;
stats.deprecate = schema.deprecate;
}); });
$scope.availableSchema = $scope.schemas.filter(function (schema) { $scope.availableSchema = $scope.schemas.filter(function (schema) {

View file

@ -10,4 +10,13 @@
style="display: none;"> style="display: none;">
</div> </div>
<div ng-if="agg.schema.deprecate" class="form-group">
<p ng-show="agg.schema.deprecateMessage" class="vis-editor-agg-error">
{{ agg.schema.deprecateMessage }}
</p>
<p ng-show="!agg.schema.deprecateMessage" class="vis-editor-agg-error">
"{{ agg.schema.title }}" has been deprecated.
</p>
</div>
<!-- schema editors get added down here: aggSelect.html, agg_types/controls/*.html --> <!-- schema editors get added down here: aggSelect.html, agg_types/controls/*.html -->

View file

@ -1,60 +1,12 @@
<div ng-controller="VisEditor" class="app-container vis-editor vis-type-{{ vis.type.name }}"> <div ng-controller="VisEditor" class="app-container vis-editor vis-type-{{ vis.type.name }}">
<navbar name="visualize-options" class="kibana-nav-options" ng-if="chrome.getVisible()">
<kbn-top-nav name="visualize" config="topNavMenu">
<div class="vis-editor-info"> <div class="vis-editor-info">
<span ng-show="savedVis.id" class="vis-editor-info-title"> <span ng-show="savedVis.id" class="vis-editor-info-title">
<span ng-bind="::savedVis.title"></span> <span ng-bind="::savedVis.title"></span>
</span> </span>
</div> </div>
</kbn-top-nav>
<div class="button-group kibana-nav-actions">
<button ng-click="startOver()" aria-label="New Visualization">
<span>New</span>
</button>
<!-- normal save -->
<button
ng-class="{active: configTemplate.is('save')}"
ng-click="configTemplate.toggle('save')"
ng-if="!editableVis.dirty"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('save') }}"
aria-label="Save Visualization">
<span>Save</span>
</button>
<!-- save stub with tooltip -->
<button disabled ng-if="editableVis.dirty" tooltip="Apply or Discard your changes before saving" aria-label="Apply or Discard your changes before saving">
<span>Save</span>
</button>
<button
ng-class="{active: configTemplate.is('load')}"
ng-click="configTemplate.toggle('load')"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('load') }}"
aria-label="Load Saved Visualization">
<span>Load</span>
</button>
<button
ng-class="{active: configTemplate.is('share')}"
ng-click="configTemplate.toggle('share')"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('share') }}"
aria-label="Share Visualization">
<span>Share</span>
</button>
<button
ng-click="fetch()"
aria-label="Refresh">
<span>Refresh</span>
</button>
<div class="chrome-actions"kbn-chrome-append-nav-controls></div>
</div>
</navbar>
<config
ng-if="chrome.getVisible()"
config-template="configTemplate"
config-object="opts">
</config>
<navbar ng-if="chrome.getVisible()" name="visualize-search"> <navbar ng-if="chrome.getVisible()" name="visualize-search">
<div class="fill bitty-modal-container"> <div class="fill bitty-modal-container">
<div ng-if="vis.type.requiresSearch && $state.linked && !unlinking" <div ng-if="vis.type.requiresSearch && $state.linked && !unlinking"

View file

@ -2,12 +2,11 @@ import _ from 'lodash';
import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations';
import 'plugins/kibana/visualize/editor/sidebar'; import 'plugins/kibana/visualize/editor/sidebar';
import 'plugins/kibana/visualize/editor/agg_filter'; import 'plugins/kibana/visualize/editor/agg_filter';
import 'ui/navbar'; import 'ui/navbar_extensions';
import 'ui/visualize'; import 'ui/visualize';
import 'ui/collapsible_sidebar'; import 'ui/collapsible_sidebar';
import 'ui/share'; import 'ui/share';
import angular from 'angular'; import angular from 'angular';
import ConfigTemplate from 'ui/config_template';
import Notifier from 'ui/notify/notifier'; import Notifier from 'ui/notify/notifier';
import RegistryVisTypesProvider from 'ui/registry/vis_types'; import RegistryVisTypesProvider from 'ui/registry/vis_types';
import DocTitleProvider from 'ui/doc_title'; import DocTitleProvider from 'ui/doc_title';
@ -80,14 +79,27 @@ uiModules
const searchSource = savedVis.searchSource; const searchSource = savedVis.searchSource;
// config panel templates $scope.topNavMenu = [{
const configTemplate = new ConfigTemplate({ key: 'new',
save: require('plugins/kibana/visualize/editor/panels/save.html'), description: 'New Visualization',
load: require('plugins/kibana/visualize/editor/panels/load.html'), run: function () { kbnUrl.change('/visualize', {}); }
share: require('plugins/kibana/visualize/editor/panels/share.html'), }, {
filter: require('ui/chrome/config/filter.html'), key: 'save',
interval: require('ui/chrome/config/interval.html') template: require('plugins/kibana/visualize/editor/panels/save.html'),
}); description: 'Save Visualization'
}, {
key: 'load',
template: require('plugins/kibana/visualize/editor/panels/load.html'),
description: 'Load Saved Visualization',
}, {
key: 'share',
template: require('plugins/kibana/visualize/editor/panels/share.html'),
description: 'Share Visualization'
}, {
key: 'refresh',
description: 'Refresh',
run: function () { $scope.fetch(); }
}];
if (savedVis.id) { if (savedVis.id) {
docTitle.change(savedVis.title); docTitle.change(savedVis.title);
@ -129,7 +141,6 @@ uiModules
$scope.uiState = $state.makeStateful('uiState'); $scope.uiState = $state.makeStateful('uiState');
$scope.timefilter = timefilter; $scope.timefilter = timefilter;
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter'); $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter');
$scope.configTemplate = configTemplate;
editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state); editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state);
editableVis.listeners.brush = vis.listeners.brush = brushEvent; editableVis.listeners.brush = vis.listeners.brush = brushEvent;
@ -235,7 +246,7 @@ uiModules
savedVis.save() savedVis.save()
.then(function (id) { .then(function (id) {
configTemplate.close('save'); $scope.kbnTopNav.close('save');
if (id) { if (id) {
notify.info('Saved Visualization "' + savedVis.title + '"'); notify.info('Saved Visualization "' + savedVis.title + '"');

View file

@ -1,5 +1,5 @@
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
import { chain, memoize } from 'lodash'; import { chain, memoize } from 'lodash';
import { resolve } from 'path'; import { resolve } from 'path';
import { map, fromNode } from 'bluebird'; import { map, fromNode } from 'bluebird';

View file

@ -1,7 +1,7 @@
import { union } from 'lodash'; import { union } from 'lodash';
import findSourceFiles from './find_source_files'; import findSourceFiles from './find_source_files';
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
export default (kibana) => { export default (kibana) => {
return new kibana.Plugin({ return new kibana.Plugin({

View file

@ -9,12 +9,12 @@ import Joi from 'joi';
* *
* Config should be newed up with a joi schema (containing defaults via joi) * Config should be newed up with a joi schema (containing defaults via joi)
* *
* var schema = { ... } * let schema = { ... }
* new Config(schema); * new Config(schema);
* *
*/ */
var data = { let data = {
test: { test: {
hosts: ['host-01', 'host-02'], hosts: ['host-01', 'host-02'],
client: { client: {
@ -25,7 +25,7 @@ var data = {
} }
}; };
var schema = Joi.object({ let schema = Joi.object({
test: Joi.object({ test: Joi.object({
enable: Joi.boolean().default(true), enable: Joi.boolean().default(true),
hosts: Joi.array().items(Joi.string()), hosts: Joi.array().items(Joi.string()),
@ -44,39 +44,39 @@ describe('lib/config/config', function () {
describe('constructor', function () { describe('constructor', function () {
it('should not allow any config if the schema is not passed', function () { it('should not allow any config if the schema is not passed', function () {
var config = new Config(); let config = new Config();
var run = function () { let run = function () {
config.set('something.enable', true); config.set('something.enable', true);
}; };
expect(run).to.throwException(); expect(run).to.throwException();
}); });
it('should allow keys in the schema', function () { it('should allow keys in the schema', function () {
var config = new Config(schema); let config = new Config(schema);
var run = function () { let run = function () {
config.set('test.client.host', 'http://0.0.0.0'); config.set('test.client.host', 'http://0.0.0.0');
}; };
expect(run).to.not.throwException(); expect(run).to.not.throwException();
}); });
it('should not allow keys not in the schema', function () { it('should not allow keys not in the schema', function () {
var config = new Config(schema); let config = new Config(schema);
var run = function () { let run = function () {
config.set('paramNotDefinedInTheSchema', true); config.set('paramNotDefinedInTheSchema', true);
}; };
expect(run).to.throwException(); expect(run).to.throwException();
}); });
it('should not allow child keys not in the schema', function () { it('should not allow child keys not in the schema', function () {
var config = new Config(schema); let config = new Config(schema);
var run = function () { let run = function () {
config.set('test.client.paramNotDefinedInTheSchema', true); config.set('test.client.paramNotDefinedInTheSchema', true);
}; };
expect(run).to.throwException(); expect(run).to.throwException();
}); });
it('should set defaults', function () { it('should set defaults', function () {
var config = new Config(schema); let config = new Config(schema);
expect(config.get('test.enable')).to.be(true); expect(config.get('test.enable')).to.be(true);
expect(config.get('test.client.type')).to.be('datastore'); expect(config.get('test.client.type')).to.be('datastore');
}); });
@ -92,7 +92,7 @@ describe('lib/config/config', function () {
it('should reset the config object with new values', function () { it('should reset the config object with new values', function () {
config.set(data); config.set(data);
var newData = config.get(); let newData = config.get();
newData.test.enable = false; newData.test.enable = false;
config.resetTo(newData); config.resetTo(newData);
expect(config.get()).to.eql(newData); expect(config.get()).to.eql(newData);
@ -134,21 +134,21 @@ describe('lib/config/config', function () {
}); });
it('should use an object to set config values', function () { it('should use an object to set config values', function () {
var hosts = ['host-01', 'host-02']; let hosts = ['host-01', 'host-02'];
config.set({ test: { enable: false, hosts: hosts } }); config.set({ test: { enable: false, hosts: hosts } });
expect(config.get('test.enable')).to.be(false); expect(config.get('test.enable')).to.be(false);
expect(config.get('test.hosts')).to.eql(hosts); expect(config.get('test.hosts')).to.eql(hosts);
}); });
it('should use a flatten object to set config values', function () { it('should use a flatten object to set config values', function () {
var hosts = ['host-01', 'host-02']; let hosts = ['host-01', 'host-02'];
config.set({ 'test.enable': false, 'test.hosts': hosts }); config.set({ 'test.enable': false, 'test.hosts': hosts });
expect(config.get('test.enable')).to.be(false); expect(config.get('test.enable')).to.be(false);
expect(config.get('test.hosts')).to.eql(hosts); expect(config.get('test.hosts')).to.eql(hosts);
}); });
it('should override values with just the values present', function () { it('should override values with just the values present', function () {
var newData = _.cloneDeep(data); let newData = _.cloneDeep(data);
config.set(data); config.set(data);
newData.test.enable = false; newData.test.enable = false;
config.set({ test: { enable: false } }); config.set({ test: { enable: false } });
@ -156,7 +156,7 @@ describe('lib/config/config', function () {
}); });
it('should thow an exception when setting a value with the wrong type', function (done) { it('should thow an exception when setting a value with the wrong type', function (done) {
var run = function () { let run = function () {
config.set('test.enable', 'something'); config.set('test.enable', 'something');
}; };
expect(run).to.throwException(function (err) { expect(run).to.throwException(function (err) {
@ -179,7 +179,7 @@ describe('lib/config/config', function () {
}); });
it('should return the whole config object when called without a key', function () { it('should return the whole config object when called without a key', function () {
var newData = _.cloneDeep(data); let newData = _.cloneDeep(data);
newData.test.enable = true; newData.test.enable = true;
expect(config.get()).to.eql(newData); expect(config.get()).to.eql(newData);
}); });
@ -194,14 +194,14 @@ describe('lib/config/config', function () {
}); });
it('should throw exception for unknown config values', function () { it('should throw exception for unknown config values', function () {
var run = function () { let run = function () {
config.get('test.does.not.exist'); config.get('test.does.not.exist');
}; };
expect(run).to.throwException(/Unknown config key: test.does.not.exist/); expect(run).to.throwException(/Unknown config key: test.does.not.exist/);
}); });
it('should not throw exception for undefined known config values', function () { it('should not throw exception for undefined known config values', function () {
var run = function getUndefValue() { let run = function getUndefValue() {
config.get('test.undefValue'); config.get('test.undefValue');
}; };
expect(run).to.not.throwException(); expect(run).to.not.throwException();
@ -216,13 +216,13 @@ describe('lib/config/config', function () {
}); });
it('should allow you to extend the schema at the top level', function () { it('should allow you to extend the schema at the top level', function () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('myTest', newSchema); config.extendSchema('myTest', newSchema);
expect(config.get('myTest.test')).to.be(true); expect(config.get('myTest.test')).to.be(true);
}); });
it('should allow you to extend the schema with a prefix', function () { it('should allow you to extend the schema with a prefix', function () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('prefix.myTest', newSchema); config.extendSchema('prefix.myTest', newSchema);
expect(config.get('prefix')).to.eql({ myTest: { test: true }}); expect(config.get('prefix')).to.eql({ myTest: { test: true }});
expect(config.get('prefix.myTest')).to.eql({ test: true }); expect(config.get('prefix.myTest')).to.eql({ test: true });
@ -230,8 +230,8 @@ describe('lib/config/config', function () {
}); });
it('should NOT allow you to extend the schema if somethign else is there', function () { it('should NOT allow you to extend the schema if somethign else is there', function () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
var run = function () { let run = function () {
config.extendSchema('test', newSchema); config.extendSchema('test', newSchema);
}; };
expect(run).to.throwException(); expect(run).to.throwException();
@ -241,7 +241,7 @@ describe('lib/config/config', function () {
describe('#removeSchema(key)', function () { describe('#removeSchema(key)', function () {
it('should completely remove the key', function () { it('should completely remove the key', function () {
var config = new Config(Joi.object().keys({ let config = new Config(Joi.object().keys({
a: Joi.number().default(1) a: Joi.number().default(1)
})); }));
@ -251,7 +251,7 @@ describe('lib/config/config', function () {
}); });
it('only removes existing keys', function () { it('only removes existing keys', function () {
var config = new Config(Joi.object()); let config = new Config(Joi.object());
expect(() => config.removeSchema('b')).to.throwException('Unknown schema'); expect(() => config.removeSchema('b')).to.throwException('Unknown schema');
}); });

View file

@ -4,7 +4,7 @@ import expect from 'expect.js';
describe('explode_by(dot, flatObject)', function () { describe('explode_by(dot, flatObject)', function () {
it('should explode a flatten object with dots', function () { it('should explode a flatten object with dots', function () {
var flatObject = { let flatObject = {
'test.enable': true, 'test.enable': true,
'test.hosts': ['host-01', 'host-02'] 'test.hosts': ['host-01', 'host-02']
}; };
@ -17,7 +17,7 @@ describe('explode_by(dot, flatObject)', function () {
}); });
it('should explode a flatten object with slashes', function () { it('should explode a flatten object with slashes', function () {
var flatObject = { let flatObject = {
'test/enable': true, 'test/enable': true,
'test/hosts': ['host-01', 'host-02'] 'test/hosts': ['host-01', 'host-02']
}; };

View file

@ -4,7 +4,7 @@ import expect from 'expect.js';
describe('flatten_with(dot, nestedObj)', function () { describe('flatten_with(dot, nestedObj)', function () {
it('should flatten object with dot', function () { it('should flatten object with dot', function () {
var nestedObj = { let nestedObj = {
test: { test: {
enable: true, enable: true,
hosts: ['host-01', 'host-02'], hosts: ['host-01', 'host-02'],

View file

@ -4,7 +4,7 @@ import expect from 'expect.js';
describe('override(target, source)', function () { describe('override(target, source)', function () {
it('should override the values form source to target', function () { it('should override the values form source to target', function () {
var target = { let target = {
test: { test: {
enable: true, enable: true,
host: ['host-01', 'host-02'], host: ['host-01', 'host-02'],
@ -13,7 +13,7 @@ describe('override(target, source)', function () {
} }
} }
}; };
var source = { test: { client: { type: 'nosql' } } }; let source = { test: { client: { type: 'nosql' } } };
expect(override(target, source)).to.eql({ expect(override(target, source)).to.eql({
test: { test: {
enable: true, enable: true,

View file

@ -1,10 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
module.exports = function (dot, flatObject) { module.exports = function (dot, flatObject) {
var fullObject = {}; let fullObject = {};
_.each(flatObject, function (value, key) { _.each(flatObject, function (value, key) {
var keys = key.split(dot); let keys = key.split(dot);
(function walk(memo, keys, value) { (function walk(memo, keys, value) {
var _key = keys.shift(); let _key = keys.shift();
if (keys.length === 0) { if (keys.length === 0) {
memo[_key] = value; memo[_key] = value;
} else { } else {

View file

@ -1,8 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
module.exports = function (dot, nestedObj, flattenArrays) { module.exports = function (dot, nestedObj, flattenArrays) {
let key; // original key let key; // original key
var stack = []; // track key stack let stack = []; // track key stack
var flatObj = {}; let flatObj = {};
(function flattenObj(obj) { (function flattenObj(obj) {
_.keys(obj).forEach(function (key) { _.keys(obj).forEach(function (key) {
stack.push(key); stack.push(key);

View file

@ -3,8 +3,8 @@ import flattenWith from './flatten_with';
import explodeBy from './explode_by'; import explodeBy from './explode_by';
module.exports = function (target, source) { module.exports = function (target, source) {
var _target = flattenWith('.', target); let _target = flattenWith('.', target);
var _source = flattenWith('.', source); let _source = flattenWith('.', source);
return explodeBy('.', _.defaults(_source, _target)); return explodeBy('.', _.defaults(_source, _target));
}; };

View file

@ -5,7 +5,7 @@ import { get } from 'lodash';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import os from 'os'; import os from 'os';
import fromRoot from '../../utils/from_root'; import { fromRoot } from '../../utils';
module.exports = () => Joi.object({ module.exports = () => Joi.object({
pkg: Joi.object({ pkg: Joi.object({

View file

@ -1,8 +1,8 @@
import Hapi from 'hapi'; import Hapi from 'hapi';
import { constant, once, compact, flatten } from 'lodash'; import { constant, once, compact, flatten } from 'lodash';
import { promisify, resolve, fromNode } from 'bluebird'; import { promisify, resolve, fromNode } from 'bluebird';
import fromRoot from '../utils/from_root'; import { isWorker } from 'cluster';
import pkg from '../utils/package_json'; import { fromRoot, pkg } from '../utils';
let rootDir = fromRoot('.'); let rootDir = fromRoot('.');
@ -78,6 +78,11 @@ module.exports = class KbnServer {
await this.ready(); await this.ready();
await fromNode(cb => server.start(cb)); await fromNode(cb => server.start(cb));
if (isWorker) {
// help parent process know when we are ready
process.send(['WORKER_LISTENING']);
}
server.log(['listening', 'info'], `Server running at ${server.info.uri}`); server.log(['listening', 'info'], `Server running at ${server.info.uri}`);
return server; return server;
} }

View file

@ -3,7 +3,7 @@ import expect from 'expect.js';
describe('applyFiltersToKeys(obj, actionsByKey)', function () { describe('applyFiltersToKeys(obj, actionsByKey)', function () {
it('applies for each key+prop in actionsByKey', function () { it('applies for each key+prop in actionsByKey', function () {
var data = applyFiltersToKeys({ let data = applyFiltersToKeys({
a: { a: {
b: { b: {
c: 1 c: 1

View file

@ -21,7 +21,7 @@ function apply(obj, key, action) {
obj[k] = ('' + val).replace(/./g, 'X'); obj[k] = ('' + val).replace(/./g, 'X');
} }
else if (/\/.+\//.test(action)) { else if (/\/.+\//.test(action)) {
var matches = action.match(/\/(.+)\//); let matches = action.match(/\/(.+)\//);
if (matches) { if (matches) {
let regex = new RegExp(matches[1]); let regex = new RegExp(matches[1]);
obj[k] = ('' + val).replace(regex, replacer); obj[k] = ('' + val).replace(regex, replacer);

View file

@ -41,13 +41,13 @@ module.exports = class TransformObjStream extends Stream.Transform {
} }
_transform(event, enc, next) { _transform(event, enc, next) {
var data = this.filter(this.readEvent(event)); let data = this.filter(this.readEvent(event));
this.push(this.format(data) + '\n'); this.push(this.format(data) + '\n');
next(); next();
} }
readEvent(event) { readEvent(event) {
var data = { let data = {
type: event.event, type: event.event,
'@timestamp': moment.utc(event.timestamp).format(), '@timestamp': moment.utc(event.timestamp).format(),
tags: [].concat(event.tags || []), tags: [].concat(event.tags || []),
@ -69,7 +69,7 @@ module.exports = class TransformObjStream extends Stream.Transform {
referer: event.source.referer referer: event.source.referer
}; };
var contentLength = 0; let contentLength = 0;
if (typeof event.responsePayload === 'object') { if (typeof event.responsePayload === 'object') {
contentLength = stringify(event.responsePayload).length; contentLength = stringify(event.responsePayload).length;
} else { } else {
@ -82,7 +82,7 @@ module.exports = class TransformObjStream extends Stream.Transform {
contentLength: contentLength contentLength: contentLength
}; };
var query = querystring.stringify(event.query); let query = querystring.stringify(event.query);
if (query) data.req.url += '?' + query; if (query) data.req.url += '?' + query;

View file

@ -2,19 +2,19 @@ import _ from 'lodash';
import Boom from 'boom'; import Boom from 'boom';
import Promise from 'bluebird'; import Promise from 'bluebird';
import { unlinkSync as unlink } from 'fs'; import { unlinkSync as unlink } from 'fs';
var writeFile = Promise.promisify(require('fs').writeFile); let writeFile = Promise.promisify(require('fs').writeFile);
module.exports = Promise.method(function (kbnServer, server, config) { module.exports = Promise.method(function (kbnServer, server, config) {
var path = config.get('pid.file'); let path = config.get('pid.file');
if (!path) return; if (!path) return;
var pid = String(process.pid); let pid = String(process.pid);
return writeFile(path, pid, { flag: 'wx' }) return writeFile(path, pid, { flag: 'wx' })
.catch(function (err) { .catch(function (err) {
if (err.code !== 'EEXIST') throw err; if (err.code !== 'EEXIST') throw err;
var log = { let log = {
tmpl: 'pid file already exists at <%= path %>', tmpl: 'pid file already exists at <%= path %>',
path: path, path: path,
pid: pid pid: pid
@ -36,7 +36,7 @@ module.exports = Promise.method(function (kbnServer, server, config) {
pid: pid pid: pid
}); });
var clean = _.once(function (code) { let clean = _.once(function (code) {
unlink(path); unlink(path);
}); });

View file

@ -6,7 +6,7 @@ import { each } from 'bluebird';
import PluginCollection from './plugin_collection'; import PluginCollection from './plugin_collection';
module.exports = async (kbnServer, server, config) => { module.exports = async (kbnServer, server, config) => {
var plugins = kbnServer.plugins = new PluginCollection(kbnServer); let plugins = kbnServer.plugins = new PluginCollection(kbnServer);
let scanDirs = [].concat(config.get('plugins.scanDirs') || []); let scanDirs = [].concat(config.get('plugins.scanDirs') || []);
let pluginPaths = [].concat(config.get('plugins.paths') || []); let pluginPaths = [].concat(config.get('plugins.paths') || []);

View file

@ -17,21 +17,21 @@ describe('ServerStatus class', function () {
describe('#create(name)', function () { describe('#create(name)', function () {
it('should create a new status by name', function () { it('should create a new status by name', function () {
var status = serverStatus.create('name'); let status = serverStatus.create('name');
expect(status).to.be.a(Status); expect(status).to.be.a(Status);
}); });
}); });
describe('#get(name)', function () { describe('#get(name)', function () {
it('exposes plugins by name', function () { it('exposes plugins by name', function () {
var status = serverStatus.create('name'); let status = serverStatus.create('name');
expect(serverStatus.get('name')).to.be(status); expect(serverStatus.get('name')).to.be(status);
}); });
}); });
describe('#getState(name)', function () { describe('#getState(name)', function () {
it('should expose the state of the plugin by name', function () { it('should expose the state of the plugin by name', function () {
var status = serverStatus.create('name'); let status = serverStatus.create('name');
status.green(); status.green();
expect(serverStatus.getState('name')).to.be('green'); expect(serverStatus.getState('name')).to.be('green');
}); });
@ -39,11 +39,11 @@ describe('ServerStatus class', function () {
describe('#overall()', function () { describe('#overall()', function () {
it('considers each status to produce a summary', function () { it('considers each status to produce a summary', function () {
var status = serverStatus.create('name'); let status = serverStatus.create('name');
expect(serverStatus.overall().state).to.be('uninitialized'); expect(serverStatus.overall().state).to.be('uninitialized');
var match = function (overall, state) { let match = function (overall, state) {
expect(overall).to.have.property('state', state.id); expect(overall).to.have.property('state', state.id);
expect(overall).to.have.property('title', state.title); expect(overall).to.have.property('title', state.title);
expect(overall).to.have.property('icon', state.icon); expect(overall).to.have.property('icon', state.icon);
@ -65,20 +65,20 @@ describe('ServerStatus class', function () {
describe('#toJSON()', function () { describe('#toJSON()', function () {
it('serializes to overall status and individuals', function () { it('serializes to overall status and individuals', function () {
var one = serverStatus.create('one'); let one = serverStatus.create('one');
var two = serverStatus.create('two'); let two = serverStatus.create('two');
var three = serverStatus.create('three'); let three = serverStatus.create('three');
one.green(); one.green();
two.yellow(); two.yellow();
three.red(); three.red();
var obj = JSON.parse(JSON.stringify(serverStatus)); let obj = JSON.parse(JSON.stringify(serverStatus));
expect(obj).to.have.property('overall'); expect(obj).to.have.property('overall');
expect(obj.overall.state).to.eql(serverStatus.overall().state); expect(obj.overall.state).to.eql(serverStatus.overall().state);
expect(obj.statuses).to.have.length(3); expect(obj.statuses).to.have.length(3);
var outs = _.indexBy(obj.statuses, 'name'); let outs = _.indexBy(obj.statuses, 'name');
expect(outs.one).to.have.property('state', 'green'); expect(outs.one).to.have.property('state', 'green');
expect(outs.two).to.have.property('state', 'yellow'); expect(outs.two).to.have.property('state', 'yellow');
expect(outs.three).to.have.property('state', 'red'); expect(outs.three).to.have.property('state', 'red');

View file

@ -17,7 +17,7 @@ describe('Status class', function () {
}); });
it('emits change when the status is set', function (done) { it('emits change when the status is set', function (done) {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
status.once('change', function (prev, prevMsg) { status.once('change', function (prev, prevMsg) {
expect(status.state).to.be('green'); expect(status.state).to.be('green');
@ -40,8 +40,8 @@ describe('Status class', function () {
}); });
it('should only trigger the change listener when something changes', function () { it('should only trigger the change listener when something changes', function () {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var stub = sinon.stub(); let stub = sinon.stub();
status.on('change', stub); status.on('change', stub);
status.green('Ready'); status.green('Ready');
status.green('Ready'); status.green('Ready');
@ -50,17 +50,17 @@ describe('Status class', function () {
}); });
it('should create a JSON representation of the status', function () { it('should create a JSON representation of the status', function () {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
status.green('Ready'); status.green('Ready');
var json = status.toJSON(); let json = status.toJSON();
expect(json.state).to.eql('green'); expect(json.state).to.eql('green');
expect(json.message).to.eql('Ready'); expect(json.message).to.eql('Ready');
}); });
it('should call on handler if status is already matched', function (done) { it('should call on handler if status is already matched', function (done) {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var msg = 'Test Ready'; let msg = 'Test Ready';
status.green(msg); status.green(msg);
status.on('green', function (prev, prevMsg) { status.on('green', function (prev, prevMsg) {
@ -73,8 +73,8 @@ describe('Status class', function () {
}); });
it('should call once handler if status is already matched', function (done) { it('should call once handler if status is already matched', function (done) {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var msg = 'Test Ready'; let msg = 'Test Ready';
status.green(msg); status.green(msg);
status.once('green', function (prev, prevMsg) { status.once('green', function (prev, prevMsg) {
@ -88,16 +88,16 @@ describe('Status class', function () {
function testState(color) { function testState(color) {
it(`should change the state to ${color} when #${color}() is called`, function () { it(`should change the state to ${color} when #${color}() is called`, function () {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var message = 'testing ' + color; let message = 'testing ' + color;
status[color](message); status[color](message);
expect(status).to.have.property('state', color); expect(status).to.have.property('state', color);
expect(status).to.have.property('message', message); expect(status).to.have.property('message', message);
}); });
it(`should trigger the "change" listner when #${color}() is called`, function (done) { it(`should trigger the "change" listner when #${color}() is called`, function (done) {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var message = 'testing ' + color; let message = 'testing ' + color;
status.on('change', function (prev, prevMsg) { status.on('change', function (prev, prevMsg) {
expect(status.state).to.be(color); expect(status.state).to.be(color);
expect(status.message).to.be(message); expect(status.message).to.be(message);
@ -110,8 +110,8 @@ describe('Status class', function () {
}); });
it(`should trigger the "${color}" listner when #${color}() is called`, function (done) { it(`should trigger the "${color}" listner when #${color}() is called`, function (done) {
var status = serverStatus.create('test'); let status = serverStatus.create('test');
var message = 'testing ' + color; let message = 'testing ' + color;
status.on(color, function (prev, prevMsg) { status.on(color, function (prev, prevMsg) {
expect(status.state).to.be(color); expect(status.state).to.be(color);
expect(status.message).to.be(message); expect(status.message).to.be(message);

View file

@ -22,8 +22,8 @@ module.exports = function (kbnServer, server, config) {
}); });
server.decorate('reply', 'renderStatusPage', function () { server.decorate('reply', 'renderStatusPage', function () {
var app = kbnServer.uiExports.getHiddenApp('status_page'); let app = kbnServer.uiExports.getHiddenApp('status_page');
var resp = app ? this.renderApp(app) : this(kbnServer.status.toString()); let resp = app ? this.renderApp(app) : this(kbnServer.status.toString());
resp.code(kbnServer.status.isGreen() ? 200 : 503); resp.code(kbnServer.status.isGreen() ? 200 : 503);
return resp; return resp;
}); });

View file

@ -10,7 +10,7 @@ module.exports = function (kbnServer, server, config) {
let secSinceLast = (now - lastReport) / 1000; let secSinceLast = (now - lastReport) / 1000;
lastReport = now; lastReport = now;
var port = config.get('server.port'); let port = config.get('server.port');
let requests = _.get(event, ['requests', port, 'total'], 0); let requests = _.get(event, ['requests', port, 'total'], 0);
let requestsPerSecond = requests / secSinceLast; let requestsPerSecond = requests / secSinceLast;

View file

@ -7,8 +7,8 @@ function Samples(max) {
} }
Samples.prototype.add = function (sample) { Samples.prototype.add = function (sample) {
var vals = this.vals; let vals = this.vals;
var length = this.length = Math.min(this.length + 1, this.max); let length = this.length = Math.min(this.length + 1, this.max);
_.forOwn(sample, function (val, name) { _.forOwn(sample, function (val, name) {
if (val == null) val = null; if (val == null) val = null;

View file

@ -31,15 +31,15 @@ module.exports = class ServerStatus {
} }
overall() { overall() {
var state = _(this._created) let state = _(this._created)
.map(function (status) { .map(function (status) {
return states.get(status.state); return states.get(status.state);
}) })
.sortBy('severity') .sortBy('severity')
.pop(); .pop();
var statuses = _.where(this._created, { state: state.id }); let statuses = _.where(this._created, { state: state.id });
var since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); let since = _.get(_.sortBy(statuses, 'since'), [0, 'since']);
return { return {
state: state.id, state: state.id,
@ -59,7 +59,7 @@ module.exports = class ServerStatus {
} }
toString() { toString() {
var overall = this.overall(); let overall = this.overall();
return `${overall.title} ${overall.nickname}`; return `${overall.title} ${overall.nickname}`;
} }

View file

@ -13,7 +13,7 @@ class Status extends EventEmitter {
this.on('change', function (previous, previousMsg) { this.on('change', function (previous, previousMsg) {
this.since = new Date(); this.since = new Date();
var tags = ['status', name]; let tags = ['status', name];
tags.push(this.state === 'red' ? 'error' : 'info'); tags.push(this.state === 'red' ? 'error' : 'info');
server.log(tags, { server.log(tags, {

View file

@ -8,7 +8,7 @@ Bluebird.longStackTraces();
* replace the Promise service with Bluebird so that tests * replace the Promise service with Bluebird so that tests
* can use promises without having to call $rootScope.apply() * can use promises without having to call $rootScope.apply()
* *
* var noDigestPromises = require('test_utils/no_digest_promises'); * let noDigestPromises = require('test_utils/no_digest_promises');
* *
* describe('some module that does complex shit with promises', function () { * describe('some module that does complex shit with promises', function () {
* beforeEach(noDigestPromises.activate); * beforeEach(noDigestPromises.activate);
@ -16,7 +16,7 @@ Bluebird.longStackTraces();
* }); * });
*/ */
var active = false; let active = false;
uiModules uiModules
.get('kibana') .get('kibana')

View file

@ -2,8 +2,8 @@ import $ from 'jquery';
import _ from 'lodash'; import _ from 'lodash';
import Promise from 'bluebird'; import Promise from 'bluebird';
import keyMap from 'ui/utils/key_map'; import keyMap from 'ui/utils/key_map';
var reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); let reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1));
var KeyboardEvent = window.KeyboardEvent; let KeyboardEvent = window.KeyboardEvent;
/** /**
* Simulate keyboard events in an element. This allows testing the way that * Simulate keyboard events in an element. This allows testing the way that
@ -35,7 +35,7 @@ var KeyboardEvent = window.KeyboardEvent;
* @async * @async
*/ */
export default function ($el, sequence) { export default function ($el, sequence) {
var modifierState = { let modifierState = {
ctrlKey: false, ctrlKey: false,
shiftKey: false, shiftKey: false,
altKey: false, altKey: false,
@ -45,7 +45,7 @@ export default function ($el, sequence) {
return doList(_.clone(sequence)); return doList(_.clone(sequence));
function setModifier(key, state) { function setModifier(key, state) {
var name = key + 'Key'; let name = key + 'Key';
if (modifierState.hasOwnProperty(name)) { if (modifierState.hasOwnProperty(name)) {
modifierState[name] = !!state; modifierState[name] = !!state;
} }
@ -55,7 +55,7 @@ export default function ($el, sequence) {
return Promise.try(function () { return Promise.try(function () {
if (!list || !list.length) return; if (!list || !list.length) return;
var event = list[0]; let event = list[0];
if (_.isString(event)) { if (_.isString(event)) {
event = { type: 'press', key: event }; event = { type: 'press', key: event };
} }
@ -91,14 +91,14 @@ export default function ($el, sequence) {
} }
function fire(type, key, repeat) { function fire(type, key, repeat) {
var keyCode = reverseKeyMap[key]; let keyCode = reverseKeyMap[key];
if (!keyCode) throw new TypeError('invalid key "' + key + '"'); if (!keyCode) throw new TypeError('invalid key "' + key + '"');
if (type === 'keydown') setModifier(key, true); if (type === 'keydown') setModifier(key, true);
if (type === 'keyup') setModifier(key, false); if (type === 'keyup') setModifier(key, false);
var $target = _.isFunction($el) ? $el() : $el; let $target = _.isFunction($el) ? $el() : $el;
var $event = new $.Event(type, _.defaults({ keyCode: keyCode }, modifierState)); let $event = new $.Event(type, _.defaults({ keyCode: keyCode }, modifierState));
$target.trigger($event); $target.trigger($event);
} }
}; };

View file

@ -9,10 +9,10 @@ import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit'; import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit';
import IndexPatternsFieldProvider from 'ui/index_patterns/_field'; import IndexPatternsFieldProvider from 'ui/index_patterns/_field';
export default function (Private) { export default function (Private) {
var fieldFormats = Private(RegistryFieldFormatsProvider); let fieldFormats = Private(RegistryFieldFormatsProvider);
var flattenHit = Private(IndexPatternsFlattenHitProvider); let flattenHit = Private(IndexPatternsFlattenHitProvider);
var Field = Private(IndexPatternsFieldProvider); let Field = Private(IndexPatternsFieldProvider);
function StubIndexPattern(pattern, timeField, fields) { function StubIndexPattern(pattern, timeField, fields) {
this.id = pattern; this.id = pattern;

View file

@ -1,6 +1,5 @@
import 'angular'; import 'angular';
import 'ui/chrome'; import 'ui/chrome';
import 'ui/chrome/context';
import 'ui/bind'; import 'ui/bind';
import 'ui/bound_to_config_obj'; import 'ui/bound_to_config_obj';
import 'ui/config'; import 'ui/config';

View file

@ -1,9 +1,9 @@
const store = Symbol('store'); const store = Symbol('store');
export default class TabFakeStore { export default class StubBrowserStorage {
constructor() { this[store] = new Map(); } constructor() { this[store] = new Map(); }
getItem(k) { return this[store].get(k); } getItem(k) { return this[store].get(k); }
setItem(k, v) { return this[store].set(k, v); } setItem(k, v) { return this[store].set(k, String(v)); }
removeItem(k) { return this[store].delete(k); } removeItem(k) { return this[store].delete(k); }
getKeys() { return [ ...this[store].keys() ]; } getKeys() { return [ ...this[store].keys() ]; }
getValues() { return [ ...this[store].values() ]; } getValues() { return [ ...this[store].values() ]; }

View file

@ -0,0 +1,36 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import uiModules from 'ui/modules';
import $ from 'jquery';
import '../directives/kbn_loading_indicator';
describe('kbnLoadingIndicator', function () {
let compile;
beforeEach(() => {
ngMock.module('kibana');
ngMock.inject(function ($compile, $rootScope) {
compile = function (hasActiveConnections) {
$rootScope.chrome = {
httpActive: (hasActiveConnections ? [1] : [])
};
const $el = $('<kbn-loading-indicator></kbn-loading-indicator>');
$rootScope.$apply();
$compile($el)($rootScope);
return $el;
};
});
});
it('injects a loading .spinner into the element', function () {
const $el = compile();
expect($el.find('.spinner')).to.have.length(1);
});
it('applies removes ng-hide class when there are connections', function () {
const $el = compile(true);
expect($el.find('.spinner.ng-hide')).to.have.length(0);
});
});

View file

@ -1,7 +1,8 @@
import sinon from 'auto-release-sinon'; import sinon from 'auto-release-sinon';
import Tab from '../tab'; import Tab from '../tab';
import expect from 'expect.js'; import expect from 'expect.js';
import TabFakeStore from './_tab_fake_store'; import StubBrowserStorage from './fixtures/stub_browser_storage';
describe('Chrome Tab', function () { describe('Chrome Tab', function () {
describe('construction', function () { describe('construction', function () {
@ -88,7 +89,7 @@ describe('Chrome Tab', function () {
}); });
it('discovers the lastUrl', function () { it('discovers the lastUrl', function () {
const lastUrlStore = new TabFakeStore(); const lastUrlStore = new StubBrowserStorage();
const tab = new Tab({ id: 'foo', lastUrlStore }); const tab = new Tab({ id: 'foo', lastUrlStore });
expect(tab.lastUrl).to.not.equal('/foo/bar'); expect(tab.lastUrl).to.not.equal('/foo/bar');
@ -100,7 +101,7 @@ describe('Chrome Tab', function () {
}); });
it('logs a warning about last urls that do not match the rootUrl', function () { it('logs a warning about last urls that do not match the rootUrl', function () {
const lastUrlStore = new TabFakeStore(); const lastUrlStore = new StubBrowserStorage();
const tab = new Tab({ id: 'foo', baseUrl: '/bar', lastUrlStore }); const tab = new Tab({ id: 'foo', baseUrl: '/bar', lastUrlStore });
tab.setLastUrl('/bar/foo/1'); tab.setLastUrl('/bar/foo/1');
@ -114,7 +115,7 @@ describe('Chrome Tab', function () {
describe('#setLastUrl()', function () { describe('#setLastUrl()', function () {
it('updates the lastUrl and storage value if passed a lastUrlStore', function () { it('updates the lastUrl and storage value if passed a lastUrlStore', function () {
const lastUrlStore = new TabFakeStore(); const lastUrlStore = new StubBrowserStorage();
const tab = new Tab({ id: 'foo', lastUrlStore }); const tab = new Tab({ id: 'foo', lastUrlStore });
expect(tab.lastUrl).to.not.equal('foo'); expect(tab.lastUrl).to.not.equal('foo');

View file

@ -1,6 +1,6 @@
import expect from 'expect.js'; import expect from 'expect.js';
import TabFakeStore from './_tab_fake_store'; import StubBrowserStorage from './fixtures/stub_browser_storage';
import TabCollection from '../tab_collection'; import TabCollection from '../tab_collection';
import Tab from '../tab'; import Tab from '../tab';
import { indexBy, random } from 'lodash'; import { indexBy, random } from 'lodash';
@ -54,7 +54,7 @@ describe('Chrome TabCollection', function () {
describe('#consumeRouteUpdate()', function () { describe('#consumeRouteUpdate()', function () {
it('updates the active tab', function () { it('updates the active tab', function () {
const store = new TabFakeStore(); const store = new StubBrowserStorage();
const baseUrl = `http://localhost:${random(1000, 9999)}`; const baseUrl = `http://localhost:${random(1000, 9999)}`;
const tabs = new TabCollection({ store, defaults: { baseUrl } }); const tabs = new TabCollection({ store, defaults: { baseUrl } });
tabs.set([ tabs.set([

View file

@ -1,7 +1,6 @@
import expect from 'expect.js'; import expect from 'expect.js';
import kbnAngular from '../angular'; import kbnAngular from '../angular';
import TabFakeStore from '../../__tests__/_tab_fake_store';
import { noop } from 'lodash'; import { noop } from 'lodash';
describe('Chrome API :: Angular', () => { describe('Chrome API :: Angular', () => {

View file

@ -1,7 +1,7 @@
import expect from 'expect.js'; import expect from 'expect.js';
import setup from '../apps'; import setup from '../apps';
import TabFakeStore from '../../__tests__/_tab_fake_store'; import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage';
describe('Chrome API :: apps', function () { describe('Chrome API :: apps', function () {
describe('#get/setShowAppsLink()', function () { describe('#get/setShowAppsLink()', function () {
@ -147,7 +147,7 @@ describe('Chrome API :: apps', function () {
describe('#get/setLastUrlFor()', function () { describe('#get/setLastUrlFor()', function () {
it('reads/writes last url from storage', function () { it('reads/writes last url from storage', function () {
const chrome = {}; const chrome = {};
const store = new TabFakeStore(); const store = new StubBrowserStorage();
setup(chrome, { appUrlStore: store }); setup(chrome, { appUrlStore: store });
expect(chrome.getLastUrlFor('app')).to.equal(undefined); expect(chrome.getLastUrlFor('app')).to.equal(undefined);
chrome.setLastUrlFor('app', 'url'); chrome.setLastUrlFor('app', 'url');

View file

@ -1,45 +1,73 @@
import expect from 'expect.js'; import expect from 'expect.js';
import initChromeNavApi from 'ui/chrome/api/nav'; import initChromeNavApi from 'ui/chrome/api/nav';
import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage';
const basePath = '/someBasePath'; const basePath = '/someBasePath';
function getChrome(customInternals = { basePath }) { function init(customInternals = { basePath }) {
const chrome = {}; const chrome = {};
initChromeNavApi(chrome, { const internals = {
nav: [], nav: [],
...customInternals, ...customInternals,
}); };
return chrome; initChromeNavApi(chrome, internals);
return { chrome, internals };
} }
describe('chrome nav apis', function () { describe('chrome nav apis', function () {
describe('#getBasePath()', function () { describe('#getBasePath()', function () {
it('returns the basePath', function () { it('returns the basePath', function () {
const chrome = getChrome(); const { chrome } = init();
expect(chrome.getBasePath()).to.be(basePath); expect(chrome.getBasePath()).to.be(basePath);
}); });
}); });
describe('#addBasePath()', function () { describe('#addBasePath()', function () {
it('returns undefined when nothing is passed', function () { it('returns undefined when nothing is passed', function () {
const chrome = getChrome(); const { chrome } = init();
expect(chrome.addBasePath()).to.be(undefined); expect(chrome.addBasePath()).to.be(undefined);
}); });
it('prepends the base path when the input is a path', function () { it('prepends the base path when the input is a path', function () {
const chrome = getChrome(); const { chrome } = init();
expect(chrome.addBasePath('/other/path')).to.be(`${basePath}/other/path`); expect(chrome.addBasePath('/other/path')).to.be(`${basePath}/other/path`);
}); });
it('ignores non-path urls', function () { it('ignores non-path urls', function () {
const chrome = getChrome(); const { chrome } = init();
expect(chrome.addBasePath('http://github.com/elastic/kibana')).to.be('http://github.com/elastic/kibana'); expect(chrome.addBasePath('http://github.com/elastic/kibana')).to.be('http://github.com/elastic/kibana');
}); });
it('includes the query string', function () { it('includes the query string', function () {
const chrome = getChrome(); const { chrome } = init();
expect(chrome.addBasePath('/app/kibana?a=b')).to.be(`${basePath}/app/kibana?a=b`); expect(chrome.addBasePath('/app/kibana?a=b')).to.be(`${basePath}/app/kibana?a=b`);
}); });
}); });
describe('internals.trackPossibleSubUrl()', function () {
it('injects the globalState of the current url to all links for the same app', function () {
const appUrlStore = new StubBrowserStorage();
const nav = [
{ url: 'https://localhost:9200/app/kibana#discover' },
{ url: 'https://localhost:9200/app/kibana#visualize' },
{ url: 'https://localhost:9200/app/kibana#dashboard' },
].map(l => {
l.lastSubUrl = l.url;
return l;
});
const { chrome, internals } = init({ appUrlStore, nav });
internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate');
expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate');
expect(internals.nav[0].active).to.be(false);
expect(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate');
expect(internals.nav[1].active).to.be(false);
expect(internals.nav[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate');
expect(internals.nav[2].active).to.be(true);
});
});
}); });

View file

@ -40,25 +40,72 @@ export default function (chrome, internals) {
} }
function refreshLastUrl(link) { function refreshLastUrl(link) {
link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)); link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)) || link.lastSubUrl || link.url;
}
function getAppId(url) {
const pathname = parse(url).pathname;
const pathnameWithoutBasepath = pathname.slice(chrome.getBasePath().length);
const match = pathnameWithoutBasepath.match(/^\/app\/([^\/]+)(?:\/|\?|#|$)/);
if (match) return match[1];
}
function decodeKibanaUrl(url) {
const parsedUrl = parse(url, true);
const appId = getAppId(parsedUrl);
const hash = parsedUrl.hash || '';
const parsedHash = parse(hash.slice(1), true);
const globalState = parsedHash.query && parsedHash.query._g;
return { appId, globalState, parsedUrl, parsedHash };
}
function injectNewGlobalState(link, fromAppId, newGlobalState) {
// parse the lastSubUrl of this link so we can manipulate its parts
const { appId: toAppId, parsedHash: toHash, parsedUrl: toParsed } = decodeKibanaUrl(link.lastSubUrl);
// don't copy global state if links are for different apps
if (fromAppId !== toAppId) return;
// add the new globalState to the hashUrl in the linkurl
const toHashQuery = toHash.query || {};
toHashQuery._g = newGlobalState;
// format the new subUrl and include the newHash
link.lastSubUrl = format({
protocol: toParsed.protocol,
port: toParsed.port,
hostname: toParsed.hostname,
pathname: toParsed.pathname,
query: toParsed.query,
hash: format({
pathname: toHash.pathname,
query: toHashQuery,
hash: toHash.hash,
}),
});
} }
internals.trackPossibleSubUrl = function (url) { internals.trackPossibleSubUrl = function (url) {
for (const link of internals.nav) { const { appId, globalState: newGlobalState } = decodeKibanaUrl(url);
link.active = startsWith(url, link.url);
for (const link of internals.nav) {
const matchingTab = find(internals.tabs, { rootUrl: link.url });
link.active = startsWith(url, link.url);
if (link.active) { if (link.active) {
setLastUrl(link, url); setLastUrl(link, url);
continue; continue;
} }
const matchingTab = find(internals.tabs, { rootUrl: link.url });
if (matchingTab) { if (matchingTab) {
setLastUrl(link, matchingTab.getLastUrl()); setLastUrl(link, matchingTab.getLastUrl());
continue; } else {
refreshLastUrl(link);
} }
refreshLastUrl(link); if (newGlobalState) {
injectNewGlobalState(link, appId, newGlobalState);
}
} }
}; };

View file

@ -1,6 +1,6 @@
<div class="content" chrome-context > <div class="content" chrome-context >
<!-- TODO: These config dropdowns shouldn't be hard coded --> <!-- TODO: These config dropdowns shouldn't be hard coded -->
<nav class="app-links-wrapper"> <nav class="app-links-wrapper" ng-show="chrome.getVisible()">
<li <li
ng-if="!chrome.getBrand('logo') && !chrome.getBrand('smallLogo')" ng-if="!chrome.getBrand('logo') && !chrome.getBrand('smallLogo')"
aria-label="{{ chrome.getAppTitle() }} Logo" aria-label="{{ chrome.getAppTitle() }} Logo"
@ -21,28 +21,12 @@
<app-switcher> <app-switcher>
</app-switcher> </app-switcher>
<div class="bottom-apps hide app-links"> <div class="bottom-apps">
<div class="app-link"> <div class="chrome-actions app-links" kbn-chrome-append-nav-controls></div>
<a href="http://elastic.co">
<div class="app-icon">
<i class="fa fa-gear"></i>
</div>
<div class="app-title">settings</div>
</a>
</div>
<div class="app-link">
<a href="http://elastic.co">
<div class="app-icon">
<i class="fa fa-user"></i>
</div>
<div class="app-title">Jon Doe</div>
<div class="app-title">Logout</div>
</a>
</div>
</div> </div>
</nav> </nav>
<div class="app-wrapper"> <div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
<div class="app-wrapper-panel"> <div class="app-wrapper-panel">
<kbn-notifications list="notifList"></kbn-notifications> <kbn-notifications list="notifList"></kbn-notifications>
<nav <nav
@ -80,6 +64,7 @@
</div> </div>
<!-- /Full navbar --> <!-- /Full navbar -->
</nav> </nav>
<kbn-loading-indicator></kbn-loading-indicator>
<div class="application" ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()" ng-view></div> <div class="application" ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()" ng-view></div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
<kbn-timepicker <kbn-timepicker
from="opts.timefilter.time.from" from="timefilter.time.from"
to="opts.timefilter.time.to" to="timefilter.time.to"
mode="opts.timefilter.time.mode" mode="timefilter.time.mode"
active-tab="'filter'" active-tab="'filter'"
interval="opts.timefilter.refreshInterval"> interval="timefilter.refreshInterval">
</kbn-timepicker> </kbn-timepicker>

View file

@ -1,7 +1,7 @@
<kbn-timepicker <kbn-timepicker
from="opts.timefilter.time.from" from="timefilter.time.from"
to="opts.timefilter.time.to" to="timefilter.time.to"
mode="opts.timefilter.time.mode" mode="timefilter.time.mode"
active-tab="'interval'" active-tab="'interval'"
interval="opts.timefilter.refreshInterval"> interval="timefilter.refreshInterval">
</kbn-timepicker> </kbn-timepicker>

View file

@ -1,30 +0,0 @@
import _ from 'lodash';
import ConfigTemplate from 'ui/config_template';
import uiModules from 'ui/modules';
uiModules
.get('kibana')
// TODO: all of this really belongs in the timepicker
.directive('chromeContext', function (timefilter, globalState) {
var listenForUpdates = _.once(function ($scope) {
$scope.$listen(timefilter, 'update', function (newVal, oldVal) {
globalState.time = _.clone(timefilter.time);
globalState.refreshInterval = _.clone(timefilter.refreshInterval);
globalState.save();
});
});
return {
link: function ($scope) {
listenForUpdates($scope);
// chrome is responsible for timepicker ui and state transfer...
$scope.timefilter = timefilter;
$scope.toggleRefresh = function () {
timefilter.refreshInterval.pause = !timefilter.refreshInterval.pause;
};
}
};
});

View file

@ -1 +0,0 @@
<div class="spinner" ng-show="chrome.httpActive.length"></div>

View file

@ -54,6 +54,7 @@ body { overflow-x: hidden; }
margin: 0 auto; margin: 0 auto;
background-color: #fff; background-color: #fff;
&.hidden-chrome { left: 0; }
&-panel { &-panel {
.flex-parent(@shrink: 0); .flex-parent(@shrink: 0);
box-shadow: -4px 0px 3px rgba(0,0,0,0.2); box-shadow: -4px 0px 3px rgba(0,0,0,0.2);
@ -84,7 +85,6 @@ body { overflow-x: hidden; }
.app-icon { .app-icon {
float: left; float: left;
filter: invert(100%);
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
font-size: 1.7em; font-size: 1.7em;
@ -95,6 +95,11 @@ body { overflow-x: hidden; }
> img { > img {
height: 18px; height: 18px;
margin-top: 8px; margin-top: 8px;
filter: invert(100%);
}
> i {
color: #fff;
line-height: @app-icon-height
} }
} }
@ -128,7 +133,7 @@ body { overflow-x: hidden; }
text-decoration: none; text-decoration: none;
} }
img { img {
filter: invert(100%); filter: none;
} }
} }

View file

@ -2,12 +2,6 @@ import $ from 'jquery';
import chromeNavControlsRegistry from 'ui/registry/chrome_nav_controls'; import chromeNavControlsRegistry from 'ui/registry/chrome_nav_controls';
import UiModules from 'ui/modules'; import UiModules from 'ui/modules';
import spinnerHtml from './active_http_spinner.html';
const spinner = {
name: 'active http requests',
template: spinnerHtml
};
export default function (chrome, internals) { export default function (chrome, internals) {
@ -19,7 +13,7 @@ export default function (chrome, internals) {
const parts = [$element.html()]; const parts = [$element.html()];
const controls = Private(chromeNavControlsRegistry); const controls = Private(chromeNavControlsRegistry);
for (const control of [spinner, ...controls.inOrder]) { for (const control of controls.inOrder) {
parts.unshift( parts.unshift(
`<!-- nav control ${control.name} -->`, `<!-- nav control ${control.name} -->`,
control.template control.template

View file

@ -1,8 +1,9 @@
import 'ui/directives/config'; import 'ui/directives/kbn_top_nav';
import './app_switcher'; import './app_switcher';
import kbnChromeProv from './kbn_chrome'; import kbnChromeProv from './kbn_chrome';
import kbnChromeNavControlsProv from './append_nav_controls'; import kbnChromeNavControlsProv from './append_nav_controls';
import './kbn_loading_indicator';
export default function (chrome, internals) { export default function (chrome, internals) {
kbnChromeProv(chrome, internals); kbnChromeProv(chrome, internals);

View file

@ -1,7 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import UiModules from 'ui/modules'; import UiModules from 'ui/modules';
import ConfigTemplate from 'ui/config_template';
export default function (chrome, internals) { export default function (chrome, internals) {
@ -46,10 +45,6 @@ export default function (chrome, internals) {
// and some local values // and some local values
chrome.httpActive = $http.pendingRequests; chrome.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs; $scope.notifList = require('ui/notify')._notifs;
$scope.appSwitcherTemplate = new ConfigTemplate({
switcher: '<app-switcher></app-switcher>'
});
return chrome; return chrome;
} }
}; };

View file

@ -0,0 +1,13 @@
import UiModules from 'ui/modules';
import angular from 'angular';
const spinnerTemplate = '<div class="spinner" ng-show="chrome.httpActive.length"></div>';
UiModules
.get('ui/kibana')
.directive('kbnLoadingIndicator', function ($compile) {
return {
restrict: 'E',
template: spinnerTemplate,
};
});

View file

@ -1,34 +0,0 @@
import _ from 'lodash';
function ConfigTemplate(templates) {
var template = this;
template.current = null;
template.toggle = _.partial(update, null);
template.open = _.partial(update, true);
template.close = _.partial(update, false);
function update(newState, name) {
var toUpdate = templates[name];
var curState = template.is(name);
if (newState == null) newState = !curState;
if (newState) {
template.current = toUpdate;
} else {
template.current = null;
}
return newState;
}
template.is = function (name) {
return template.current === templates[name];
};
template.toString = function () {
return template.current;
};
}
export default ConfigTemplate;

View file

@ -3,6 +3,7 @@ import ngMock from 'ng_mock';
import expect from 'expect.js'; import expect from 'expect.js';
import NormalizeSortRequestProvider from 'ui/courier/data_source/_normalize_sort_request'; import NormalizeSortRequestProvider from 'ui/courier/data_source/_normalize_sort_request';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import _ from 'lodash';
describe('SearchSource#normalizeSortRequest', function () { describe('SearchSource#normalizeSortRequest', function () {
let normalizeSortRequest; let normalizeSortRequest;
@ -87,4 +88,17 @@ describe('SearchSource#normalizeSortRequest', function () {
expect(result).to.eql([normalizedSort]); expect(result).to.eql([normalizedSort]);
}); });
it('should remove unmapped_type parameter from _score sorting', function () {
var sortable = { _score: 'desc'};
var expected = [{
_score: {
order: 'desc'
}
}];
var result = normalizeSortRequest(sortable, indexPattern);
expect(_.isEqual(result, expected)).to.be.ok();
});
}); });

View file

@ -43,6 +43,10 @@ export default function normalizeSortRequest(config) {
sortValue = { order: sortValue }; sortValue = { order: sortValue };
} }
sortValue = _.defaults({}, sortValue, defaultSortOptions); sortValue = _.defaults({}, sortValue, defaultSortOptions);
if (sortField === '_score') {
delete sortValue.unmapped_type;
}
} }
normalized[sortField] = sortValue; normalized[sortField] = sortValue;

View file

@ -1,59 +0,0 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import { assign } from 'lodash';
import $ from 'jquery';
describe('Config Directive', function () {
var build = function () {};
beforeEach(ngMock.module('kibana', function ($compileProvider) {
var renderCount = 0;
$compileProvider.directive('renderCounter', function () {
return {
link: function ($scope, $el) {
$el.html(++renderCount);
}
};
});
}));
beforeEach(ngMock.inject(function ($compile, $rootScope) {
build = function (attrs, scopeVars) {
var $el = $('<config>').attr(attrs);
var $scope = $rootScope.$new();
assign($scope, scopeVars || {});
$compile($el)($scope);
$scope.$digest();
return $el;
};
}));
it('renders it\'s config template', function () {
var $config = build({ 'config-template': '"<uniqel></uniqel>"' });
expect($config.find('uniqel').size()).to.be(1);
});
it('exposes an object a config object using it\'s name', function () {
var $config = build(
{
'config-template': '"<uniqel>{{ controller.name }}</uniqel>"',
'config-object': 'controller',
},
{
controller: {
name: 'foobar'
}
}
);
expect($config.find('uniqel').text()).to.be('foobar');
});
it('only renders the config-template once', function () {
var $config = build({ 'config-template': '"<div render-counter></div>"' });
expect($config.find('[render-counter]').text()).to.be('1');
});
});

View file

@ -0,0 +1,55 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import { assign } from 'lodash';
import $ from 'jquery';
import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
import Registry from 'ui/registry/_registry';
import 'ui/navbar_extensions';
describe('kbnTopNav Directive', function () {
var build = function () {};
let $testScope = null;
let stubRegistry;
beforeEach(ngMock.module('kibana', function ($compileProvider, PrivateProvider) {
var renderCount = 0;
$compileProvider.directive('renderCounter', function () {
return {
link: function ($scope, $el) {
$el.html(++renderCount);
}
};
});
stubRegistry = new Registry({
index: ['name'],
group: ['appName'],
order: ['order']
});
PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
ngMock.module('kibana/navbar');
}));
beforeEach(ngMock.inject(function ($compile, $rootScope) {
build = function (scopeVars) {
var $el = $('<kbn-top-nav name="foo">');
$testScope = $rootScope.$new();
assign($testScope, scopeVars || {});
$compile($el)($testScope);
$testScope.$digest();
return $el;
};
}));
it('sets the proper functions on the kbnTopNav prop on scope', function () {
var $config = build();
expect($testScope.kbnTopNav.open).to.be.a(Function);
expect($testScope.kbnTopNav.close).to.be.a(Function);
expect($testScope.kbnTopNav.is).to.be.a(Function);
expect($testScope.kbnTopNav.toggle).to.be.a(Function);
});
});

View file

@ -1,82 +0,0 @@
import _ from 'lodash';
import 'ui/watch_multi';
import ConfigTemplate from 'ui/config_template';
import angular from 'angular';
import 'ui/directives/input_focus';
import uiModules from 'ui/modules';
var module = uiModules.get('kibana');
/**
* config directive
*
* Creates a full width horizonal config section, usually under a nav/subnav.
* ```
* <config config-template="configTemplate" config-object="configurable"></config>
* ```
*/
module.directive('config', function ($compile) {
return {
restrict: 'E',
scope: {
configTemplate: '=',
configClose: '=',
configSubmit: '=',
configObject: '='
},
link: function ($scope, element, attr) {
var tmpScope = $scope.$new();
$scope.$watch('configObject', function (newVal) {
$scope[attr.configObject] = $scope.configObject;
});
var wrapTmpl = function (tmpl) {
if ($scope.configSubmit) {
return '<form role="form" class="container-fluid" ng-submit="configSubmit()">' + tmpl + '</form>';
} else {
return '<div class="container-fluid">' + tmpl + '</div>';
}
};
$scope.$watchMulti([
'configSubmit',
'configTemplate.current || configTemplate'
], function () {
var tmpl = $scope.configTemplate;
if (tmpl instanceof ConfigTemplate) {
tmpl = tmpl.toString();
}
tmpScope.$destroy();
tmpScope = $scope.$new();
var html = '';
if (tmpl) {
html = $compile('' +
'<div class="config" ng-show="configTemplate">' +
wrapTmpl(tmpl) +
' <div class="config-close remove">' +
' <i class="fa fa-chevron-circle-up" ng-click="close()"></i>' +
' </div>' +
'</div>' +
''
)(tmpScope);
}
element.html(html);
});
$scope.close = function () {
if (_.isFunction($scope.configClose)) $scope.configClose();
if ($scope.configTemplate instanceof ConfigTemplate) {
$scope.configTemplate.current = null;
} else {
$scope.configTemplate = null;
}
};
}
};
});

View file

@ -0,0 +1,116 @@
import _ from 'lodash';
import 'ui/watch_multi';
import angular from 'angular';
import 'ui/directives/input_focus';
import uiModules from 'ui/modules';
var module = uiModules.get('kibana');
/**
* kbnTopNav directive
*
* The top section that shows the timepicker, load, share and save dialogues.
* ```
* <kbn-top-nav name="current-app-for-extensions" config="path.to.menuItems"></kbn-top-nav>
* ```
*/
module.directive('kbnTopNav', function (Private) {
const filterTemplate = require('ui/chrome/config/filter.html');
const intervalTemplate = require('ui/chrome/config/interval.html');
function optionsNormalizer(defaultFunction, opt) {
if (!opt.key) {
return false;
}
return _.assign({
label: _.capitalize(opt.key),
hasFunction: !!opt.run,
description: ('Toggle ' + opt.key),
run: defaultFunction
}, opt);
}
function getTemplatesMap(configs) {
const templateMap = {};
configs.forEach(conf => {
if (conf.template) {
templateMap[conf.key] = conf.template;
}
});
return templateMap;
}
return {
restrict: 'E',
transclude: true,
template: function ($el, $attrs) {
// This is ugly
// This is necessary because of navbar-extensions
// It will no accept any programatic way of setting its name
// besides this because it happens so early in the digest cycle
return `
<navbar class="kibana-nav-options">
<div ng-transclude></div>
<div class="button-group kibana-nav-actions" role="toolbar">
<button
ng-repeat="menuItem in kbnTopNav.menuItems"
aria-label="{{::menuItem.description}}"
aria-haspopup="{{!menuItem.hasFunction}}"
aria-expanded="{{kbnTopNav.is(menuItem.key)}}"
ng-class="{active: kbnTopNav.is(menuItem.key)}"
ng-click="menuItem.run(menuItem)"
ng-bind="menuItem.label">
</button>
<navbar-extensions name="${$attrs.name}"></navbar-extensions>
</div>
<kbn-global-timepicker></kbn-global-timepicker>
</navbar>
<div class="config" ng-show="kbnTopNav.currTemplate">
<div id="template_wrapper" class="container-fluid"></div>
<div class="config-close remove">
<i class="fa fa-chevron-circle-up" ng-click="kbnTopNav.close()"></i>
</div>
</div>`;
},
controller: ['$scope', '$compile', '$attrs', function ($scope, $compile, $attrs) {
const ctrlObj = this;
// toggleCurrTemplate(false) to turn it off
ctrlObj.toggleCurrTemplate = function (which) {
if (ctrlObj.curr === which || !which) {
ctrlObj.curr = null;
} else {
ctrlObj.curr = which;
}
const templateToCompile = ctrlObj.templates[ctrlObj.curr] || false;
$scope.kbnTopNav.currTemplate = templateToCompile ? $compile(templateToCompile)($scope) : false;
};
const normalizeOpts = _.partial(optionsNormalizer, (item) => {
ctrlObj.toggleCurrTemplate(item.key);
});
const niceMenuItems = _.compact(($scope[$attrs.config] || []).map(normalizeOpts));
ctrlObj.templates = _.assign({
interval: intervalTemplate,
filter: filterTemplate,
}, getTemplatesMap(niceMenuItems));
$scope.kbnTopNav = {
menuItems: niceMenuItems,
currTemplate: false,
is: which => { return ctrlObj.curr === which; },
close: () => { ctrlObj.toggleCurrTemplate(false); },
toggle: ctrlObj.toggleCurrTemplate,
open: which => {
if (ctrlObj.curr !== which) {
ctrlObj.toggleCurrTemplate(which);
}
}
};
}],
link: function ($scope, element, attr, configCtrl) {
$scope.$watch('kbnTopNav.currTemplate', newVal => {
element.find('#template_wrapper').html(newVal);
});
}
};
});

View file

@ -1,140 +0,0 @@
import ngMock from 'ng_mock';
import sinon from 'sinon';
import expect from 'expect.js';
import angular from 'angular';
import _ from 'lodash';
import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
import Registry from 'ui/registry/_registry';
import 'ui/navbar';
const defaultMarkup = `
<navbar name="testing">
<div class="button-group" role="toolbar">
<button>
<i aria-hidden="true" class="fa fa-file-new-o"></i>
</button>
<button>
<i aria-hidden="true" class="fa fa-save"></i>
</button>
<button>
<i aria-hidden="true" class="fa fa-folder-open-o"></i>
</button>
</div>
</navbar>`;
describe('navbar directive', function () {
let $rootScope;
let $compile;
let stubRegistry;
beforeEach(function () {
ngMock.module('kibana', function (PrivateProvider) {
stubRegistry = new Registry({
index: ['name'],
group: ['appName'],
order: ['order']
});
PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
});
ngMock.module('kibana/navbar');
// Create the scope
ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
});
});
function init(markup = defaultMarkup) {
// Give us a scope
const $el = angular.element(markup);
$compile($el)($rootScope);
$el.scope().$digest();
return $el;
}
describe('incorrect use', function () {
it('should throw if missing a name property', function () {
const markup = `<navbar><div class="button-group" role="toolbar"></div></navbar>`;
expect(() => init(markup)).to.throwException(/requires a name attribute/);
});
it('should throw if missing a button group', function () {
const markup = `<navbar name="testing"></navbar>`;
expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
});
it('should throw if multiple button groups', function () {
const markup = ` <navbar name="testing">
<div class="button-group" role="toolbar">
<button>
<i aria-hidden="true" class="fa fa-file-new-o"></i>
</button>
<button>
<i aria-hidden="true" class="fa fa-save"></i>
</button>
</div>
<div class="button-group" role="toolbar">
<button>
<i aria-hidden="true" class="fa fa-folder-open-o"></i>
</button>
</div>
</navbar>`;
expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
});
it('should throw if button group not direct child', function () {
const markup = `<navbar><div><div class="button-group" role="toolbar"></div></div></navbar>`;
expect(() => init(markup)).to.throwException(/must have exactly 1 button group/);
});
});
describe('injecting extensions', function () {
function registerExtension(def = {}) {
stubRegistry.register(function () {
return _.defaults(def, {
name: 'exampleButton',
appName: 'testing',
order: 0,
template: `
<button class="test-button">
<i aria-hidden="true" class="fa fa-rocket"></i>
</button>`
});
});
}
it('should use the default markup', function () {
var $el = init();
expect($el.find('.button-group button').length).to.equal(3);
});
it('should append to end then order == 0', function () {
registerExtension({ order: 0 });
var $el = init();
expect($el.find('.button-group button').length).to.equal(4);
expect($el.find('.button-group button').last().hasClass('test-button')).to.be.ok();
});
it('should append to end then order > 0', function () {
registerExtension({ order: 1 });
var $el = init();
expect($el.find('.button-group button').length).to.equal(4);
expect($el.find('.button-group button').last().hasClass('test-button')).to.be.ok();
});
it('should append to end then order < 0', function () {
registerExtension({ order: -1 });
var $el = init();
expect($el.find('.button-group button').length).to.equal(4);
expect($el.find('.button-group button').first().hasClass('test-button')).to.be.ok();
});
});
});

View file

@ -0,0 +1,106 @@
import ngMock from 'ng_mock';
import sinon from 'sinon';
import expect from 'expect.js';
import angular from 'angular';
import _ from 'lodash';
import navbarExtensionsRegistry from 'ui/registry/navbar_extensions';
import Registry from 'ui/registry/_registry';
import 'ui/navbar_extensions';
const defaultMarkup = `
<navbar-extensions name="testing"></navbar-extensions>`;
describe('navbar-extensions directive', function () {
let $rootScope;
let $compile;
let stubRegistry;
beforeEach(function () {
ngMock.module('kibana', function (PrivateProvider) {
stubRegistry = new Registry({
index: ['name'],
group: ['appName'],
order: ['order']
});
PrivateProvider.swap(navbarExtensionsRegistry, stubRegistry);
});
ngMock.module('kibana/navbar');
// Create the scope
ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
});
});
function init(markup = defaultMarkup) {
// Give us a scope
const $el = angular.element(markup);
$compile($el)($rootScope);
$el.scope().$digest();
return $el;
}
describe('incorrect use', function () {
it('should throw if missing a name property', function () {
const markup = `<navbar-extensions><div class="button-group" role="toolbar"></div></navbar-extensions>`;
expect(() => init(markup)).to.throwException(/requires a name attribute/);
});
});
describe('injecting extensions', function () {
function registerExtension(def = {}) {
stubRegistry.register(function () {
return _.defaults(def, {
name: 'exampleButton',
appName: 'testing',
order: 0,
template: `
<button class="test-button">
<i aria-hidden="true" class="fa fa-rocket"></i>
</button>`
});
});
}
it('should append to end then order == 0', function () {
registerExtension({ order: 0 });
var $el = init();
expect($el.find('button').last().hasClass('test-button')).to.be.ok();
});
it('should enforce the order prop', function () {
registerExtension({
order: 1,
template: `
<button class="test-button-1">
<i aria-hidden="true" class="fa fa-rocket"></i>
</button>`
});
registerExtension({
order: 2,
template: `
<button class="test-button-2">
<i aria-hidden="true" class="fa fa-rocket"></i>
</button>`
});
registerExtension({
order: 0,
template: `
<button class="test-button-0">
<i aria-hidden="true" class="fa fa-rocket"></i>
</button>`
});
var $el = init();
expect($el.find('button').length).to.equal(3);
expect($el.find('button').last().hasClass('test-button-2')).to.be.ok();
expect($el.find('button').first().hasClass('test-button-0')).to.be.ok();
});
});
});

View file

@ -6,7 +6,7 @@ import uiModules from 'ui/modules';
const navbar = uiModules.get('kibana/navbar'); const navbar = uiModules.get('kibana/navbar');
navbar.directive('navbar', function (Private, $compile) { navbar.directive('navbarExtensions', function (Private, $compile) {
const navbarExtensions = Private(RegistryNavbarExtensionsProvider); const navbarExtensions = Private(RegistryNavbarExtensionsProvider);
const getExtensions = _.memoize(function (name) { const getExtensions = _.memoize(function (name) {
if (!name) throw new Error('navbar directive requires a name attribute'); if (!name) throw new Error('navbar directive requires a name attribute');
@ -16,36 +16,20 @@ navbar.directive('navbar', function (Private, $compile) {
return { return {
restrict: 'E', restrict: 'E',
template: function ($el, $attrs) { template: function ($el, $attrs) {
const $buttonGroup = $el.children('.button-group');
if ($buttonGroup.length !== 1) throw new Error('navbar must have exactly 1 button group');
const extensions = getExtensions($attrs.name); const extensions = getExtensions($attrs.name);
const buttons = $buttonGroup.children().detach().toArray(); const controls = extensions.map(function (extension, i) {
const controls = [
...buttons.map(function (button) {
return {
order: 0,
$el: $(button),
};
}),
...extensions.map(function (extension, i) {
return { return {
order: extension.order, order: extension.order,
index: i, index: i,
extension: extension, extension: extension,
}; };
}), });
];
_.sortBy(controls, 'order').forEach(function (control) { _.sortBy(controls, 'order').forEach(function (control) {
if (control.$el) {
return $buttonGroup.append(control.$el);
}
const { extension, index } = control; const { extension, index } = control;
const $ext = $(`<render-directive definition="navbar.extensions[${index}]"></render-directive>`); const $ext = $(`<render-directive definition="navbar.extensions[${index}]"></render-directive>`);
$ext.html(extension.template); $ext.html(extension.template);
$buttonGroup.append($ext); $el.append($ext);
}); });
return $el.html(); return $el.html();

View file

@ -1,8 +0,0 @@
<div class="config" ng-show="configTemplate">
<form role="form" class="container-fluid" ng-submit="configSubmit()">
<div ng-bind-template="{{configTemplate}}" />
</form>
<div class="config-close remove">
<i class="fa fa-chevron-circle-up" ng-click="close()"/>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more