Skip to content
Merged
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
84 changes: 66 additions & 18 deletions InfoLogger/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,25 @@ export default class Model extends Observable {

this.frameworkInfoEnabled = false;
this.frameworkInfo = RemoteData.notAsked();
this.getFrameworkInfo();

/** @type {Promise<void>} resolves once the framework info has loaded (success or failure) */
this.frameworkInfoLoaded = this.getFrameworkInfo();

this.inspectorEnabled = false;
this.accountMenuEnabled = false;

// Setup WS connection before the router as parsing live=true awaits wsSettled
this.ws = new WebSocketClient();
this.ws.addListener('command', this.handleWSCommand.bind(this));
this.ws.addListener('authed', this.handleWSAuthed.bind(this));

/** @type {Promise<void>} resolves once the websocket is authenticated or closed */
this.wsSettled = new Promise((resolve) => {
this.ws.addListener('authed', resolve);
this.ws.addListener('close', resolve);
});
this.ws.addListener('close', this.handleWSClose.bind(this));

// Setup router
this.router = new QueryRouter();
this.router.observe(this.handleLocationChange.bind(this));
Expand All @@ -73,12 +87,6 @@ export default class Model extends Observable {
window.addEventListener('keydown', this.handleKeyboardDown.bind(this));
window.addEventListener('wheel', this.handleWheel.bind(this), { passive: false });

// Setup WS connection
this.ws = new WebSocketClient();
this.ws.addListener('command', this.handleWSCommand.bind(this));
this.ws.addListener('authed', this.handleWSAuthed.bind(this));
this.ws.addListener('close', this.handleWSClose.bind(this));

// update router on model change
// Model can change very often we protect router with callRateLimiter
// Router limit: 100 calls per 30 seconds max = 30ms, 2 FPS is enough (500ms)
Expand Down Expand Up @@ -344,37 +352,77 @@ export default class Model extends Observable {
/**
* Delegates sub-model actions depending new location of the page
*/
handleLocationChange() {
async handleLocationChange() {
const { params } = this.router;
if (params) {
this.parseLocation(params);
try {
await this.parseLocation(params);
} catch (error) {
this.notification.show(`Unable to load page from URL: ${error.message}`, 'danger');
}
}
}

/**
* 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;
}
} else {
this.getUserProfile();
}

// Profile loading shows its own notification; wait so it doesn't overwrite the filter error
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') {
await this.startLiveModeFromURL();
}
}

/**
* Start live mode from URL once dependencies are ready
*/
async startLiveModeFromURL() {
await Promise.all([
this.frameworkInfoLoaded,
this.wsSettled,
]);

if (!this.ws.authed) {
Comment thread
graduta marked this conversation as resolved.
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();
}

/**
Expand Down
72 changes: 71 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,68 @@ 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' });
await page.waitForFunction(
() => model.notification.message.startsWith('Invalid URL filter format'),
{ timeout: 5000 },
);

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('startLiveModeFromURL 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