Skip to content
72 changes: 60 additions & 12 deletions InfoLogger/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
// Import frontend framework
import {
Observable, WebSocketClient, QueryRouter,
Loader, RemoteData, sessionService, Notification,
Loader, RemoteData, sessionService, Notification, iconMediaPlay, iconMediaStop,
} from '/js/src/index.js';
import Log from './log/Log.js';
import Timezone from './common/Timezone.js';
import { callRateLimiter, setBrowserTabTitle } from './common/utils.js';
import { setBrowserTabTitle } from './common/utils.js';
import Table from './table/Table.js';
import { MODE } from './constants/mode.const.js';
import { BUTTON } from './constants/button-states.const.js';

/**
* Main model of InfoLoggerGui, contains sub-models modules
Expand All @@ -33,6 +34,7 @@ export default class Model extends Observable {
constructor() {
super();

this.guiReadyToUse = RemoteData.loading();
this.session = sessionService.get();
this.session.personid = parseInt(this.session.personid, 10); // cast, sessionService has only strings

Expand All @@ -48,6 +50,10 @@ export default class Model extends Observable {
this.timezone = new Timezone();
this.timezone.bubbleTo(this);

this.queryButtonType = BUTTON.PRIMARY;
this.liveButtonType = BUTTON.DEFAULT;
this.liveButtonIcon = iconMediaPlay();

this.notification = new Notification(this);
this.notification.bubbleTo(this);

Expand All @@ -62,6 +68,9 @@ export default class Model extends Observable {
this.router = new QueryRouter();
this.router.observe(this.handleLocationChange.bind(this));
this.router.bubbleTo(this);
this.log.filter.observe(() => {
this.router.go(`?q=${JSON.stringify(this.log.filter.toObject())}`, true, true);
});
this.handleLocationChange(); // Init first page

// Setup keyboard dispatcher
Expand All @@ -72,19 +81,16 @@ export default class Model extends Observable {
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)
this.observe(callRateLimiter(this.updateRouteOnModelChange.bind(this), 500));
}

/**
* Handle websocket authentication success
*/
handleWSAuthed() {
// Tell server not to stream by default
this.guiReadyToUse = RemoteData.success();
this.ws.setFilter(() => false);
this.notify();
}

/**
Expand Down Expand Up @@ -319,7 +325,7 @@ 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');
Expand All @@ -330,17 +336,59 @@ export default class Model extends Observable {
} else if (params.q) {
this.getUserProfile();
this.log.filter.fromObject(JSON.parse(params.q.replaceAll('\n', '\\n')));
if (params.live == 'true') {
await this.loadLiveMode();
}
} else if (!params.q) {
this.getUserProfile();
this.log.filter.resetCriteria();
if (params.live == 'true') {
await this.loadLiveMode();
}
} else {
this.getUserProfile();
}
}

/**
* When model change (filters), update address bar with the filter
* do it silently to avoid infinite loop
* Attempt to load into the live mode of the ILG
*/
async loadLiveMode() {
while (this.guiReadyToUse.isLoading()
|| !this.frameworkInfo.isSuccess()
|| !this.frameworkInfo.payload.infoLoggerServer.status.ok) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
try {
this.log.liveStart();
this.setLiveButton(BUTTON.SUCCESS_ACTIVE, iconMediaStop());
this.setQueryButton(BUTTON.DEFAULT);
this.log.enableAutoScroll();
setBrowserTabTitle(`${window.ILG.name} LIVE`);
this.notify();
} catch (error) {
this.notification.show(error.toString(), 'danger', 3000);
}
}

/**
* Method to change the icon and type of the liveButton
* @param {string} liveType - Type of the Live Button
* @param {Icon} liveIcon - Icon of the Live Button
*/
setLiveButton(liveType, liveIcon) {
this.liveButtonType = liveType;
this.liveButtonIcon = liveIcon;
this.notify();
}

