Skip to content

Commit c96b41e

Browse files
committed
GH-89: Support multiple instances
Use JavaScript module and class. Do not use window variables and functions.
1 parent b4f6bd0 commit c96b41e

File tree

2 files changed

+196
-156
lines changed

2 files changed

+196
-156
lines changed

taccsite_cms/contrib/taccsite_system_monitor/static/taccsite_system_monitor/js/system_monitor.js

Lines changed: 189 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
/**
2+
* Show public runtime data about a TACC system
3+
*
4+
* - Manipulates attributes and text nodes within existing markup.
5+
* - Data is NOT dynamically updated after initial load.
6+
* @module systemMonitor
7+
*/
18
// GH-295: Use server-side logic instead of client-side
29
// NOTE: If JavaScript is a long-term solution, then use a class.
310

11+
12+
413
/* Definitions */
514

615
/**
716
* All system data
8-
* @typedef {array<System>} AllSystems
17+
* @typedef {array<module:systemMonitor~System>} AllSystems
918
* @see https://frontera-portal.tacc.utexas.edu/api/system-monitor/
1019
*/
1120

@@ -15,185 +24,213 @@
1524
* @see https://frontera-portal.tacc.utexas.edu/api/system-monitor/
1625
*/
1726

18-
/* Internal Constants (if using a class, Static Properties) */
1927

20-
// Allow system mointor to work(-ish) on local server
21-
const USE_SAMPLE_DATA = (window.location.hostname === 'localhost');
22-
const API_SAMPLE_DATA = JSON.parse('[{"hostname": "frontera.tacc.utexas.edu", "display_name": "Frontera", "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:02Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:02Z"}, "status_tests": {"ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:02.176Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:02.174Z"}}, "resource_type": "compute", "jobs": {"running": 322, "queued": 1468, "other": 364}, "load_percentage": 99, "cpu_count": 472760, "cpu_used": 468616, "is_operational": true}, {"hostname": "stampede2.tacc.utexas.edu", "display_name": "Stampede2", "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:03Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:03Z"}, "status_tests": {"heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:03.069Z"}, "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:03.074Z"}}, "resource_type": "compute", "jobs": {"running": 1115, "queued": 1032, "other": 444}, "load_percentage": 96, "cpu_count": 1309056, "cpu_used": 1257184, "is_operational": true}]');
28+
29+
/* Constants */
30+
/* IDEA: These could be static properties, once Safari support is widespread */
31+
32+
/**
33+
* Sample system data
34+
* @type {array<module:systemMonitor~System>}
35+
*/
36+
const API_SAMPLE_DATA = JSON.parse('[{"hostname": "frontera.tacc.utexas.edu", "display_name": "Frontera", "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:02Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:02Z"}, "status_tests": {"ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:02.176Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:02.174Z"}}, "resource_type": "compute", "jobs": {"running": 322, "queued": 1468, "other": 364}, "load_percentage": 99, "cpu_count": 472760, "cpu_used": 468616, "is_operational": true}, {"hostname": "stampede2.tacc.utexas.edu", "display_name": "Stampede2", "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:03Z"}, "heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:03Z"}, "status_tests": {"heartbeat": {"type": "heartbeat", "status": true, "timestamp": "2021-07-30T19:45:03.069Z"}, "ssh": {"type": "ssh", "status": true, "timestamp": "2021-07-30T19:45:03.074Z"}}, "resource_type": "compute", "jobs": {"running": 0, "queued": 1032, "other": 444}, "load_percentage": 0, "cpu_count": 1309056, "cpu_used": 1257184, "is_operational": true}]');
2337

2438
/**
2539
* The URL of the API endpoint
2640
* @type {string}
2741
*/
2842
const API_URL = '/api/system-monitor';
2943

30-
/* External Constants (if using a class, Parameters) */
3144

32-
/**
33-
* The systems to show
34-
*
35-
* _Notice: This value is expected to be available from another script_
36-
* @type {string}
37-
*/
38-
const SYSTEM_HOSTNAME = window.SYSMON_SYSTEM_HOSTNAME;
45+
46+
/* Exports */
3947

4048
/**
41-
* The DOM element for display
42-
*
43-
* _Notice: This value is expected to be available from another script_
44-
* @type {HTMLElement}
49+
* Populate a system monitor element
4550
*/
46-
const SYSTEM_DOM_ELEMENT = window.SYSMON_SYSTEM_DOM_ELEMENT;
51+
export class SystemMonitor {
4752

48-
/* Functions (if using a class, Methods) */
4953

50-
/**
51-
* Load system status
52-
* @param {string} path
53-
* @param {function} onSuccess - Callback for success (receives JSON)
54-
* @param {function} onError - Callback for success (receives XMLHttpRequest)
55-
*/
56-
function loadStatus(path, onSuccess, onError) {
57-
var xhr = new XMLHttpRequest();
58-
xhr.onreadystatechange = function () {
59-
if (xhr.readyState === XMLHttpRequest.DONE) {
60-
if (xhr.status === 200) {
61-
if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
62-
} else {
63-
if (onError) onError(xhr);
64-
}
65-
}
66-
};
67-
xhr.open('GET', path, true);
68-
xhr.send();
69-
}
7054

71-
/**
72-
* Whether system is operational
73-
* @param {System} system
74-
* @return {boolean}
75-
*/
76-
function isOperational(system) {
77-
if (system['load_percentage'] < 1 || system['load_percentage'] > 99) {
78-
system['load_percentage'] = 0;
79-
return system['jobs']['running'] > 1;
55+
/**
56+
* Initialize system monitor
57+
* @param {string} hostname - The systems to show
58+
* @param {HTMLElement} domElement - The DOM element for display
59+
* @param {HTMLElement} [shouldUseSampleData=false] - Whether to present fake data
60+
*/
61+
constructor(hostname, domElement, shouldUseSampleData=false) {
62+
/**
63+
* The systems to show
64+
* @type {string}
65+
*/
66+
this.hostname = hostname;
67+
68+
/**
69+
* The DOM element for display
70+
* @type {HTMLElement}
71+
*/
72+
this.domElement = domElement;
73+
74+
/**
75+
* Whether to present fake data on a local server
76+
* @type {boolean}
77+
*/
78+
this.shouldUseSampleData = shouldUseSampleData;
79+
80+
81+
82+
this.init();
8083
}
81-
return true;
82-
}
8384

84-
/**
85-
* Get element in UI by an ID
86-
* @param {string} id - The (internally unique) identifier of an element
87-
* @return {HTMLElement}
88-
*/
89-
function getElement(id) {
90-
// NOTE: To permit multiple instances,
91-
// an ID must be reusable across instances,
92-
// so `document.getElementById` SHOULD NOT be used.
93-
return SYSTEM_DOM_ELEMENT.querySelector(`[data-id="${id}"]`);
94-
}
9585

96-
/**
97-
* Show system content in UI
98-
*/
99-
function showStatus() {
100-
getElement('status').classList.remove('d-none');
101-
}
10286

103-
/**
104-
* Style system status
105-
* @param {string} type - A type: "warning"
106-
*/
107-
function setStatusStyle(type) {
108-
const element = getElement('status');
109-
110-
switch (type) {
111-
case 'warning':
112-
element.classList.remove('badge-success');
113-
element.removeAttribute('data-icon');
114-
element.innerHTML = 'Maintenance';
115-
element.classList.add('badge-warning');
116-
117-
default:
118-
break;
87+
/**
88+
* Load system status
89+
* @param {string} path
90+
* @param {function} onSuccess - Callback for success (receives JSON)
91+
* @param {function} onError - Callback for success (receives XMLHttpRequest)
92+
*/
93+
loadStatus(path, onSuccess, onError) {
94+
const xhr = new XMLHttpRequest();
95+
96+
xhr.onreadystatechange = () => {
97+
if (xhr.readyState === XMLHttpRequest.DONE) {
98+
if (xhr.status === 200) {
99+
if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
100+
} else {
101+
if (onError) onError(xhr);
102+
}
103+
}
104+
};
105+
xhr.open('GET', path, true);
106+
xhr.send();
119107
}
120-
}
121108

122-
/**
123-
* Populate system status content in markup
124-
* @param {System} status
125-
*/
126-
function setStatusMarkup(status) {
127-
getElement('load_percentage').innerHTML =
128-
status['load_percentage'] + '%';
129-
getElement('jobs_running').innerHTML =
130-
status['jobs']['running'];
131-
getElement('jobs_queued').innerHTML =
132-
status['jobs']['queued'];
133-
}
109+
/**
110+
* Whether system is operational
111+
* @param {module:systemMonitor~System} system
112+
* @return {boolean}
113+
*/
114+
isOperational(system) {
115+
if (system['load_percentage'] < 1 || system['load_percentage'] > 99) {
116+
system['load_percentage'] = 0;
117+
return system['jobs']['running'] > 1;
118+
}
119+
return true;
120+
}
134121

135-
/**
136-
* Populate system status in UI
137-
* @param {System} status
138-
*/
139-
function setStatus(status) {
140-
const isFound = status;
141-
const isWorking = isOperational(status);
142-
143-
if (isFound && isWorking) {
144-
setStatusMarkup(status);
145-
} else {
146-
setStatusStyle('warning');
147-
if (isFound) {
148-
setStatusMarkup(status);
122+
/**
123+
* Get element in UI by an ID
124+
* @param {string} id - The (internally unique) identifier of an element
125+
* @return {HTMLElement}
126+
*/
127+
getElement(id) {
128+
// NOTE: To permit multiple instances,
129+
// an ID must be reusable across instances,
130+
// so `document.getElementById` SHOULD NOT be used.
131+
return this.domElement.querySelector(`[data-id="${id}"]`);
132+
}
133+
134+
/**
135+
* Show system content in UI
136+
*/
137+
showStatus() {
138+
this.getElement('status').classList.remove('d-none');
139+
}
140+
141+
/**
142+
* Style system status
143+
* @param {string} type - A type: "warning"
144+
*/
145+
setStatusStyle(type) {
146+
const element = this.getElement('status');
147+
148+
switch (type) {
149+
case 'warning':
150+
element.classList.remove('badge-success');
151+
element.removeAttribute('data-icon');
152+
element.innerHTML = 'Maintenance';
153+
element.classList.add('badge-warning');
154+
155+
default:
156+
break;
149157
}
150158
}
151-
showStatus();
152-
}
153159

154-
/**
155-
* Populate monitor based on data
156-
* @param {AllSystems} systems
157-
*/
158-
function populate(systems) {
159-
let status;
160-
161-
systems.forEach(function (system) {
162-
if (system['hostname'] === SYSTEM_HOSTNAME) {
163-
status = system;
164-
console.info(`System Monitor: System found (${SYSTEM_HOSTNAME})`);
165-
return false;
160+
/**
161+
* Populate system status content in markup
162+
* @param {module:systemMonitor~System} status
163+
*/
164+
setStatusMarkup(status) {
165+
this.getElement('load_percentage').innerHTML =
166+
status['load_percentage'] + '%';
167+
this.getElement('jobs_running').innerHTML =
168+
status['jobs']['running'];
169+
this.getElement('jobs_queued').innerHTML =
170+
status['jobs']['queued'];
171+
}
172+
173+
/**
174+
* Populate system status in UI
175+
* @param {module:systemMonitor~System} status
176+
*/
177+
setStatus(status) {
178+
const isFound = status;
179+
const isWorking = this.isOperational(status);
180+
181+
if (isFound && isWorking) {
182+
this.setStatusMarkup(status);
183+
} else {
184+
this.setStatusStyle('warning');
185+
if (isFound) {
186+
this.setStatusMarkup(status);
187+
}
166188
}
167-
});
189+
this.showStatus();
190+
}
168191

169-
setStatus(status);
170-
}
192+
/**
193+
* Populate monitor based on data
194+
* @param {module:systemMonitor~AllSystems} systems
195+
*/
196+
populate(systems) {
197+
let status;
198+
199+
systems.forEach((system) => {
200+
if (system['hostname'] === this.hostname) {
201+
status = system;
202+
console.info(`System Monitor: System found (${this.hostname})`);
203+
return false;
204+
}
205+
});
206+
207+
this.setStatus(status);
208+
}
171209

172-
/* Initialize (if using a class, Constructor) */
173-
174-
/** Load and populate UI */
175-
function init() {
176-
document.addEventListener(
177-
'DOMContentLoaded',
178-
function () {
179-
loadStatus(
180-
API_URL,
181-
function (data) {
182-
populate(data);
183-
},
184-
function (xhr) {
185-
if (USE_SAMPLE_DATA) {
186-
populate(API_SAMPLE_DATA);
187-
} else {
188-
console.error(xhr);
210+
/* Initialize (if using a class, Constructor) */
211+
212+
/** Load and populate UI */
213+
init() {
214+
document.addEventListener(
215+
'DOMContentLoaded',
216+
() => {
217+
this.loadStatus(
218+
API_URL,
219+
(data) => {
220+
this.populate(data);
221+
},
222+
(xhr) => {
223+
if (this.shouldUseSampleData) {
224+
this.populate(API_SAMPLE_DATA);
225+
} else {
226+
console.error(xhr);
227+
}
189228
}
190-
}
191-
);
229+
);
192230

193-
console.log('System Monitor: Load complete');
194-
},
195-
false
196-
);
231+
console.log('System Monitor: Load complete');
232+
},
233+
false
234+
);
235+
}
197236
}
198-
199-
init();

0 commit comments

Comments
 (0)