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