diff --git a/package.json b/package.json index 80773a932f837..0259e8d3bee0e 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "ngreact": "0.5.1", "no-ui-slider": "1.2.0", "node-fetch": "1.3.2", + "opn": "^5.4.0", "oppsy": "^2.0.0", "pegjs": "0.9.0", "postcss-loader": "2.0.6", @@ -252,6 +253,7 @@ "@types/moment-timezone": "^0.5.8", "@types/mustache": "^0.8.31", "@types/node": "^8.10.20", + "@types/opn": "^5.1.0", "@types/prop-types": "^15.5.3", "@types/puppeteer": "^1.6.2", "@types/react": "16.3.14", diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 2aafd44aabc6b..347391dfe8e06 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -18,9 +18,12 @@ */ import { resolve } from 'path'; +import { format as formatUrl } from 'url'; +import opn from 'opn'; + import { debounce, invoke, bindAll, once, uniq } from 'lodash'; -import { fromEvent, race } from 'rxjs'; -import { first } from 'rxjs/operators'; +import * as Rx from 'rxjs'; +import { first, mapTo, filter, map, take } from 'rxjs/operators'; import Log from '../log'; import Worker from './worker'; @@ -88,6 +91,15 @@ export default class ClusterManager { bindAll(this, 'onWatcherAdd', 'onWatcherError', 'onWatcherChange'); + if (opts.open) { + this.setupOpen(formatUrl({ + protocol: config.get('server.ssl.enabled') ? 'https' : 'http', + hostname: config.get('server.host'), + port: config.get('server.port'), + pathname: (this.basePathProxy ? this.basePathProxy.basePath : ''), + })); + } + if (opts.watch) { const pluginPaths = config.get('plugins.paths'); const scanDirs = config.get('plugins.scanDirs'); @@ -124,6 +136,28 @@ export default class ClusterManager { } } + setupOpen(openUrl) { + const serverListening$ = Rx.merge( + Rx.fromEvent(this.server, 'listening') + .pipe(mapTo(true)), + Rx.fromEvent(this.server, 'fork:exit') + .pipe(mapTo(false)), + Rx.fromEvent(this.server, 'crashed') + .pipe(mapTo(false)) + ); + + const optimizeSuccess$ = Rx.fromEvent(this.optimizer, 'optimizeStatus') + .pipe(map(msg => !!msg.success)); + + Rx.combineLatest(serverListening$, optimizeSuccess$) + .pipe( + filter(([serverListening, optimizeSuccess]) => serverListening && optimizeSuccess), + take(1), + ) + .toPromise() + .then(() => opn(openUrl)); + } + setupWatching(extraPaths, extraIgnores) { const chokidar = require('chokidar'); const { fromRoot } = require('../../utils'); @@ -229,7 +263,10 @@ export default class ClusterManager { return Promise.resolve(); } - return race(fromEvent(this.server, 'listening'), fromEvent(this.server, 'crashed')) + return Rx.race( + Rx.fromEvent(this.server, 'listening'), + Rx.fromEvent(this.server, 'crashed') + ) .pipe(first()) .toPromise(); } diff --git a/src/cli/cluster/worker.js b/src/cli/cluster/worker.js index a7176bc0c734c..2d7c870d38522 100644 --- a/src/cli/cluster/worker.js +++ b/src/cli/cluster/worker.js @@ -125,6 +125,9 @@ export default class Worker extends EventEmitter { case 'WORKER_BROADCAST': this.emit('broadcast', data); break; + case 'OPTIMIZE_STATUS': + this.emit('optimizeStatus', data); + break; case 'WORKER_LISTENING': this.listening = true; this.emit('listening'); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index e1d0421003e07..8de60c209dc7c 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -191,6 +191,7 @@ export default function (program) { if (CAN_CLUSTER) { command .option('--dev', 'Run the server with development mode defaults') + .option('--open', 'Open a browser window to the base url after the server is started') .option('--ssl', 'Run the dev server using HTTPS') .option('--no-base-path', 'Don\'t put a proxy in front of the dev server, which adds a random basePath') .option('--no-watch', 'Prevents automatic restarts of the server in --dev mode'); @@ -214,6 +215,7 @@ export default function (program) { configs: [].concat(opts.config || []), cliArgs: { dev: !!opts.dev, + open: !!opts.open, envName: unknownOptions.env ? unknownOptions.env.name : undefined, quiet: !!opts.quiet, silent: !!opts.silent, diff --git a/src/core/server/config/__mocks__/env.ts b/src/core/server/config/__mocks__/env.ts index 839b3d4983462..2f9c0575858c3 100644 --- a/src/core/server/config/__mocks__/env.ts +++ b/src/core/server/config/__mocks__/env.ts @@ -30,6 +30,7 @@ export function getEnvOptions(options: DeepPartial = {}): EnvOptions configs: options.configs || [], cliArgs: { dev: true, + open: false, quiet: false, silent: false, watch: false, diff --git a/src/core/server/config/__snapshots__/env.test.ts.snap b/src/core/server/config/__snapshots__/env.test.ts.snap index ebe6c4ea83470..71609bf130142 100644 --- a/src/core/server/config/__snapshots__/env.test.ts.snap +++ b/src/core/server/config/__snapshots__/env.test.ts.snap @@ -7,6 +7,7 @@ Env { "basePath": false, "dev": true, "envName": "development", + "open": false, "optimize": false, "quiet": false, "repl": false, @@ -43,6 +44,7 @@ Env { "basePath": false, "dev": false, "envName": "production", + "open": false, "optimize": false, "quiet": false, "repl": false, @@ -78,6 +80,7 @@ Env { "cliArgs": Object { "basePath": false, "dev": true, + "open": false, "optimize": false, "quiet": false, "repl": false, @@ -113,6 +116,7 @@ Env { "cliArgs": Object { "basePath": false, "dev": false, + "open": false, "optimize": false, "quiet": false, "repl": false, @@ -148,6 +152,7 @@ Env { "cliArgs": Object { "basePath": false, "dev": false, + "open": false, "optimize": false, "quiet": false, "repl": false, @@ -183,6 +188,7 @@ Env { "cliArgs": Object { "basePath": false, "dev": false, + "open": false, "optimize": false, "quiet": false, "repl": false, diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index b5a1092724b84..516d6626296a1 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -50,6 +50,7 @@ export interface CliArgs { repl: boolean; basePath: boolean; optimize: boolean; + open: boolean; } export class Env { diff --git a/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap index ea64afb06caf7..1734a88912aec 100644 --- a/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap @@ -6,6 +6,7 @@ Array [ Object { "basePath": true, "dev": true, + "open": false, "optimize": false, "quiet": true, "repl": false, @@ -59,6 +60,7 @@ Array [ Object { "basePath": false, "dev": true, + "open": false, "optimize": false, "quiet": false, "repl": false, diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index d774a421c6f13..e96e98a339f3f 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -18,23 +18,33 @@ */ import WatchServer from './watch_server'; -import WatchOptimizer from './watch_optimizer'; +import WatchOptimizer, { STATUS } from './watch_optimizer'; export default async (kbnServer, kibanaHapiServer, config) => { + const watchOptimizer = new WatchOptimizer({ + log: (tags, data) => kibanaHapiServer.log(tags, data), + uiBundles: kbnServer.uiBundles, + profile: config.get('optimize.profile'), + sourceMaps: config.get('optimize.sourceMaps'), + prebuild: config.get('optimize.watchPrebuild'), + unsafeCache: config.get('optimize.unsafeCache'), + }); + const server = new WatchServer( config.get('optimize.watchHost'), config.get('optimize.watchPort'), config.get('server.basePath'), - new WatchOptimizer({ - log: (tags, data) => kibanaHapiServer.log(tags, data), - uiBundles: kbnServer.uiBundles, - profile: config.get('optimize.profile'), - sourceMaps: config.get('optimize.sourceMaps'), - prebuild: config.get('optimize.watchPrebuild'), - unsafeCache: config.get('optimize.unsafeCache'), - }) + watchOptimizer ); + watchOptimizer.status$.subscribe({ + next(status) { + process.send(['OPTIMIZE_STATUS', { + success: status.type === STATUS.SUCCESS + }]); + } + }); + let ready = false; const sendReady = () => { diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 8adacf880680d..7c033d50bf922 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -24,7 +24,7 @@ import BaseOptimizer from '../base_optimizer'; import { createBundlesRoute } from '../bundles_route'; -const STATUS = { +export const STATUS = { RUNNING: 'optimizer running', SUCCESS: 'optimizer completed successfully', FAILURE: 'optimizer failed with stats', diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index e124bde041b07..d99c7d9c434a3 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -57,6 +57,7 @@ export function createRootWithSettings(...settings: Array>) configs: [], cliArgs: { dev: false, + open: false, quiet: false, silent: false, watch: false, diff --git a/yarn.lock b/yarn.lock index 45a2c63a912e1..c39057fff2585 100644 --- a/yarn.lock +++ b/yarn.lock @@ -887,6 +887,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285" integrity sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w== +"@types/opn@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/opn/-/opn-5.1.0.tgz#bff7bc371677f4bdbb37884400e03fd81f743927" + integrity sha512-TNPrB7Y1xl06zDI0aGyqkgxjhIev3oJ+cdqlZ52MTAHauWpEL/gIUdHebIfRHFZk9IqSBpE2ci1DT48iZH81yg== + dependencies: + "@types/node" "*" + "@types/p-cancelable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-0.3.0.tgz#3e4fcc54a3dfd81d0f5b93546bb68d0df50553bb" @@ -8880,6 +8887,11 @@ is-word-character@^1.0.0: resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.1.tgz#5a03fa1ea91ace8a6eb0c7cd770eb86d65c8befb" integrity sha1-WgP6HqkazopusMfNdw64bWXIvvs= +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -11855,6 +11867,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +opn@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" + integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== + dependencies: + is-wsl "^1.1.0" + oppsy@2.x.x, oppsy@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/oppsy/-/oppsy-2.0.0.tgz#3a194517adc24c3c61cdc56f35f4537e93a35e34"