/**
* Method to change the type of the queryButton
* @param {string} queryType - Type of the queryButton
*/
updateRouteOnModelChange() {
this.router.go(`?q=${JSON.stringify(this.log.filter.toObject())}`, true, true);
setQueryButton(queryType) {
this.queryButtonType = queryType;
this.notify();
}

/**
Expand Down
24 changes: 10 additions & 14 deletions InfoLogger/public/log/commandLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import { BUTTON } from '../constants/button-states.const.js';
import { MODE } from '../constants/mode.const.js';
import { setBrowserTabTitle } from '../common/utils.js';

let queryButtonType = BUTTON.PRIMARY;
let liveButtonType = BUTTON.DEFAULT;
let liveButtonIcon = iconMediaPlay();

export default (model) => [
userActionsDropdown(model),
h('div.btn-group.mh3', [
Expand Down Expand Up @@ -111,7 +107,7 @@ const queryButton = (model) => h('button.btn', model.frameworkInfo.match({
title: frameworkInfo.mysql && frameworkInfo.mysql.status.ok
? 'Query database with filters (Enter)' : 'Query service not configured',
disabled: !frameworkInfo.mysql || !frameworkInfo.mysql.status.ok || model.log.queryResult.isLoading(),
className: model.log.queryResult.isLoading() ? 'loading' : queryButtonType,
className: model.log.queryResult.isLoading() ? 'loading' : model.queryButtonType,
style: 'font-weight: bold',
onclick: () => toggleButtonStates(model, false),
}),
Expand Down Expand Up @@ -165,12 +161,12 @@ const liveButton = (model) => h('button.btn', model.frameworkInfo.match({
Success: (frameworkInfo) => ({
title: frameworkInfo.infoLoggerServer.status.ok ? 'Stream logs with filtering' : 'Live service not configured',
disabled: !frameworkInfo.infoLoggerServer.status.ok || model.log.queryResult.isLoading(),
className: !model.ws.authed ? 'loading' : liveButtonType,
className: model.guiReadyToUse.isLoading() ? 'loading' : model.liveButtonType,
style: 'font-weight: bold',
onclick: () => toggleButtonStates(model, true),
}),
Failure: () => ({ disabled: true, className: 'danger' }),
}), 'Live', ' ', liveButtonIcon);
}), 'Live', ' ', model.liveButtonIcon);

/**
* Method to toggle states of the buttons(Query/Live) depending on the mode the tool is running on
Expand All @@ -185,7 +181,7 @@ function toggleButtonStates(model, wasLivePressed) {
case MODE.LIVE.PAUSED:
try {
model.log.liveStart();
setButtonsType(BUTTON.DEFAULT, BUTTON.SUCCESS_ACTIVE, iconMediaStop());
setButtonsType(BUTTON.DEFAULT, BUTTON.SUCCESS_ACTIVE, iconMediaStop(), model);
model.log.enableAutoScroll();
setBrowserTabTitle(`${window.ILG.name} LIVE`);
} catch (error) {
Expand All @@ -195,24 +191,24 @@ function toggleButtonStates(model, wasLivePressed) {
default: // MODE.LIVE.RUNNING
model.log.liveStop(MODE.LIVE.PAUSED);
setBrowserTabTitle(`${window.ILG.name} LIVE PAUSED`);
setButtonsType(BUTTON.DEFAULT, BUTTON.PRIMARY, iconMediaPlay());
setButtonsType(BUTTON.DEFAULT, BUTTON.PRIMARY, iconMediaPlay(), model);
model.log.disableAutoScroll();
}
} else {
model.log.query();
setBrowserTabTitle(`${window.ILG.name} QUERY`);
setButtonsType(BUTTON.PRIMARY, BUTTON.DEFAULT, iconMediaPlay());
setButtonsType(BUTTON.PRIMARY, BUTTON.DEFAULT, iconMediaPlay(), model);
}

/**
* Method to change types of the buttons based on the mode being run
* @param {string} queryType Type of the Query Button
* @param {string} liveType Type of the Live Button
* @param {Icon} liveIcon Icon of the Live Button
* @param {Model} model - Model, stores liveButton type and icon state
*/
function setButtonsType(queryType, liveType, liveIcon) {
queryButtonType = queryType;
liveButtonType = liveType;
liveButtonIcon = liveIcon;
function setButtonsType(queryType, liveType, liveIcon, model) {
model.setQueryButton(queryType);
model.setLiveButton(liveType, liveIcon);
}
}
1 change: 0 additions & 1 deletion InfoLogger/public/logFilter/LogFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export default class LogFilter extends Observable {
super();

this.model = model;

this.resetCriteria();
}

Expand Down
2 changes: 1 addition & 1 deletion InfoLogger/public/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default (model) => [
]),
h('div.flex-grow.flex-row.shadow-level0.logs-container', [
aboutComponent(model),
logsTable(model),
model.guiReadyToUse.isSuccess() ? logsTable(model) : 'waiting for WebSocket connection',
inspectorSide(model),
]),
h('footer.f7.ph1', [statusBar(model)]),
Expand Down
29 changes: 14 additions & 15 deletions InfoLogger/test/mocha-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
*/

