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",
"author": "Rashid Khan <rashid.khan@elastic.co>",
"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>",
"Court Ewing <court@elastic.co>",
"Jim Unger <jim.unger@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>",
"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>",
"Tim Sullivan <tim@elastic.co>",
"Jim Unger <jim.unger@elastic.co>"
"Spencer Alger <spencer.alger@elastic.co>",
"Tim Sullivan <tim@elastic.co>"
],
"scripts": {
"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';
module.exports = class ClusterManager {
constructor(opts, settings) {
constructor(opts = {}, settings = {}) {
this.log = new Log(opts.quiet, opts.silent);
this.addedCount = 0;

View file

@ -1,9 +1,9 @@
import _ from 'lodash';
import cluster from 'cluster';
let { resolve } = require('path');
let { EventEmitter } = require('events');
import { resolve } from 'path';
import { EventEmitter } from 'events';
import fromRoot from '../../utils/from_root';
import { BinderFor, fromRoot } from '../../utils';
let cliPath = fromRoot('src/cli');
let baseArgs = _.difference(process.argv.slice(2), ['--no-watch']);
@ -18,13 +18,6 @@ let dead = fork => {
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 {
constructor(opts) {
opts = opts || {};
@ -36,26 +29,33 @@ module.exports = class Worker extends EventEmitter {
this.watch = (opts.watch !== false);
this.startCount = 0;
this.online = false;
this.listening = false;
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 || []);
this.env = {
kbnWorkerType: this.type,
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) {
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
this.fork = null;
this.forkBinder = null;
this.online = false;
this.listening = false;
this.emit('fork:exit');
if (code) {
this.log.bad(`${this.title} crashed`, 'with status code', code);
@ -72,26 +72,48 @@ module.exports = class Worker extends EventEmitter {
this.start();
}
shutdown() {
async shutdown() {
if (this.fork && !dead(this.fork)) {
kill(this.fork);
this.fork.removeListener('message', this.onMessage);
this.fork.removeListener('online', this.onOnline);
this.fork.removeListener('disconnect', this.onDisconnect);
// kill the fork
this.fork.process.kill();
this.fork.killed = true;
// 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) {
if (!_.isArray(msg) || msg[0] !== 'WORKER_BROADCAST') return;
this.emit('broadcast', msg[1]);
parseIncomingMessage(msg) {
if (!_.isArray(msg)) return;
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() {
this.online = true;
this.emit('fork:online');
}
onDisconnect() {
this.online = false;
this.listening = false;
}
flushChangeBuffer() {
@ -102,9 +124,13 @@ module.exports = class Worker extends EventEmitter {
}, '');
}
start() {
// once "exit" event is received with 0 status, start() is called again
if (this.fork) return this.shutdown();
async start() {
if (this.fork) {
// once "exit" event is received with 0 status, start() is called again
this.shutdown();
await new Promise(cb => this.once('online', cb));
return;
}
if (this.changes.length) {
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.on('message', this.onMessage);
this.fork.on('online', this.onOnline);
this.fork.on('disconnect', this.onDisconnect);
this.forkBinder = new BinderFor(this.fork);
// 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) {
return action.call(this, function (...args) {
var ret = fn.apply(this, args);
let ret = fn.apply(this, args);
if (ret && typeof ret.then === 'function') {
ret.then(null, function (e) {
console.log('FATAL CLI ERROR', e.stack);

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ program
.command('help <command>')
.description('get the help for a specific command')
.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}`);
cmd.help();
});
@ -35,7 +35,7 @@ program
});
// 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) {
program.defaultHelp();
}

View file

@ -1,6 +1,6 @@
import path from 'path';
import expect from 'expect.js';
import fromRoot from '../../../utils/from_root';
import { fromRoot } from '../../../utils';
import { resolve } from 'path';
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 Logger from '../lib/logger';
import pkg from '../../utils/package_json';

View file

@ -1,5 +1,5 @@
import _ from 'lodash';
import fromRoot from '../../utils/from_root';
import { fromRoot } from '../../utils';
import KbnServer from '../../server/kbn_server';
import readYamlConfig from '../../cli/serve/read_yaml_config';
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 Logger from '../lib/logger';
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 Logger from '../lib/logger';
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(65, 90), // 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
var docCount = 0;
var buckets = _.times(_.random(40, 200), function () {
let docCount = 0;
let buckets = _.times(_.random(40, 200), function () {
return _.sample(geoHashCharts, 3).join('');
})
.sort()
.map(function (geoHash) {
var count = _.random(1, 5000);
let count = _.random(1, 5000);
totalDocCount += count;
docCount += count;

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
function stubbedLogstashFields() {
var sourceData = [
let sourceData = [
{ 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: '@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';
export default function (Private, Promise) {
var indexPatterns = Private(FixturesStubbedLogstashIndexPatternProvider);
var getIndexPatternStub = sinon.stub();
let indexPatterns = Private(FixturesStubbedLogstashIndexPatternProvider);
let getIndexPatternStub = sinon.stub();
getIndexPatternStub.returns(Promise.resolve(indexPatterns));
var courier = {
let courier = {
indexPatterns: { get: getIndexPatternStub },
getStub: getIndexPatternStub
};

View file

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

View file

@ -1,7 +1,7 @@
import FixturesLogstashFieldsProvider from 'fixtures/logstash_fields';
function stubbedDocSourceResponse(Private) {
var mockLogstashFields = Private(FixturesLogstashFieldsProvider);
let mockLogstashFields = Private(FixturesLogstashFieldsProvider);
return function (id, index) {
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 FixturesLogstashFieldsProvider from 'fixtures/logstash_fields';
export default function stubbedLogstashIndexPatternService(Private) {
var StubIndexPattern = Private(TestUtilsStubIndexPatternProvider);
var fieldTypes = Private(IndexPatternsFieldTypesProvider);
var mockLogstashFields = Private(FixturesLogstashFieldsProvider);
let StubIndexPattern = Private(TestUtilsStubIndexPatternProvider);
let fieldTypes = Private(IndexPatternsFieldTypesProvider);
let mockLogstashFields = Private(FixturesLogstashFieldsProvider);
var fields = mockLogstashFields.map(function (field) {
let fields = mockLogstashFields.map(function (field) {
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 (!_.has(field, 'sortable')) field.sortable = type.sortable;
if (!_.has(field, 'filterable')) field.filterable = type.filterable;
return field;
});
var indexPattern = new StubIndexPattern('logstash-*', 'time', fields);
let indexPattern = new StubIndexPattern('logstash-*', 'time', fields);
indexPattern.id = 'logstash-*';
return indexPattern;

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');

View file

@ -1,5 +1,8 @@
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
// this file is not transpiled
'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) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');

View file

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

View file

@ -26,7 +26,7 @@ docViewsRegistry.register(function () {
};
$scope.showArrayInObjectsWarning = function (row, field) {
var value = $scope.flattened[field];
let value = $scope.flattened[field];
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 FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html';
export default function TileMapVisType(Private, getAppState, courier, config) {
const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider);
const Schemas = Private(VisSchemasProvider);
@ -120,6 +121,8 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
group: 'buckets',
name: 'split',
title: 'Split Chart',
deprecate: true,
deprecateMessage: 'The Split Chart feature for Tile Maps has been deprecated.',
min: 0,
max: 1
}

View file

@ -1,60 +1,11 @@
<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">
<span ng-show="dash.id" class="kibana-nav-info-title">
<span ng-bind="::dash.title"></span>
</span>
</div>
<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>
</kbn-top-nav>
<navbar ng-show="chrome.getVisible()" name="dashboard-search">
<form name="queryInput"
@ -91,7 +42,7 @@
<div ng-show="!state.panels.length" class="text-center start-screen">
<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>
<dashboard-grid></dashboard-grid>

View file

@ -1,14 +1,13 @@
import _ from 'lodash';
import $ from 'jquery';
import angular from 'angular';
import ConfigTemplate from 'ui/config_template';
import chrome from 'ui/chrome';
import 'ui/directives/config';
import 'ui/directives/kbn_top_nav';
import 'ui/courier';
import 'ui/config';
import 'ui/notify';
import 'ui/typeahead';
import 'ui/navbar';
import 'ui/navbar_extensions';
import 'ui/share';
import 'plugins/kibana/dashboard/directives/grid';
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.configTemplate = new ConfigTemplate({
save: require('plugins/kibana/dashboard/partials/save_dashboard.html'),
load: require('plugins/kibana/dashboard/partials/load_dashboard.html'),
share: require('plugins/kibana/dashboard/partials/share.html'),
pickVis: require('plugins/kibana/dashboard/partials/pick_visualization.html'),
options: require('plugins/kibana/dashboard/partials/options.html'),
filter: require('ui/chrome/config/filter.html'),
interval: require('ui/chrome/config/interval.html')
});
$scope.topNavMenu = [{
key: 'new',
description: 'New Dashboard',
run: function () { kbnUrl.change('/dashboard', {}); },
}, {
key: 'add',
description: 'Add a panel to the dashboard',
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');
@ -198,7 +209,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
dash.save()
.then(function (id) {
$scope.configTemplate.close('save');
$scope.kbnTopNav.close('save');
if (id) {
notify.info('Saved Dashboard as "' + dash.title + '"');
if (dash.id !== $routeParams.id) {

View file

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

View file

@ -1,5 +1,5 @@
<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">
<span ng-show="opts.savedSearch.id" class="kibana-nav-info-title">
<span ng-bind="::opts.savedSearch.title"></span>
@ -9,44 +9,7 @@
<strong class="discover-info-hits">{{(hits || 0) | number:0}}</strong>
<ng-pluralize count="hits" when="{'1':'hit', 'other':'hits'}"></ng-pluralize>
</div>
<div class="kibana-nav-actions button-group" role="toolbar">
<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>
</kbn-top-nav>
<navbar name="discover-search">
<form role="form" class="fill inline-form" ng-submit="fetch()" name="discoverSearch">
<div class="typeahead" kbn-typeahead="discover">
@ -116,7 +79,7 @@
<div ng-show="opts.timefield">
<p>
<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>
</div>

View file

@ -1,6 +1,6 @@
import 'plugins/kibana/discover/saved_searches/saved_searches';
import 'plugins/kibana/discover/directives/timechart';
import 'ui/navbar';
import 'ui/navbar_extensions';
import 'ui/collapsible_sidebar';
import 'plugins/kibana/discover/components/field_chooser/field_chooser';
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...">
</div>
<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
</button>
</div>

View file

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

View file

@ -2,6 +2,7 @@
<label>Select {{ groupName }} type</label>
<ul class="form-group list-group list-group-menu">
<li
ng-hide="schema.deprecate"
ng-repeat="schema in availableSchema"
ng-click="add.submit(schema)"
class="list-group-item list-group-menu-item">
@ -18,10 +19,10 @@
class="vis-editor-agg-wide-btn">
<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 }}
</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 }}
</div>
</div>

View file

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

View file

@ -10,4 +10,13 @@
style="display: none;">
</div>
<!-- schema editors get added down here: aggSelect.html, agg_types/controls/*.html -->
<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 -->

View file

@ -1,60 +1,12 @@
<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">
<span ng-show="savedVis.id" class="vis-editor-info-title">
<span ng-bind="::savedVis.title"></span>
</span>
</div>
<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>
</kbn-top-nav>
<navbar ng-if="chrome.getVisible()" name="visualize-search">
<div class="fill bitty-modal-container">
<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/editor/sidebar';
import 'plugins/kibana/visualize/editor/agg_filter';
import 'ui/navbar';
import 'ui/navbar_extensions';
import 'ui/visualize';
import 'ui/collapsible_sidebar';
import 'ui/share';
import angular from 'angular';
import ConfigTemplate from 'ui/config_template';
import Notifier from 'ui/notify/notifier';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
import DocTitleProvider from 'ui/doc_title';
@ -80,14 +79,27 @@ uiModules
const searchSource = savedVis.searchSource;
// config panel templates
const configTemplate = new ConfigTemplate({
save: require('plugins/kibana/visualize/editor/panels/save.html'),
load: require('plugins/kibana/visualize/editor/panels/load.html'),
share: require('plugins/kibana/visualize/editor/panels/share.html'),
filter: require('ui/chrome/config/filter.html'),
interval: require('ui/chrome/config/interval.html')
});
$scope.topNavMenu = [{
key: 'new',
description: 'New Visualization',
run: function () { kbnUrl.change('/visualize', {}); }
}, {
key: 'save',
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) {
docTitle.change(savedVis.title);
@ -129,7 +141,6 @@ uiModules
$scope.uiState = $state.makeStateful('uiState');
$scope.timefilter = timefilter;
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter');
$scope.configTemplate = configTemplate;
editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state);
editableVis.listeners.brush = vis.listeners.brush = brushEvent;
@ -235,7 +246,7 @@ uiModules
savedVis.save()
.then(function (id) {
configTemplate.close('save');
$scope.kbnTopNav.close('save');
if (id) {
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 { resolve } from 'path';
import { map, fromNode } from 'bluebird';

View file

@ -1,7 +1,7 @@
import { union } from 'lodash';
import findSourceFiles from './find_source_files';
import fromRoot from '../../utils/from_root';
import { fromRoot } from '../../utils';
export default (kibana) => {
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)
*
* var schema = { ... }
* let schema = { ... }
* new Config(schema);
*
*/
var data = {
let data = {
test: {
hosts: ['host-01', 'host-02'],
client: {
@ -25,7 +25,7 @@ var data = {
}
};
var schema = Joi.object({
let schema = Joi.object({
test: Joi.object({
enable: Joi.boolean().default(true),
hosts: Joi.array().items(Joi.string()),
@ -44,39 +44,39 @@ describe('lib/config/config', function () {
describe('constructor', function () {
it('should not allow any config if the schema is not passed', function () {
var config = new Config();
var run = function () {
let config = new Config();
let run = function () {
config.set('something.enable', true);
};
expect(run).to.throwException();
});
it('should allow keys in the schema', function () {
var config = new Config(schema);
var run = function () {
let config = new Config(schema);
let run = function () {
config.set('test.client.host', 'http://0.0.0.0');
};
expect(run).to.not.throwException();
});
it('should not allow keys not in the schema', function () {
var config = new Config(schema);
var run = function () {
let config = new Config(schema);
let run = function () {
config.set('paramNotDefinedInTheSchema', true);
};
expect(run).to.throwException();
});
it('should not allow child keys not in the schema', function () {
var config = new Config(schema);
var run = function () {
let config = new Config(schema);
let run = function () {
config.set('test.client.paramNotDefinedInTheSchema', true);
};
expect(run).to.throwException();
});
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.client.type')).to.be('datastore');
});
@ -92,7 +92,7 @@ describe('lib/config/config', function () {
it('should reset the config object with new values', function () {
config.set(data);
var newData = config.get();
let newData = config.get();
newData.test.enable = false;
config.resetTo(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 () {
var hosts = ['host-01', 'host-02'];
let hosts = ['host-01', 'host-02'];
config.set({ test: { enable: false, hosts: hosts } });
expect(config.get('test.enable')).to.be(false);
expect(config.get('test.hosts')).to.eql(hosts);
});
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 });
expect(config.get('test.enable')).to.be(false);
expect(config.get('test.hosts')).to.eql(hosts);
});
it('should override values with just the values present', function () {
var newData = _.cloneDeep(data);
let newData = _.cloneDeep(data);
config.set(data);
newData.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) {
var run = function () {
let run = function () {
config.set('test.enable', 'something');
};
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 () {
var newData = _.cloneDeep(data);
let newData = _.cloneDeep(data);
newData.test.enable = true;
expect(config.get()).to.eql(newData);
});
@ -194,14 +194,14 @@ describe('lib/config/config', function () {
});
it('should throw exception for unknown config values', function () {
var run = function () {
let run = function () {
config.get('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 () {
var run = function getUndefValue() {
let run = function getUndefValue() {
config.get('test.undefValue');
};
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 () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('myTest', newSchema);
expect(config.get('myTest.test')).to.be(true);
});
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);
expect(config.get('prefix')).to.eql({ myTest: { 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 () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
var run = function () {
let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
let run = function () {
config.extendSchema('test', newSchema);
};
expect(run).to.throwException();
@ -241,7 +241,7 @@ describe('lib/config/config', function () {
describe('#removeSchema(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)
}));
@ -251,7 +251,7 @@ describe('lib/config/config', 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');
});

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import expect from 'expect.js';
describe('override(target, source)', function () {
it('should override the values form source to target', function () {
var target = {
let target = {
test: {
enable: true,
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({
test: {
enable: true,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import Hapi from 'hapi';
import { constant, once, compact, flatten } from 'lodash';
import { promisify, resolve, fromNode } from 'bluebird';
import fromRoot from '../utils/from_root';
import pkg from '../utils/package_json';
import { isWorker } from 'cluster';
import { fromRoot, pkg } from '../utils';
let rootDir = fromRoot('.');
@ -78,6 +78,11 @@ module.exports = class KbnServer {
await this.ready();
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}`);
return server;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import { each } from 'bluebird';
import PluginCollection from './plugin_collection';
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 pluginPaths = [].concat(config.get('plugins.paths') || []);

View file

@ -17,21 +17,21 @@ describe('ServerStatus class', function () {
describe('#create(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);
});
});
describe('#get(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);
});
});
describe('#getState(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();
expect(serverStatus.getState('name')).to.be('green');
});
@ -39,11 +39,11 @@ describe('ServerStatus class', function () {
describe('#overall()', 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');
var match = function (overall, state) {
let match = function (overall, state) {
expect(overall).to.have.property('state', state.id);
expect(overall).to.have.property('title', state.title);
expect(overall).to.have.property('icon', state.icon);
@ -65,20 +65,20 @@ describe('ServerStatus class', function () {
describe('#toJSON()', function () {
it('serializes to overall status and individuals', function () {
var one = serverStatus.create('one');
var two = serverStatus.create('two');
var three = serverStatus.create('three');
let one = serverStatus.create('one');
let two = serverStatus.create('two');
let three = serverStatus.create('three');
one.green();
two.yellow();
three.red();
var obj = JSON.parse(JSON.stringify(serverStatus));
let obj = JSON.parse(JSON.stringify(serverStatus));
expect(obj).to.have.property('overall');
expect(obj.overall.state).to.eql(serverStatus.overall().state);
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.two).to.have.property('state', 'yellow');
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) {
var status = serverStatus.create('test');
let status = serverStatus.create('test');
status.once('change', function (prev, prevMsg) {
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 () {
var status = serverStatus.create('test');
var stub = sinon.stub();
let status = serverStatus.create('test');
let stub = sinon.stub();
status.on('change', stub);
status.green('Ready');
status.green('Ready');
@ -50,17 +50,17 @@ describe('Status class', function () {
});
it('should create a JSON representation of the status', function () {
var status = serverStatus.create('test');
let status = serverStatus.create('test');
status.green('Ready');
var json = status.toJSON();
let json = status.toJSON();
expect(json.state).to.eql('green');
expect(json.message).to.eql('Ready');
});
it('should call on handler if status is already matched', function (done) {
var status = serverStatus.create('test');
var msg = 'Test Ready';
let status = serverStatus.create('test');
let msg = 'Test Ready';
status.green(msg);
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) {
var status = serverStatus.create('test');
var msg = 'Test Ready';
let status = serverStatus.create('test');
let msg = 'Test Ready';
status.green(msg);
status.once('green', function (prev, prevMsg) {
@ -88,16 +88,16 @@ describe('Status class', function () {
function testState(color) {
it(`should change the state to ${color} when #${color}() is called`, function () {
var status = serverStatus.create('test');
var message = 'testing ' + color;
let status = serverStatus.create('test');
let message = 'testing ' + color;
status[color](message);
expect(status).to.have.property('state', color);
expect(status).to.have.property('message', message);
});
it(`should trigger the "change" listner when #${color}() is called`, function (done) {
var status = serverStatus.create('test');
var message = 'testing ' + color;
let status = serverStatus.create('test');
let message = 'testing ' + color;
status.on('change', function (prev, prevMsg) {
expect(status.state).to.be(color);
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) {
var status = serverStatus.create('test');
var message = 'testing ' + color;
let status = serverStatus.create('test');
let message = 'testing ' + color;
status.on(color, function (prev, prevMsg) {
expect(status.state).to.be(color);
expect(status.message).to.be(message);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ Bluebird.longStackTraces();
* replace the Promise service with Bluebird so that tests
* 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 () {
* beforeEach(noDigestPromises.activate);
@ -16,7 +16,7 @@ Bluebird.longStackTraces();
* });
*/
var active = false;
let active = false;
uiModules
.get('kibana')

View file

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

View file

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

View file

@ -1,9 +1,9 @@
const store = Symbol('store');
export default class TabFakeStore {
export default class StubBrowserStorage {
constructor() { this[store] = new Map(); }
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); }
getKeys() { return [ ...this[store].keys() ]; }
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 Tab from '../tab';
import expect from 'expect.js';
import TabFakeStore from './_tab_fake_store';
import StubBrowserStorage from './fixtures/stub_browser_storage';
describe('Chrome Tab', function () {
describe('construction', function () {
@ -88,7 +89,7 @@ describe('Chrome Tab', function () {
});
it('discovers the lastUrl', function () {
const lastUrlStore = new TabFakeStore();
const lastUrlStore = new StubBrowserStorage();
const tab = new Tab({ id: 'foo', lastUrlStore });
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 () {
const lastUrlStore = new TabFakeStore();
const lastUrlStore = new StubBrowserStorage();
const tab = new Tab({ id: 'foo', baseUrl: '/bar', lastUrlStore });
tab.setLastUrl('/bar/foo/1');
@ -114,7 +115,7 @@ describe('Chrome Tab', function () {
describe('#setLastUrl()', 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 });
expect(tab.lastUrl).to.not.equal('foo');

View file

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

View file

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

View file

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

View file

@ -1,45 +1,73 @@
import expect from 'expect.js';
import initChromeNavApi from 'ui/chrome/api/nav';
import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage';
const basePath = '/someBasePath';
function getChrome(customInternals = { basePath }) {
function init(customInternals = { basePath }) {
const chrome = {};
initChromeNavApi(chrome, {
const internals = {
nav: [],
...customInternals,
});
return chrome;
};
initChromeNavApi(chrome, internals);
return { chrome, internals };
}
describe('chrome nav apis', function () {
describe('#getBasePath()', function () {
it('returns the basePath', function () {
const chrome = getChrome();
const { chrome } = init();
expect(chrome.getBasePath()).to.be(basePath);
});
});
describe('#addBasePath()', function () {
it('returns undefined when nothing is passed', function () {
const chrome = getChrome();
const { chrome } = init();
expect(chrome.addBasePath()).to.be(undefined);
});
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`);
});
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');
});
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`);
});
});
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) {
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) {
for (const link of internals.nav) {
link.active = startsWith(url, link.url);
const { appId, globalState: newGlobalState } = decodeKibanaUrl(url);
for (const link of internals.nav) {
const matchingTab = find(internals.tabs, { rootUrl: link.url });
link.active = startsWith(url, link.url);
if (link.active) {
setLastUrl(link, url);
continue;
}
const matchingTab = find(internals.tabs, { rootUrl: link.url });
if (matchingTab) {
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 >
<!-- TODO: These config dropdowns shouldn't be hard coded -->
<nav class="app-links-wrapper">
<nav class="app-links-wrapper" ng-show="chrome.getVisible()">
<li
ng-if="!chrome.getBrand('logo') && !chrome.getBrand('smallLogo')"
aria-label="{{ chrome.getAppTitle() }} Logo"
@ -21,28 +21,12 @@
<app-switcher>
</app-switcher>
<div class="bottom-apps hide app-links">
<div class="app-link">
<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 class="bottom-apps">
<div class="chrome-actions app-links" kbn-chrome-append-nav-controls></div>
</div>
</nav>
<div class="app-wrapper">
<div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
<div class="app-wrapper-panel">
<kbn-notifications list="notifList"></kbn-notifications>
<nav
@ -80,6 +64,7 @@
</div>
<!-- /Full navbar -->
</nav>
<kbn-loading-indicator></kbn-loading-indicator>
<div class="application" ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()" ng-view></div>
</div>
</div>

View file

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

View file

@ -1,7 +1,7 @@
<kbn-timepicker
from="opts.timefilter.time.from"
to="opts.timefilter.time.to"
mode="opts.timefilter.time.mode"
from="timefilter.time.from"
to="timefilter.time.to"
mode="timefilter.time.mode"
active-tab="'interval'"
interval="opts.timefilter.refreshInterval">
interval="timefilter.refreshInterval">
</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;
background-color: #fff;
&.hidden-chrome { left: 0; }
&-panel {
.flex-parent(@shrink: 0);
box-shadow: -4px 0px 3px rgba(0,0,0,0.2);
@ -84,7 +85,6 @@ body { overflow-x: hidden; }
.app-icon {
float: left;
filter: invert(100%);
font-weight: bold;
text-align: center;
font-size: 1.7em;
@ -95,6 +95,11 @@ body { overflow-x: hidden; }
> img {
height: 18px;
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;
}
img {
filter: invert(100%);
filter: none;
}
}

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import $ from 'jquery';
import UiModules from 'ui/modules';
import ConfigTemplate from 'ui/config_template';
export default function (chrome, internals) {
@ -46,10 +45,6 @@ export default function (chrome, internals) {
// and some local values
chrome.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;
$scope.appSwitcherTemplate = new ConfigTemplate({
switcher: '<app-switcher></app-switcher>'
});
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 NormalizeSortRequestProvider from 'ui/courier/data_source/_normalize_sort_request';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import _ from 'lodash';
describe('SearchSource#normalizeSortRequest', function () {
let normalizeSortRequest;
@ -87,4 +88,17 @@ describe('SearchSource#normalizeSortRequest', function () {
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 = _.defaults({}, sortValue, defaultSortOptions);
if (sortField === '_score') {
delete sortValue.unmapped_type;
}
}
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');
navbar.directive('navbar', function (Private, $compile) {
navbar.directive('navbarExtensions', function (Private, $compile) {
const navbarExtensions = Private(RegistryNavbarExtensionsProvider);
const getExtensions = _.memoize(function (name) {
if (!name) throw new Error('navbar directive requires a name attribute');
@ -16,36 +16,20 @@ navbar.directive('navbar', function (Private, $compile) {
return {
restrict: 'E',
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 buttons = $buttonGroup.children().detach().toArray();
const controls = [
...buttons.map(function (button) {
return {
order: 0,
$el: $(button),
};
}),
...extensions.map(function (extension, i) {
return {
order: extension.order,
index: i,
extension: extension,
};
}),
];
const controls = extensions.map(function (extension, i) {
return {
order: extension.order,
index: i,
extension: extension,
};
});
_.sortBy(controls, 'order').forEach(function (control) {
if (control.$el) {
return $buttonGroup.append(control.$el);
}
const { extension, index } = control;
const $ext = $(`<render-directive definition="navbar.extensions[${index}]"></render-directive>`);
$ext.html(extension.template);
$buttonGroup.append($ext);
$el.append($ext);
});
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