Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions InfoLogger/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -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
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -355,34 +361,72 @@ export default class Model extends Observable {
* Delegates sub-model actions depending if location is filters or profile
* @param {object} params - URL parameters
*/
parseLocation(params) {
async parseLocation(params) {
if (params.profile && params.q) {
this.log.filter.resetCriteria();
this.notification.show('URL can contain only filters or profile, not both', 'warning');
return;
} else if (params.profile) {
this.getProfile(params.profile);
return;
} else if (params.q) {
this.getUserProfile();
};

const profileLoaded = params.profile ? this.getProfile(params.profile) : this.getUserProfile();

let filterError = null;
if (params.q) {
try {
this.log.filter.fromObject(JSON.parse(params.q));
} catch (error) {
this.log.filter.resetCriteria();
this.updateRouteOnModelChange();
this.notification.show(`Invalid URL filter format: ${error.message}`, 'danger');
filterError = error; // don't go live when q is invalid
}
} else {
this.getUserProfile();
}

// await as our notification system only shows one notification at a time
// and profile loading will trigger a notification
// and we don't want it and error messages to interfere with each other
await profileLoaded;

if (filterError) {
this.notification.show(`Invalid URL filter format: ${filterError.message}`, 'danger');
return; // don't go live when q is invalid
}

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()) {
this.notification.show('A query was run before live mode was ready, staying in query mode', 'warning', 3000);
return;
}
if (!this.log.isLiveModeAvailable()) {
this.notification.show('Live mode is currently unavailable, loaded in query mode', 'danger', 3000);
return;
}

this.log.liveStart();
}

/**
* When model change (filters), update address bar with the filter
* do it silently to avoid infinite loop
*/
updateRouteOnModelChange() {
this.router.go(this.log.filter.queryString, true, true);
this.router.go(this.log.filter.queryString + (this.log.activeMode === 'Running' ? '&live=true' : ''), true, true);
}

/**
Expand Down
67 changes: 66 additions & 1 deletion InfoLogger/test/public/live-mode-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,7 +33,7 @@ describe('Live Mode test-suite', async () => {
});

after(async () => {
await page.evaluate(() => model.log.liveStop('Query'));
await page.evaluate(() => model.log.liveStop());
});

it('should successfully go to homepage with predefined filters', async () => {
Expand Down Expand Up @@ -122,6 +130,63 @@ 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: model.log.activeMode,
notification: 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' });
await page.waitForFunction(() => model.log.activeMode === 'Running', { timeout: 5000 });

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' });
await page.waitForFunction(() => model.log.activeMode === 'Running', { timeout: 5000 });

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}`);
});

it('should notify and stay in QUERY when live service is unavailable', async () => {
const { activeMode, message } = await page.evaluate(async () => {
model.log.liveStop();
const { status } = window.model.frameworkInfo.payload.infoLoggerServer;
status.ok = false;
try {
await model.startLiveModeFromURL();
return { activeMode: model.log.activeMode, message: model.notification.message };
} finally {
status.ok = true;
}
});
assert.strictEqual(activeMode, 'Query');
assert.ok(message.startsWith('Live mode is currently unavailable'));
});

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'));
Expand Down
Loading