diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index cda40dc3c..89186b423 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -58,7 +58,7 @@ export default class Model extends Observable { this.frameworkInfoEnabled = false; this.frameworkInfo = RemoteData.notAsked(); - this.getFrameworkInfo(); + this.frameworkInfoLoaded = this.getFrameworkInfo(); this.inspectorEnabled = false; this.accountMenuEnabled = false; @@ -67,7 +67,6 @@ export default class Model extends Observable { this.router = new QueryRouter(); this.router.observe(this.handleLocationChange.bind(this)); this.router.bubbleTo(this); - this.handleLocationChange(); // Init first page // Setup keyboard and wheel dispatchers window.addEventListener('keydown', this.handleKeyboardDown.bind(this)); @@ -77,6 +76,10 @@ export default class Model extends Observable { this.ws = new WebSocketClient(); this.ws.addListener('command', this.handleWSCommand.bind(this)); this.ws.addListener('authed', this.handleWSAuthed.bind(this)); + this.wsSettled = new Promise((resolve) => { + this.ws.addListener('authed', resolve); + this.ws.addListener('close', resolve); + }); this.ws.addListener('close', this.handleWSClose.bind(this)); // update router on model change @@ -86,6 +89,9 @@ export default class Model extends Observable { this.zoom = new Zoom(); this.zoom.bubbleTo(this); + + // Must run last as live=true awaits frameworkInfoLoaded and wsSettled, so both must exist before the URL is parsed + this.handleLocationChange(); // Init first page } /** @@ -362,7 +368,7 @@ export default class Model extends Observable { return; } else if (params.profile) { this.getProfile(params.profile); - return; + return; // live=true ignored if a profile is used } else if (params.q) { this.getUserProfile(); try { @@ -371,10 +377,39 @@ export default class Model extends Observable { this.log.filter.resetCriteria(); this.updateRouteOnModelChange(); this.notification.show(`Invalid URL filter format: ${error.message}`, 'danger'); + return; // don't go live when q is invalid } } else { this.getUserProfile(); } + + if (params.live === 'true') { + this.startLiveModeFromURL(); + } + } + + /** + * Start live mode from URL once dependencies are ready + */ + async startLiveModeFromURL() { + await Promise.all([ + this.frameworkInfoLoaded, + this.wsSettled, + ]); + + if (!this.ws.authed) { + return; // connection lost before auth, handleWSClose has already notified the user + } + + if (!this.log.queryResult.isNotAsked()) { + return; // user started a query as framework has loaded but WS not yet + } + if (!this.log.isLiveModeAvailable()) { + this.notification.show('Live mode is currently unavailable, loaded in query mode', 'danger', 3000); + return; + } + + this.log.liveStart(); } /** diff --git a/InfoLogger/test/public/live-mode-mocha.js b/InfoLogger/test/public/live-mode-mocha.js index 271786f8e..a120311d1 100644 --- a/InfoLogger/test/public/live-mode-mocha.js +++ b/InfoLogger/test/public/live-mode-mocha.js @@ -17,6 +17,14 @@ const test = require('../mocha-index'); const isFieldEmpty = (value) => value === undefined || value === null || value === ''; +const getLiveState = (page) => page.evaluate(() => ({ + activeMode: window.model.log.activeMode, + className: document.querySelector('#live-button').className, + title: document.title, + autoScroll: window.model.log.autoScrollLive, + search: decodeURIComponent(window.location.search), +})); + describe('Live Mode test-suite', async () => { let baseUrl = null; let page = null; @@ -24,6 +32,10 @@ describe('Live Mode test-suite', async () => { ({ helpers: { baseUrl }, page } = test); }); + after(async () => { + await page.evaluate(() => window.model.log.liveStop()); + }); + it('should successfully go to homepage with predefined filters', async () => { await page.goto(baseUrl, { waitUntil: 'networkidle0' }); const location = await page.evaluate(() => window.location); @@ -118,6 +130,44 @@ describe('Live Mode test-suite', async () => { assert.ok(isUserNameMatching); }); + it('should stay in QUERY mode when live=true is combined with an invalid url filter', async () => { + await page.goto(`${baseUrl}?q={"severity":{"in":"E F"}&live=true`, { waitUntil: 'networkidle0' }); + const { activeMode, notification } = await page.evaluate(() => ({ + activeMode: window.model.log.activeMode, + notification: window.model.notification, + })); + + assert.strictEqual(activeMode, 'Query'); + assert.strictEqual(notification.type, 'danger'); + assert.ok(notification.message.startsWith('Invalid URL filter format')); + }); + + it('should successfully enable LIVE mode from url parameter with the defined filter', async () => { + await page.goto(`${baseUrl}?q={"severity":{"in":"E F"}}&live=true`, { waitUntil: 'networkidle0' }); + + const { activeMode, className, search, autoScroll } = await getLiveState(page); + + assert.strictEqual(autoScroll, true); + assert.strictEqual(activeMode, 'Running'); + assert.strictEqual(className, 'btn bold btn-success active'); + + // Check if filter is still applied + assert.strictEqual(search, '?q={"severity":{"in":"E F"}}'); + }); + + it('should successfully enable LIVE mode from url parameter with no filter', async () => { + await page.goto(`${baseUrl}?live=true`, { waitUntil: 'networkidle0' }); + const { activeMode, className, search, title, autoScroll } = await getLiveState(page); + + assert.strictEqual(autoScroll, true); + assert.strictEqual(activeMode, 'Running'); + assert.strictEqual(className, 'btn bold btn-success active'); + + assert.strictEqual(search, '?q={"severity":{"in":"I W E F"}}'); + + assert.ok(title.endsWith(' LIVE'), `unexpected title: ${title}`); + }); + describe('Empty field filters in live mode', async () => { it('should only receive logs with empty rolename when emptyFor is set to "match"', async () => { await page.evaluate(() => window.model.log.liveStop('Paused'));