-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathModel.js
More file actions
429 lines (394 loc) · 13.6 KB
/
Copy pathModel.js
File metadata and controls
429 lines (394 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* 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.
*/
// Import frontend framework
import {
Observable, WebSocketClient, QueryRouter,
Loader, RemoteData, sessionService, Notification,
} from '/js/src/index.js';
import { callRateLimiter, setBrowserTabTitle } from './common/utils.js';
import { ConfigurationService } from './services/ConfigurationService.js';
import { MODE } from './constants/mode.const.js';
import Log from './log/Log.js';
import Zoom from './log/Zoom.js';
import Table from './table/Table.js';
import Timezone from './common/Timezone.js';
/**
* Main model of InfoLoggerGui, contains sub-models modules
*/
export default class Model extends Observable {
/**
* Instantiate main model containing other models and native events
*/
constructor() {
super();
this.session = sessionService.get();
this.session.personid = parseInt(this.session.personid, 10); // cast, sessionService has only strings
this.loader = new Loader(this);
this.loader.bubbleTo(this);
this.configurationService = new ConfigurationService(this);
this.configurationService.bubbleTo(this);
this.configurationService.load();
this.log = new Log(this);
this.log.bubbleTo(this);
this.table = new Table(this);
this.table.bubbleTo(this);
this.timezone = new Timezone();
this.timezone.bubbleTo(this);
this.notification = new Notification(this);
this.notification.bubbleTo(this);
this.frameworkInfoEnabled = false;
this.frameworkInfo = RemoteData.notAsked();
this.getFrameworkInfo();
this.inspectorEnabled = false;
this.accountMenuEnabled = false;
// Setup router
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));
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)
this.observe(callRateLimiter(this.updateRouteOnModelChange.bind(this), 500));
this.zoom = new Zoom();
this.zoom.bubbleTo(this);
}
/**
* Handle websocket authentication success
*/
handleWSAuthed() {
// Tell server not to stream by default
this.ws.setFilter(() => false);
}
/**
* Handle websocket close event
*/
handleWSClose() {
this.notification.show('Connection to server has been lost, please reload the page.', 'danger', Infinity);
}
/**
* Request data about the framework
*/
async getFrameworkInfo() {
this.frameworkInfo = RemoteData.loading();
this.notify();
const { result, ok } = await this.loader.get('/api/getFrameworkInfo');
if (!ok) {
this.frameworkInfo = RemoteData.failure(result.message);
} else {
this.frameworkInfo = RemoteData.success(result);
if (result['infoLogger-gui'].name && result['infoLogger-gui'].name.trim()) {
window.ILG = {
name: `ILG - ${result['infoLogger-gui'].name}`,
};
setBrowserTabTitle(window.ILG.name);
}
}
this.notify();
return;
}
/**
* Request data about the user's profile
*/
async getUserProfile() {
if (this.session.personid !== 0) {
this.userProfile = RemoteData.loading();
this.notify();
const { result, ok } = await this.loader.get(`/api/getUserProfile?user=${this.session.personid}`);
if (!ok) {
this.userProfile = RemoteData.failure(result.message);
this.notification.show('Unable to load your profile. Default profile will be used instead', 'danger', 2000);
} else {
this.userProfile = RemoteData.success(result);
if (this.userProfile.payload.content.colsHeader) {
this.table.colsHeader = this.userProfile.payload.content.colsHeader;
this.notification.show('Your profile was loaded successfully', 'success', 2000);
}
}
this.notify();
}
return;
}
/**
* Request to save the current configuration of the user
*/
async saveUserProfile() {
const body = {
user: this.session.personid,
content: { colsHeader: this.table.colsHeader },
};
const { result, ok } = await this.loader.post('/api/saveUserProfile', body);
if (!ok) {
this.notification.show('Profile could not be saved', 'danger', 2000);
} else {
this.notification.show(result.message, 'success', 2000);
}
this.accountMenuEnabled = false;
this.notify();
return;
}
/**
* Request data about the profile passed in the URL and set column headers and criteria
* @param {string} profile - profile to load
*/
async getProfile(profile) {
this.userProfile = RemoteData.loading();
this.notify();
const { result, ok } = await this.loader.get(`/api/getProfile?profile=${profile}`);
if (!ok) {
this.userProfile = RemoteData.failure(result.message);
this.notification.show('Unable to load profile. Default profile will be used instead', 'danger', 2000);
} else {
this.userProfile = RemoteData.success(result);
if (this.userProfile.payload.content.colsHeader) {
this.table.colsHeader = this.userProfile.payload.content.colsHeader;
}
if (this.userProfile.payload.content.criterias) {
this.log.filter.fromObject(this.userProfile.payload.content.criterias);
}
if (result.user === profile) {
this.notification.show(`The profile ${profile.toUpperCase()} was loaded successfully`, 'success', 2000);
} else {
this.notification.show(
`Cannot find profile ${profile.toUpperCase()}, default profile used instead`,
'warning',
4000,
);
}
}
this.notify();
return;
}
/**
* Handles wheel events for zoom control
* @param {WheelEvent} e - wheel event
*/
handleWheel(e) {
// Only trigger zoom if Ctrl (or Cmd on Mac) is pressed
// Windows intercepts the Windows key events, so these do not reach the browser
if (!e.ctrlKey && !e.metaKey) {
return;
}
e.preventDefault();
const now = Date.now();
// throttle zoom to avoid too many events on fast scroll, especially on trackpads
if (now - this.zoom.lastScrollTime < 50) {
return;
}
this.zoom.lastScrollTime = now;
e.deltaY < 0 ? this.zoom.zoomIn() : this.zoom.zoomOut();
}
/**
* Delegates sub-model actions depending on incoming keyboard event
* @param {Event} e - keyboard event
*/
handleKeyboardDown(e) {
// Zoom shortcuts regardless of focus
if (e.ctrlKey || e.metaKey) {
if (e.key === '=' || e.key === '+') {
e.preventDefault();
this.zoom.zoomIn();
return;
} else if (e.key === '-') {
e.preventDefault();
this.zoom.zoomOut();
return;
}
}
const code = e.keyCode;
// Enter
if ((code === 13 && !this.messageFocused || code === 13 && e.metaKey) && !this.log.isLiveModeEnabled()) {
this.log.query();
this.log.contextMenu.hide();
}
if (!this.messageFocused) {
// don't listen to keys when it comes from an input (they transform into letters)
// except spacial ones which are not chars
// http://www.foreui.com/articles/Key_Code_Table.htm
if (e.target.tagName.toLowerCase() === 'input') {
return;
}
// shortcuts
switch (e.keyCode) {
case 27: // escape
this.log.removeLogDownloadContent();
this.accountMenuEnabled = false;
this.log.contextMenu.hide();
break;
case 37: // left
if (e.altKey) {
this.log.firstError();
} else {
this.log.previousError();
}
break;
case 39: // right
if (e.altKey) {
this.log.lastError();
} else {
this.log.nextError();
}
break;
case 38: // top
e.preventDefault(); // avoid scroll
this.log.previousItem();
break;
case 40: // bottom
if (e.altKey) {
this.log.goToLastItem();
} else {
this.log.nextItem();
}
e.preventDefault(); // avoid scroll
break;
case 67:
if ((e.metaKey || e.ctrlKey) && window.getSelection().toString() === '' && this.isSecureContext()) {
navigator.clipboard.writeText(this.log.displayedItemFieldsToString());
this.notification.show('Message has been successfully copied to clipboard', 'success', 1500);
}
break;
}
}
}
/**
* Delegates sub-model actions depending on incoming command from server
* @param {WebSocketMessage} message - {command, payload}
*/
async handleWSCommand(message) {
if (message.command === 'live-log') {
this.log.addLog(message.payload);
} else {
if (message.command === 'il-server-connection-issue'
&& this.log.activeMode !== MODE.QUERY) {
if (this.frameworkInfo.isSuccess()) {
this.frameworkInfo.payload.infoLoggerServer.status =
{ ok: false, message: 'Live Mode is currently unavailable. Retrying...' };
}
this.notification.show(
'Connection to InfoLogger server is unavailable. Retrying in 5 seconds',
'warning',
2000,
);
} else if (message.command === 'il-server-close') {
if (this.frameworkInfo.isSuccess()) {
this.frameworkInfo.payload.infoLoggerServer.status =
{ ok: false, message: 'Live Mode is currently unavailable. Retrying...' };
}
this.notification.show('Connection between backend and InfoLogger server has been lost', 'warning', 2000);
} else if (message.command === 'il-server-connected') {
if (this.frameworkInfo.isSuccess()) {
this.frameworkInfo.payload.infoLoggerServer.status = { ok: true };
}
this.notification.show(
'Connection between backend and InfoLogger server has been established',
'success',
2000,
);
} else if (message.command === 'il-sql-server-status') {
if (this.frameworkInfo.isSuccess()) {
this.frameworkInfo.payload.mysql.status = message.payload;
}
this.notify();
if (!message.payload.ok && this.log.activeMode === MODE.QUERY) {
this.notification.show('SQL QUERY System is unavailable. Retrying in 5 seconds', 'warning', 2000);
} else if (message.payload.ok && this.log.activeMode === MODE.QUERY) {
this.notification.show('Connection to SQL QUERY System has been restored', 'success', 2000);
}
}
}
return;
}
/**
* Delegates sub-model actions depending new location of the page
*/
handleLocationChange() {
const { params } = this.router;
if (params) {
this.parseLocation(params);
}
}
/**
* Delegates sub-model actions depending if location is filters or profile
* @param {object} params - URL parameters
*/
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();
try {
this.log.filter.fromObject(JSON.parse(params.q.replaceAll('\n', '\\n')));
} catch (error) {
this.log.filter.resetCriteria();
this.updateRouteOnModelChange();
this.notification.show(`Invalid URL filter format: ${error.message}`, 'danger');
}
} else {
this.getUserProfile();
}
}
/**
* When model change (filters), update address bar with the filter
* do it silently to avoid infinite loop
*/
updateRouteOnModelChange() {
this.router.go(`?q=${JSON.stringify(this.log.filter.toObject())}`, true, true);
}
/**
* Toggle inspector on the right
*/
toggleInspector() {
this.inspectorEnabled = !this.inspectorEnabled;
this.notify();
}
/**
* Toggle framework info on the left
*/
async toggleFrameworkInfo() {
if (!this.frameworkInfoEnabled) {
await this.getFrameworkInfo();
}
this.frameworkInfoEnabled = !this.frameworkInfoEnabled;
this.accountMenuEnabled = false;
this.notify();
}
/**
* Toggle account menu dropdown
*/
toggleAccountMenu() {
this.accountMenuEnabled = !this.accountMenuEnabled;
this.notify();
}
/**
* Method to check if connection is secure to enable certain improvements
* e.g navigator.clipboard, notifications, service workers
* @returns {boolean} - true if connection is secure
*/
isSecureContext() {
return window.isSecureContext;
}
}