Skip to content

Commit

Permalink
Add a method for plugins to add injected vars to every app (#9071)
Browse files Browse the repository at this point in the history
* [uiExports] add replaceInjectedVars() export type

* [ui] do not assume es plugin is always there

* [server/status] fix typo

* [ui] add errror handling to /app/{id} endpoint

* [ui] add tests for replaceInjectedVars()

* [npm] swap out jsdom with cheerio

* [server/ui] continue extender => replacer rename
  • Loading branch information
spalger authored Nov 18, 2016
1 parent f171766 commit dd46f75
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"auto-release-sinon": "1.0.3",
"babel-eslint": "4.1.8",
"chai": "3.5.0",
"cheerio": "0.22.0",
"chokidar": "1.6.0",
"chromedriver": "2.24.1",
"elasticdump": "2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/server/status/states.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ exports.all = [
severity: -1,
icon: 'toggle-off',
nicknames: [
'I\'m I even a thing?'
'Am I even a thing?'
]
}
];
Expand Down
13 changes: 13 additions & 0 deletions src/ui/__tests__/fixtures/test_app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = kibana => new kibana.Plugin({
uiExports: {
app: {
name: 'test_app',
main: 'plugins/test_app/index.js',
injectVars() {
return {
from_test_app: true
};
}
}
}
});
4 changes: 4 additions & 0 deletions src/ui/__tests__/fixtures/test_app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test_app",
"version": "kibana"
}
Empty file.
125 changes: 125 additions & 0 deletions src/ui/__tests__/ui_exports_replace_injected_vars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { resolve } from 'path';

import { delay } from 'bluebird';
import expect from 'expect.js';
import sinon from 'sinon';
import cheerio from 'cheerio';
import { noop } from 'lodash';

import KbnServer from '../../server/kbn_server';

const getInjectedVarsFromResponse = (resp) => {
const $ = cheerio.load(resp.payload);
const data = $('kbn-initial-state').attr('data');
return JSON.parse(data).vars;
};

const injectReplacer = (kbnServer, replacer) => {
// normally the replacer would be defined in a plugin's uiExports,
// but that requires stubbing out an entire plugin directory for
// each test, so we fake it and jam the replacer into uiExports
kbnServer.uiExports.injectedVarsReplacers.push(replacer);
};

