mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Merge remote-tracking branch 'upstream/master' into kable
This commit is contained in:
commit
226151ebb9
29 changed files with 513 additions and 92 deletions
13
package.json
13
package.json
|
@ -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",
|
||||
|
|
44
src/cli/cluster/__tests__/_mock_cluster_fork.js
Normal file
44
src/cli/cluster/__tests__/_mock_cluster_fork.js
Normal 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');
|
||||
}());
|
||||
}
|
||||
}
|
59
src/cli/cluster/__tests__/cluster_manager.js
Normal file
59
src/cli/cluster/__tests__/cluster_manager.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
198
src/cli/cluster/__tests__/worker.js
Normal file
198
src/cli/cluster/__tests__/worker.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() ]; }
|
|
@ -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');
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
12
src/utils/binder_for.js
Normal file
12
src/utils/binder_for.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Binder } from './';
|
||||
|
||||
export default class BinderFor extends Binder {
|
||||
constructor(emitter) {
|
||||
super();
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
on(...args) {
|
||||
super.on(this.emitter, ...args);
|
||||
}
|
||||
}
|
4
src/utils/index.js
Normal file
4
src/utils/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export Binder from './binder';
|
||||
export BinderFor from './binder_for';
|
||||
export fromRoot from './from_root';
|
||||
export pkg from './package_json';
|
|
@ -69,7 +69,7 @@ define(function (require) {
|
|||
})
|
||||
.then(function selectField() {
|
||||
common.debug('Field = extension');
|
||||
return visualizePage.selectField('extension');
|
||||
return visualizePage.selectField('extension.raw');
|
||||
})
|
||||
.then(function setInterval() {
|
||||
common.debug('switch from Rows to Columns');
|
||||
|
|
|
@ -20,21 +20,6 @@ define(function (require) {
|
|||
return common.findTestSubject('settingsNav advanced').click();
|
||||
},
|
||||
|
||||
setAdvancedSettings: function setAdvancedSettings(propertyName, propertyValue) {
|
||||
var self = this;
|
||||
return common.findTestSubject('advancedSetting&' + propertyName + ' editButton')
|
||||
.click()
|
||||
.then(function setAdvancedSettingsClickPropertyValue(selectList) {
|
||||
return self.remote
|
||||
.findDisplayedByCssSelector('option[label="' + propertyValue + '"]')
|
||||
.click();
|
||||
})
|
||||
.then(function setAdvancedSettingsClickSaveButton() {
|
||||
return common.findTestSubject('advancedSetting&' + propertyName + ' saveButton')
|
||||
.click();
|
||||
});
|
||||
},
|
||||
|
||||
getAdvancedSettings: function getAdvancedSettings(propertyName) {
|
||||
common.debug('in setAdvancedSettings');
|
||||
return common.findTestSubject('advancedSetting&' + propertyName + ' currentValue')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue