|
| 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 | + */ |
1 | 8 | // GH-295: Use server-side logic instead of client-side |
2 | 9 | // NOTE: If JavaScript is a long-term solution, then use a class. |
3 | 10 |
|
| 11 | + |
| 12 | + |
4 | 13 | /* Definitions */ |
5 | 14 |
|
6 | 15 | /** |
7 | 16 | * All system data |
8 | | - * @typedef {array<System>} AllSystems |
| 17 | + * @typedef {array<module:systemMonitor~System>} AllSystems |
9 | 18 | * @see https://frontera-portal.tacc.utexas.edu/api/system-monitor/ |
10 | 19 | */ |
11 | 20 |
|
|
15 | 24 | * @see https://frontera-portal.tacc.utexas.edu/api/system-monitor/ |
16 | 25 | */ |
17 | 26 |
|
18 | | -/* Internal Constants (if using a class, Static Properties) */ |
19 | 27 |
|
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}]'); |
23 | 37 |
|
24 | 38 | /** |
25 | 39 | * The URL of the API endpoint |
26 | 40 | * @type {string} |
27 | 41 | */ |
28 | 42 | const API_URL = '/api/system-monitor'; |
29 | 43 |
|
30 | | -/* External Constants (if using a class, Parameters) */ |
31 | 44 |
|
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 */ |
39 | 47 |
|
40 | 48 | /** |
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 |
45 | 50 | */ |
46 | | -const SYSTEM_DOM_ELEMENT = window.SYSMON_SYSTEM_DOM_ELEMENT; |
| 51 | +export class SystemMonitor { |
47 | 52 |
|
48 | | -/* Functions (if using a class, Methods) */ |
49 | 53 |
|
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 | | -} |
70 | 54 |
|
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(); |
80 | 83 | } |
81 | | - return true; |
82 | | -} |
83 | 84 |
|
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 | | -} |
95 | 85 |
|
96 | | -/** |
97 | | - * Show system content in UI |
98 | | - */ |
99 | | -function showStatus() { |
100 | | - getElement('status').classList.remove('d-none'); |
101 | | -} |
102 | 86 |
|
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(); |
119 | 107 | } |
120 | | -} |
121 | 108 |
|
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 | + } |
134 | 121 |
|
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; |
149 | 157 | } |
150 | 158 | } |
151 | | - showStatus(); |
152 | | -} |
153 | 159 |
|
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 | + } |
166 | 188 | } |
167 | | - }); |
| 189 | + this.showStatus(); |
| 190 | + } |
168 | 191 |
|
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 | + } |
171 | 209 |
|
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 | + } |
189 | 228 | } |
190 | | - } |
191 | | - ); |
| 229 | + ); |
192 | 230 |
|
193 | | - console.log('System Monitor: Load complete'); |
194 | | - }, |
195 | | - false |
196 | | - ); |
| 231 | + console.log('System Monitor: Load complete'); |
| 232 | + }, |
| 233 | + false |
| 234 | + ); |
| 235 | + } |
197 | 236 | } |
198 | | - |
199 | | -init(); |
|
0 commit comments