describe('UiExports', function () {
describe('#replaceInjectedVars', function () {
this.slow(2000);
this.timeout(10000);

let kbnServer;
beforeEach(async () => {
kbnServer = new KbnServer({
server: { port: 0 }, // pick a random open port
logging: { silent: true }, // no logs
optimize: { enabled: false },
uiSettings: { enabled: false },
plugins: {
paths: [resolve(__dirname, './fixtures/test_app')] // inject an app so we can hit /app/{id}
},
});

await kbnServer.ready();
kbnServer.status.get('ui settings').state = 'green';
kbnServer.server.decorate('server', 'uiSettings', () => {
return { getDefaults: noop };
});
});

afterEach(async () => {
await kbnServer.close();
kbnServer = null;
});

it('allows sync replacing of injected vars', async () => {
injectReplacer(kbnServer, () => ({ a: 1 }));

const resp = await kbnServer.inject('/app/test_app');
const injectedVars = getInjectedVarsFromResponse(resp);

expect(injectedVars).to.eql({ a: 1 });
});

it('allows async replacing of injected vars', async () => {
const asyncThing = () => delay(100).return('world');

injectReplacer(kbnServer, async () => {
return {
hello: await asyncThing()
};
});

const resp = await kbnServer.inject('/app/test_app');
const injectedVars = getInjectedVarsFromResponse(resp);

expect(injectedVars).to.eql({
hello: 'world'
});
});

it('passes originalInjectedVars, request, and server to replacer', async () => {
const stub = sinon.stub();
injectReplacer(kbnServer, () => ({ foo: 'bar' }));
injectReplacer(kbnServer, stub);

await kbnServer.inject('/app/test_app');

sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.eql({ foo: 'bar' }); // originalInjectedVars
expect(stub.firstCall.args[1]).to.have.property('path', '/app/test_app'); // request
expect(stub.firstCall.args[1]).to.have.property('server', kbnServer.server); // request
expect(stub.firstCall.args[2]).to.be(kbnServer.server);
});

it('calls the methods sequentially', async () => {
injectReplacer(kbnServer, () => ({ name: '' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 's' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 'a' }));
injectReplacer(kbnServer, orig => ({ name: orig.name + 'm' }));

const resp = await kbnServer.inject('/app/test_app');
const injectedVars = getInjectedVarsFromResponse(resp);

expect(injectedVars).to.eql({ name: 'sam' });
});

it('propogates errors thrown in replacers', async () => {
injectReplacer(kbnServer, async () => {
await delay(100);
throw new Error('replacer failed');
});

const resp = await kbnServer.inject('/app/test_app');
expect(resp).to.have.property('statusCode', 500);
});

it('starts off with the injected vars for the app merged with the default injected vars', async () => {
const stub = sinon.stub();
injectReplacer(kbnServer, stub);
kbnServer.uiExports.defaultInjectedVars.from_defaults = true;

const resp = await kbnServer.inject('/app/test_app');
sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.eql({ from_defaults: true, from_test_app: true });
});
});
});
31 changes: 21 additions & 10 deletions src/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { format as formatUrl } from 'url';
import { readFileSync as readFile } from 'fs';
import { defaults } from 'lodash';
import Boom from 'boom';
import { reduce as reduceAsync } from 'bluebird';
import { resolve } from 'path';
import fromRoot from '../utils/from_root';
import UiExports from './ui_exports';
Expand Down Expand Up @@ -43,15 +44,19 @@ export default async (kbnServer, server, config) => {
server.route({
path: '/app/{id}',
method: 'GET',
handler: function (req, reply) {
async handler(req, reply) {
const id = req.params.id;
const app = uiExports.apps.byId[id];
if (!app) return reply(Boom.notFound('Unknown app ' + id));

if (kbnServer.status.isGreen()) {
return reply.renderApp(app);
} else {
return reply.renderStatusPage();
try {
if (kbnServer.status.isGreen()) {
await reply.renderApp(app);
} else {
await reply.renderStatusPage();
}
} catch (err) {
reply(Boom.wrap(err));
}
}
});
Expand All @@ -70,7 +75,11 @@ export default async (kbnServer, server, config) => {
defaults: await server.uiSettings().getDefaults(),
user: {}
},
vars: defaults(app.getInjectedVars() || {}, uiExports.defaultInjectedVars),
vars: await reduceAsync(
uiExports.injectedVarsReplacers,
async (acc, replacer) => await replacer(acc, this.request, server),
defaults(await app.getInjectedVars() || {}, uiExports.defaultInjectedVars)
)
};
}

Expand All @@ -83,16 +92,18 @@ export default async (kbnServer, server, config) => {
}

async function renderApp(app) {
const isElasticsearchPluginRed = server.plugins.elasticsearch.status.state === 'red';
const payload = await getPayload(app);
if (!isElasticsearchPluginRed) {
const payload = await getPayload.call(this, app);

const esStatus = kbnServer.status.getForPluginId('elasticsearch');
if (esStatus && esStatus.state !== 'red') {
payload.uiSettings.user = await server.uiSettings().getUserProvided();
}

return viewAppWithPayload.call(this, app, payload);
}

async function renderAppWithDefaultConfig(app) {
const payload = await getPayload(app);
const payload = await getPayload.call(this, app);
return viewAppWithPayload.call(this, app, payload);
}

Expand Down
6 changes: 6 additions & 0 deletions src/ui/ui_exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class UiExports {
this.consumers = [];
this.bundleProviders = [];
this.defaultInjectedVars = {};
this.injectedVarsReplacers = [];
}

consumePlugin(plugin) {
Expand Down Expand Up @@ -107,6 +108,11 @@ class UiExports {
_.merge(this.defaultInjectedVars, await injector.call(plugin, server, options));
});
};

case 'replaceInjectedVars':
return (plugin, replacer) => {
this.injectedVarsReplacers.push(replacer);
};
}
}

Expand Down

0 comments on commit dd46f75

Please sign in to comment.