github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/controllers/topology.js (about) 1 /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ 2 import Controller from '@ember/controller'; 3 import { computed, action } from '@ember/object'; 4 import { alias } from '@ember/object/computed'; 5 import { inject as service } from '@ember/service'; 6 import { tracked } from '@glimmer/tracking'; 7 import classic from 'ember-classic-decorator'; 8 import { reduceBytes, reduceHertz } from 'nomad-ui/utils/units'; 9 import { 10 serialize, 11 deserializedQueryParam as selection, 12 } from 'nomad-ui/utils/qp-serialize'; 13 import { scheduleOnce } from '@ember/runloop'; 14 import intersection from 'lodash.intersection'; 15 import Searchable from 'nomad-ui/mixins/searchable'; 16 17 const sumAggregator = (sum, value) => sum + (value || 0); 18 const formatter = new Intl.NumberFormat(window.navigator.locale || 'en', { 19 maximumFractionDigits: 2, 20 }); 21 22 @classic 23 export default class TopologyControllers extends Controller.extend(Searchable) { 24 @service userSettings; 25 26 queryParams = [ 27 { 28 searchTerm: 'search', 29 }, 30 { 31 qpState: 'status', 32 }, 33 { 34 qpVersion: 'version', 35 }, 36 { 37 qpClass: 'class', 38 }, 39 { 40 qpDatacenter: 'dc', 41 }, 42 ]; 43 44 @tracked searchTerm = ''; 45 qpState = ''; 46 qpVersion = ''; 47 qpClass = ''; 48 qpDatacenter = ''; 49 50 setFacetQueryParam(queryParam, selection) { 51 this.set(queryParam, serialize(selection)); 52 } 53 54 @selection('qpState') selectionState; 55 @selection('qpClass') selectionClass; 56 @selection('qpDatacenter') selectionDatacenter; 57 @selection('qpVersion') selectionVersion; 58 59 @computed 60 get optionsState() { 61 return [ 62 { key: 'initializing', label: 'Initializing' }, 63 { key: 'ready', label: 'Ready' }, 64 { key: 'down', label: 'Down' }, 65 { key: 'ineligible', label: 'Ineligible' }, 66 { key: 'draining', label: 'Draining' }, 67 { key: 'disconnected', label: 'Disconnected' }, 68 ]; 69 } 70 71 @computed('model.nodes', 'nodes.[]', 'selectionClass') 72 get optionsClass() { 73 const classes = Array.from(new Set(this.model.nodes.mapBy('nodeClass'))) 74 .compact() 75 .without(''); 76 77 // Remove any invalid node classes from the query param/selection 78 scheduleOnce('actions', () => { 79 // eslint-disable-next-line ember/no-side-effects 80 this.set( 81 'qpClass', 82 serialize(intersection(classes, this.selectionClass)) 83 ); 84 }); 85 86 return classes.sort().map((dc) => ({ key: dc, label: dc })); 87 } 88 89 @computed('model.nodes', 'nodes.[]', 'selectionDatacenter') 90 get optionsDatacenter() { 91 const datacenters = Array.from( 92 new Set(this.model.nodes.mapBy('datacenter')) 93 ).compact(); 94 95 // Remove any invalid datacenters from the query param/selection 96 scheduleOnce('actions', () => { 97 // eslint-disable-next-line ember/no-side-effects 98 this.set( 99 'qpDatacenter', 100 serialize(intersection(datacenters, this.selectionDatacenter)) 101 ); 102 }); 103 104 return datacenters.sort().map((dc) => ({ key: dc, label: dc })); 105 } 106 107 @computed('model.nodes', 'nodes.[]', 'selectionVersion') 108 get optionsVersion() { 109 const versions = Array.from( 110 new Set(this.model.nodes.mapBy('version')) 111 ).compact(); 112 113 // Remove any invalid versions from the query param/selection 114 scheduleOnce('actions', () => { 115 // eslint-disable-next-line ember/no-side-effects 116 this.set( 117 'qpVersion', 118 serialize(intersection(versions, this.selectionVersion)) 119 ); 120 }); 121 122 return versions.sort().map((v) => ({ key: v, label: v })); 123 } 124 125 @alias('userSettings.showTopoVizPollingNotice') showPollingNotice; 126 127 @tracked pre09Nodes = null; 128 129 get filteredNodes() { 130 const { nodes } = this.model; 131 return nodes.filter((node) => { 132 const { 133 searchTerm, 134 selectionState, 135 selectionVersion, 136 selectionDatacenter, 137 selectionClass, 138 } = this; 139 return ( 140 (selectionState.length ? selectionState.includes(node.status) : true) && 141 (selectionVersion.length 142 ? selectionVersion.includes(node.version) 143 : true) && 144 (selectionDatacenter.length 145 ? selectionDatacenter.includes(node.datacenter) 146 : true) && 147 (selectionClass.length 148 ? selectionClass.includes(node.nodeClass) 149 : true) && 150 (node.name.includes(searchTerm) || 151 node.datacenter.includes(searchTerm) || 152 node.nodeClass.includes(searchTerm)) 153 ); 154 }); 155 } 156 157 @computed('model.nodes.@each.datacenter') 158 get datacenters() { 159 return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); 160 } 161 162 @computed('model.allocations.@each.isScheduled') 163 get scheduledAllocations() { 164 return this.model.allocations.filterBy('isScheduled'); 165 } 166 167 @computed('model.nodes.@each.resources') 168 get totalMemory() { 169 const mibs = this.model.nodes 170 .mapBy('resources.memory') 171 .reduce(sumAggregator, 0); 172 return mibs * 1024 * 1024; 173 } 174 175 @computed('model.nodes.@each.resources') 176 get totalCPU() { 177 return this.model.nodes 178 .mapBy('resources.cpu') 179 .reduce((sum, cpu) => sum + (cpu || 0), 0); 180 } 181 182 @computed('totalMemory') 183 get totalMemoryFormatted() { 184 return formatter.format(reduceBytes(this.totalMemory)[0]); 185 } 186 187 @computed('totalMemory') 188 get totalMemoryUnits() { 189 return reduceBytes(this.totalMemory)[1]; 190 } 191 192 @computed('totalCPU') 193 get totalCPUFormatted() { 194 return formatter.format(reduceHertz(this.totalCPU, null, 'MHz')[0]); 195 } 196 197 @computed('totalCPU') 198 get totalCPUUnits() { 199 return reduceHertz(this.totalCPU, null, 'MHz')[1]; 200 } 201 202 @computed('scheduledAllocations.@each.allocatedResources') 203 get totalReservedMemory() { 204 const mibs = this.scheduledAllocations 205 .mapBy('allocatedResources.memory') 206 .reduce(sumAggregator, 0); 207 return mibs * 1024 * 1024; 208 } 209 210 @computed('scheduledAllocations.@each.allocatedResources') 211 get totalReservedCPU() { 212 return this.scheduledAllocations 213 .mapBy('allocatedResources.cpu') 214 .reduce(sumAggregator, 0); 215 } 216 217 @computed('totalMemory', 'totalReservedMemory') 218 get reservedMemoryPercent() { 219 if (!this.totalReservedMemory || !this.totalMemory) return 0; 220 return this.totalReservedMemory / this.totalMemory; 221 } 222 223 @computed('totalCPU', 'totalReservedCPU') 224 get reservedCPUPercent() { 225 if (!this.totalReservedCPU || !this.totalCPU) return 0; 226 return this.totalReservedCPU / this.totalCPU; 227 } 228 229 @computed( 230 'activeAllocation.taskGroupName', 231 'scheduledAllocations.@each.{job,taskGroupName}' 232 ) 233 get siblingAllocations() { 234 if (!this.activeAllocation) return []; 235 const taskGroup = this.activeAllocation.taskGroupName; 236 const jobId = this.activeAllocation.belongsTo('job').id(); 237 238 return this.scheduledAllocations.filter((allocation) => { 239 return ( 240 allocation.taskGroupName === taskGroup && 241 allocation.belongsTo('job').id() === jobId 242 ); 243 }); 244 } 245 246 @computed('activeNode') 247 get nodeUtilization() { 248 const node = this.activeNode; 249 const [formattedMemory, memoryUnits] = reduceBytes( 250 node.memory * 1024 * 1024 251 ); 252 const totalReservedMemory = node.allocations 253 .mapBy('memory') 254 .reduce(sumAggregator, 0); 255 const totalReservedCPU = node.allocations 256 .mapBy('cpu') 257 .reduce(sumAggregator, 0); 258 259 return { 260 totalMemoryFormatted: formattedMemory.toFixed(2), 261 totalMemoryUnits: memoryUnits, 262 263 totalMemory: node.memory * 1024 * 1024, 264 totalReservedMemory: totalReservedMemory * 1024 * 1024, 265 reservedMemoryPercent: totalReservedMemory / node.memory, 266 267 totalCPU: node.cpu, 268 totalReservedCPU, 269 reservedCPUPercent: totalReservedCPU / node.cpu, 270 }; 271 } 272 273 @computed('siblingAllocations.@each.node') 274 get uniqueActiveAllocationNodes() { 275 return this.siblingAllocations.mapBy('node.id').uniq(); 276 } 277 278 @action 279 async setAllocation(allocation) { 280 if (allocation) { 281 await allocation.reload(); 282 await allocation.job.reload(); 283 } 284 this.set('activeAllocation', allocation); 285 } 286 287 @action 288 setNode(node) { 289 this.set('activeNode', node); 290 } 291 292 @action 293 handleTopoVizDataError(errors) { 294 const pre09NodesError = errors.findBy('type', 'filtered-nodes'); 295 if (pre09NodesError) { 296 this.pre09Nodes = pre09NodesError.context; 297 } 298 } 299 }