github.com/hernad/nomad@v1.6.112/ui/app/controllers/jobs/index.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 { inject as service } from '@ember/service';
     8  import { alias, readOnly } from '@ember/object/computed';
     9  import Controller from '@ember/controller';
    10  import { computed, action } from '@ember/object';
    11  import { scheduleOnce } from '@ember/runloop';
    12  import intersection from 'lodash.intersection';
    13  import Sortable from 'nomad-ui/mixins/sortable';
    14  import Searchable from 'nomad-ui/mixins/searchable';
    15  import {
    16    serialize,
    17    deserializedQueryParam as selection,
    18  } from 'nomad-ui/utils/qp-serialize';
    19  import classic from 'ember-classic-decorator';
    20  
    21  @classic
    22  export default class IndexController extends Controller.extend(
    23    Sortable,
    24    Searchable
    25  ) {
    26    @service system;
    27    @service userSettings;
    28    @service router;
    29  
    30    isForbidden = false;
    31  
    32    queryParams = [
    33      {
    34        currentPage: 'page',
    35      },
    36      {
    37        searchTerm: 'search',
    38      },
    39      {
    40        sortProperty: 'sort',
    41      },
    42      {
    43        sortDescending: 'desc',
    44      },
    45      {
    46        qpType: 'type',
    47      },
    48      {
    49        qpStatus: 'status',
    50      },
    51      {
    52        qpDatacenter: 'dc',
    53      },
    54      {
    55        qpPrefix: 'prefix',
    56      },
    57      {
    58        qpNamespace: 'namespace',
    59      },
    60      {
    61        qpNodePool: 'nodePool',
    62      },
    63    ];
    64  
    65    currentPage = 1;
    66    @readOnly('userSettings.pageSize') pageSize;
    67  
    68    sortProperty = 'modifyIndex';
    69    sortDescending = true;
    70  
    71    @computed
    72    get searchProps() {
    73      return ['id', 'name'];
    74    }
    75  
    76    @computed
    77    get fuzzySearchProps() {
    78      return ['name'];
    79    }
    80  
    81    fuzzySearchEnabled = true;
    82  
    83    qpType = '';
    84    qpStatus = '';
    85    qpDatacenter = '';
    86    qpPrefix = '';
    87    qpNodePool = '';
    88  
    89    @selection('qpType') selectionType;
    90    @selection('qpStatus') selectionStatus;
    91    @selection('qpDatacenter') selectionDatacenter;
    92    @selection('qpPrefix') selectionPrefix;
    93    @selection('qpNodePool') selectionNodePool;
    94  
    95    @computed
    96    get optionsType() {
    97      return [
    98        { key: 'batch', label: 'Batch' },
    99        { key: 'pack', label: 'Pack' },
   100        { key: 'parameterized', label: 'Parameterized' },
   101        { key: 'periodic', label: 'Periodic' },
   102        { key: 'service', label: 'Service' },
   103        { key: 'system', label: 'System' },
   104        { key: 'sysbatch', label: 'System Batch' },
   105      ];
   106    }
   107  
   108    @computed
   109    get optionsStatus() {
   110      return [
   111        { key: 'pending', label: 'Pending' },
   112        { key: 'running', label: 'Running' },
   113        { key: 'dead', label: 'Dead' },
   114      ];
   115    }
   116  
   117    @computed('selectionDatacenter', 'visibleJobs.[]')
   118    get optionsDatacenter() {
   119      const flatten = (acc, val) => acc.concat(val);
   120      const allDatacenters = new Set(
   121        this.visibleJobs.mapBy('datacenters').reduce(flatten, [])
   122      );
   123  
   124      // Remove any invalid datacenters from the query param/selection
   125      const availableDatacenters = Array.from(allDatacenters).compact();
   126      scheduleOnce('actions', () => {
   127        // eslint-disable-next-line ember/no-side-effects
   128        this.set(
   129          'qpDatacenter',
   130          serialize(intersection(availableDatacenters, this.selectionDatacenter))
   131        );
   132      });
   133  
   134      return availableDatacenters.sort().map((dc) => ({ key: dc, label: dc }));
   135    }
   136  
   137    @computed('selectionPrefix', 'visibleJobs.[]')
   138    get optionsPrefix() {
   139      // A prefix is defined as the start of a job name up to the first - or .
   140      // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds
   141      const hasPrefix = /.[-._]/;
   142  
   143      // Collect and count all the prefixes
   144      const allNames = this.visibleJobs.mapBy('name');
   145      const nameHistogram = allNames.reduce((hist, name) => {
   146        if (hasPrefix.test(name)) {
   147          const prefix = name.match(/(.+?)[-._]/)[1];
   148          hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1;
   149        }
   150        return hist;
   151      }, {});
   152  
   153      // Convert to an array
   154      const nameTable = Object.keys(nameHistogram).map((key) => ({
   155        prefix: key,
   156        count: nameHistogram[key],
   157      }));
   158  
   159      // Only consider prefixes that match more than one name
   160      const prefixes = nameTable.filter((name) => name.count > 1);
   161  
   162      // Remove any invalid prefixes from the query param/selection
   163      const availablePrefixes = prefixes.mapBy('prefix');
   164      scheduleOnce('actions', () => {
   165        // eslint-disable-next-line ember/no-side-effects
   166        this.set(
   167          'qpPrefix',
   168          serialize(intersection(availablePrefixes, this.selectionPrefix))
   169        );
   170      });
   171  
   172      // Sort, format, and include the count in the label
   173      return prefixes.sortBy('prefix').map((name) => ({
   174        key: name.prefix,
   175        label: `${name.prefix} (${name.count})`,
   176      }));
   177    }
   178  
   179    @computed('qpNamespace', 'model.namespaces.[]')
   180    get optionsNamespaces() {
   181      const availableNamespaces = this.model.namespaces.map((namespace) => ({
   182        key: namespace.name,
   183        label: namespace.name,
   184      }));
   185  
   186      availableNamespaces.unshift({
   187        key: '*',
   188        label: 'All (*)',
   189      });
   190  
   191      // Unset the namespace selection if it was server-side deleted
   192      if (!availableNamespaces.mapBy('key').includes(this.qpNamespace)) {
   193        scheduleOnce('actions', () => {
   194          // eslint-disable-next-line ember/no-side-effects
   195          this.set('qpNamespace', '*');
   196        });
   197      }
   198  
   199      return availableNamespaces;
   200    }
   201  
   202    @computed('selectionNodePool', 'model.nodePools.[]')
   203    get optionsNodePool() {
   204      const availableNodePools = this.model.nodePools;
   205  
   206      scheduleOnce('actions', () => {
   207        // eslint-disable-next-line ember/no-side-effects
   208        this.set(
   209          'qpNodePool',
   210          serialize(
   211            intersection(
   212              availableNodePools.map(({ name }) => name),
   213              this.selectionNodePool
   214            )
   215          )
   216        );
   217      });
   218  
   219      return availableNodePools.map((nodePool) => ({
   220        key: nodePool.name,
   221        label: nodePool.name,
   222      }));
   223    }
   224  
   225    /**
   226      Visible jobs are those that match the selected namespace and aren't children
   227      of periodic or parameterized jobs.
   228    */
   229    @computed('model.jobs.@each.parent')
   230    get visibleJobs() {
   231      if (!this.model || !this.model.jobs) return [];
   232      return this.model.jobs
   233        .compact()
   234        .filter((job) => !job.isNew)
   235        .filter((job) => !job.get('parent.content'));
   236    }
   237  
   238    @computed(
   239      'visibleJobs.[]',
   240      'selectionType',
   241      'selectionStatus',
   242      'selectionDatacenter',
   243      'selectionNodePool',
   244      'selectionPrefix'
   245    )
   246    get filteredJobs() {
   247      const {
   248        selectionType: types,
   249        selectionStatus: statuses,
   250        selectionDatacenter: datacenters,
   251        selectionPrefix: prefixes,
   252        selectionNodePool: nodePools,
   253      } = this;
   254  
   255      // A job must match ALL filter facets, but it can match ANY selection within a facet
   256      // Always return early to prevent unnecessary facet predicates.
   257      return this.visibleJobs.filter((job) => {
   258        const shouldShowPack = types.includes('pack') && job.displayType.isPack;
   259  
   260        if (types.length && shouldShowPack) {
   261          return true;
   262        }
   263  
   264        if (types.length && !types.includes(job.get('displayType.type'))) {
   265          return false;
   266        }
   267  
   268        if (statuses.length && !statuses.includes(job.get('status'))) {
   269          return false;
   270        }
   271  
   272        if (
   273          datacenters.length &&
   274          !job.get('datacenters').find((dc) => datacenters.includes(dc))
   275        ) {
   276          return false;
   277        }
   278  
   279        if (nodePools.length && !nodePools.includes(job.get('nodePool'))) {
   280          return false;
   281        }
   282  
   283        const name = job.get('name');
   284        if (
   285          prefixes.length &&
   286          !prefixes.find((prefix) => name.startsWith(prefix))
   287        ) {
   288          return false;
   289        }
   290  
   291        return true;
   292      });
   293    }
   294  
   295    @alias('filteredJobs') listToSearch;
   296    @alias('listSearched') listToSort;
   297    @alias('listSorted') sortedJobs;
   298  
   299    isShowingDeploymentDetails = false;
   300  
   301    setFacetQueryParam(queryParam, selection) {
   302      this.set(queryParam, serialize(selection));
   303    }
   304  
   305    @action
   306    goToRun() {
   307      this.router.transitionTo('jobs.run');
   308    }
   309  }