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  }