/* eslint-disable no-console */
const puppeteer = require('puppeteer');
const assert = require('assert');
const {spawn} = require('child_process');
const { spawn } = require('child_process');

const config = require('./test-config.js');
const {createServer, closeServer} = require('./live-simulator/infoLoggerServer.js');
const { createServer, closeServer } = require('./live-simulator/infoLoggerServer.js');

// APIs:
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md
Expand All @@ -28,42 +28,43 @@ const {createServer, closeServer} = require('./live-simulator/infoLoggerServer.j
// Network and rendering can have delays this can leads to random failures
// if they are tested just after their initialization.

describe('InfoLogger', function() {
describe('InfoLogger', function () {
let browser;
let page;
let subprocess; // web-server runs into a subprocess
let subprocessOutput = '';
let ilgServer;

this.timeout(30000);
this.slow(1000);

const baseUrl = `http://${config.http.hostname}:${config.http.port}/`;

before(async () => {
// Start infologger server simulator
ilgServer = createServer();

// Start web-server in background
subprocess = spawn('node', ['index.js', 'test/test-config.js'], {stdio: 'pipe'});
subprocess = spawn('node', ['index.js', 'test/test-config.js'], { stdio: 'pipe' });
subprocess.stdout.on('data', (chunk) => subprocessOutput += chunk.toString());
subprocess.stderr.on('data', (chunk) => subprocessOutput += chunk.toString());
subprocess.on('error', (error) => console.error(`Server failed due to: ${error}`))
subprocess.on('error', (error) => console.error(`Server failed due to: ${error}`));

// Start browser to test UI
browser = await puppeteer.launch({headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox']});
browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
page = await browser.newPage();

// Export page and configurations for the other mocha files
exports.page = page;
exports.helpers = {baseUrl, jwt: config.jwt};

exports.helpers = { baseUrl, jwt: config.jwt };
});

it('should load first page "/"', async () => {
// try many times until backend server is ready
for (let i = 0; i < 10; i++) {
try {
await page.goto(baseUrl, {waitUntil: 'networkidle0'});
await page.goto(baseUrl, { waitUntil: 'networkidle0' });
break; // connection ok, this test passed
} catch (e) {
if (e.message.includes('net::ERR_CONNECTION_REFUSED')) {
Expand All @@ -75,8 +76,8 @@ describe('InfoLogger', function() {
}
});

it('should have redirected to default page "/?q={"severity":{"in":"I W E F"}}"', async function() {
await page.goto(baseUrl, {waitUntil: 'networkidle0'});
it('should have redirected to default page "/?q={"severity":{"in":"I W E F"}}"', async () => {
await page.goto(baseUrl, { waitUntil: 'networkidle0' });
const location = await page.evaluate(() => window.location);
const search = decodeURIComponent(location.search);

Expand All @@ -97,7 +98,5 @@ describe('InfoLogger', function() {
console.log('---------------------------------------------');
subprocess.kill();
closeServer(ilgServer);

});
});

Loading