/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import Controller from '@ember/controller'; import { computed, action } from '@ember/object'; import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import classic from 'ember-classic-decorator'; import { reduceBytes, reduceHertz } from 'nomad-ui/utils/units'; import { serialize, deserializedQueryParam as selection, } from 'nomad-ui/utils/qp-serialize'; import { scheduleOnce } from '@ember/runloop'; import intersection from 'lodash.intersection'; import Searchable from 'nomad-ui/mixins/searchable'; const sumAggregator = (sum, value) => sum + (value || 0); const formatter = new Intl.NumberFormat(window.navigator.locale || 'en', { maximumFractionDigits: 2, }); @classic export default class TopologyControllers extends Controller.extend(Searchable) { @service userSettings; queryParams = [ { searchTerm: 'search', }, { qpState: 'status', }, { qpVersion: 'version', }, { qpClass: 'class', }, { qpDatacenter: 'dc', }, { qpNodePool: 'nodePool', }, ]; @tracked searchTerm = ''; qpState = ''; qpVersion = ''; qpClass = ''; qpDatacenter = ''; qpNodePool = ''; setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); } @selection('qpState') selectionState; @selection('qpClass') selectionClass; @selection('qpDatacenter') selectionDatacenter; @selection('qpNodePool') selectionNodePool; @selection('qpVersion') selectionVersion; @computed get optionsState() { return [ { key: 'initializing', label: 'Initializing' }, { key: 'ready', label: 'Ready' }, { key: 'down', label: 'Down' }, { key: 'ineligible', label: 'Ineligible' }, { key: 'draining', label: 'Draining' }, { key: 'disconnected', label: 'Disconnected' }, ]; } @computed('model.nodes', 'nodes.[]', 'selectionClass') get optionsClass() { const classes = Array.from(new Set(this.model.nodes.mapBy('nodeClass'))) .compact() .without(''); // Remove any invalid node classes from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpClass', serialize(intersection(classes, this.selectionClass)) ); }); return classes.sort().map((dc) => ({ key: dc, label: dc })); } @computed('model.nodes', 'nodes.[]', 'selectionDatacenter') get optionsDatacenter() { const datacenters = Array.from( new Set(this.model.nodes.mapBy('datacenter')) ).compact(); // Remove any invalid datacenters from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpDatacenter', serialize(intersection(datacenters, this.selectionDatacenter)) ); }); return datacenters.sort().map((dc) => ({ key: dc, label: dc })); } @computed('model.nodePools.[]', 'selectionNodePool') get optionsNodePool() { const availableNodePools = this.model.nodePools; scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpNodePool', serialize( intersection( availableNodePools.map(({ name }) => name), this.selectionNodePool ) ) ); }); return availableNodePools.sort().map((nodePool) => ({ key: nodePool.name, label: nodePool.name, })); } @computed('model.nodes', 'nodes.[]', 'selectionVersion') get optionsVersion() { const versions = Array.from( new Set(this.model.nodes.mapBy('version')) ).compact(); // Remove any invalid versions from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpVersion', serialize(intersection(versions, this.selectionVersion)) ); }); return versions.sort().map((v) => ({ key: v, label: v })); } @alias('userSettings.showTopoVizPollingNotice') showPollingNotice; @tracked pre09Nodes = null; get filteredNodes() { const { nodes } = this.model; return nodes.filter((node) => { const { searchTerm, selectionState, selectionVersion, selectionDatacenter, selectionClass, selectionNodePool, } = this; return ( (selectionState.length ? selectionState.includes(node.status) : true) && (selectionVersion.length ? selectionVersion.includes(node.version) : true) && (selectionDatacenter.length ? selectionDatacenter.includes(node.datacenter) : true) && (selectionClass.length ? selectionClass.includes(node.nodeClass) : true) && (selectionNodePool.length ? selectionNodePool.includes(node.nodePool) : true) && (node.name.includes(searchTerm) || node.datacenter.includes(searchTerm) || node.nodeClass.includes(searchTerm)) ); }); } @computed('model.nodes.@each.datacenter') get datacenters() { return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); } @computed('model.allocations.@each.isScheduled') get scheduledAllocations() { return this.model.allocations.filterBy('isScheduled'); } @computed('model.nodes.@each.resources') get totalMemory() { const mibs = this.model.nodes .mapBy('resources.memory') .reduce(sumAggregator, 0); return mibs * 1024 * 1024; } @computed('model.nodes.@each.resources') get totalCPU() { return this.model.nodes .mapBy('resources.cpu') .reduce((sum, cpu) => sum + (cpu || 0), 0); } @computed('totalMemory') get totalMemoryFormatted() { return formatter.format(reduceBytes(this.totalMemory)[0]); } @computed('totalMemory') get totalMemoryUnits() { return reduceBytes(this.totalMemory)[1]; } @computed('totalCPU') get totalCPUFormatted() { return formatter.format(reduceHertz(this.totalCPU, null, 'MHz')[0]); } @computed('totalCPU') get totalCPUUnits() { return reduceHertz(this.totalCPU, null, 'MHz')[1]; } @computed('scheduledAllocations.@each.allocatedResources') get totalReservedMemory() { const mibs = this.scheduledAllocations .mapBy('allocatedResources.memory') .reduce(sumAggregator, 0); return mibs * 1024 * 1024; } @computed('scheduledAllocations.@each.allocatedResources') get totalReservedCPU() { return this.scheduledAllocations .mapBy('allocatedResources.cpu') .reduce(sumAggregator, 0); } @computed('totalMemory', 'totalReservedMemory') get reservedMemoryPercent() { if (!this.totalReservedMemory || !this.totalMemory) return 0; return this.totalReservedMemory / this.totalMemory; } @computed('totalCPU', 'totalReservedCPU') get reservedCPUPercent() { if (!this.totalReservedCPU || !this.totalCPU) return 0; return this.totalReservedCPU / this.totalCPU; } @computed( 'activeAllocation.taskGroupName', 'scheduledAllocations.@each.{job,taskGroupName}' ) get siblingAllocations() { if (!this.activeAllocation) return []; const taskGroup = this.activeAllocation.taskGroupName; const jobId = this.activeAllocation.belongsTo('job').id(); return this.scheduledAllocations.filter((allocation) => { return ( allocation.taskGroupName === taskGroup && allocation.belongsTo('job').id() === jobId ); }); } @computed('activeNode') get nodeUtilization() { const node = this.activeNode; const [formattedMemory, memoryUnits] = reduceBytes( node.memory * 1024 * 1024 ); const totalReservedMemory = node.allocations .mapBy('memory') .reduce(sumAggregator, 0); const totalReservedCPU = node.allocations .mapBy('cpu') .reduce(sumAggregator, 0); return { totalMemoryFormatted: formattedMemory.toFixed(2), totalMemoryUnits: memoryUnits, totalMemory: node.memory * 1024 * 1024, totalReservedMemory: totalReservedMemory * 1024 * 1024, reservedMemoryPercent: totalReservedMemory / node.memory, totalCPU: node.cpu, totalReservedCPU, reservedCPUPercent: totalReservedCPU / node.cpu, }; } @computed('siblingAllocations.@each.node') get uniqueActiveAllocationNodes() { return this.siblingAllocations.mapBy('node.id').uniq(); } @action async setAllocation(allocation) { if (allocation) { await allocation.reload(); await allocation.job.reload(); } this.set('activeAllocation', allocation); } @action setNode(node) { this.set('activeNode', node); } @action handleTopoVizDataError(errors) { const pre09NodesError = errors.findBy('type', 'filtered-nodes'); if (pre09NodesError) { this.pre09Nodes = pre09NodesError.context; } } }