github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/controllers/optimize.js (about)

     1  /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */
     2  import Controller from '@ember/controller';
     3  import { action } from '@ember/object';
     4  import { tracked } from '@glimmer/tracking';
     5  import { inject as controller } from '@ember/controller';
     6  import { inject as service } from '@ember/service';
     7  import { scheduleOnce } from '@ember/runloop';
     8  import { task } from 'ember-concurrency';
     9  import intersection from 'lodash.intersection';
    10  import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize';
    11  
    12  import EmberObject, { computed } from '@ember/object';
    13  import { alias } from '@ember/object/computed';
    14  import Searchable from 'nomad-ui/mixins/searchable';
    15  import classic from 'ember-classic-decorator';
    16  
    17  export default class OptimizeController extends Controller {
    18    @controller('optimize/summary') summaryController;
    19    @service router;
    20    @service system;
    21  
    22    queryParams = [
    23      {
    24        includeAllNamespaces: 'all-namespaces',
    25      },
    26      {
    27        searchTerm: 'search',
    28      },
    29      {
    30        qpType: 'type',
    31      },
    32      {
    33        qpStatus: 'status',
    34      },
    35      {
    36        qpDatacenter: 'dc',
    37      },
    38      {
    39        qpPrefix: 'prefix',
    40      },
    41    ];
    42  
    43    constructor() {
    44      super(...arguments);
    45  
    46      this.summarySearch = RecommendationSummarySearch.create({
    47        dataSource: this,
    48      });
    49    }
    50  
    51    @tracked searchTerm = '';
    52  
    53    @tracked qpType = '';
    54    @tracked qpStatus = '';
    55    @tracked qpDatacenter = '';
    56    @tracked qpPrefix = '';
    57  
    58    @tracked includeAllNamespaces = true;
    59  
    60    @selection('qpType') selectionType;
    61    @selection('qpStatus') selectionStatus;
    62    @selection('qpDatacenter') selectionDatacenter;
    63    @selection('qpPrefix') selectionPrefix;
    64  
    65    optionsType = [{ key: 'service', label: 'Service' }, { key: 'system', label: 'System' }];
    66  
    67    optionsStatus = [
    68      { key: 'pending', label: 'Pending' },
    69      { key: 'running', label: 'Running' },
    70      { key: 'dead', label: 'Dead' },
    71    ];
    72  
    73    get optionsDatacenter() {
    74      const flatten = (acc, val) => acc.concat(val);
    75      const allDatacenters = new Set(this.model.mapBy('job.datacenters').reduce(flatten, []));
    76  
    77      // Remove any invalid datacenters from the query param/selection
    78      const availableDatacenters = Array.from(allDatacenters).compact();
    79      scheduleOnce('actions', () => {
    80        // eslint-disable-next-line ember/no-side-effects
    81        this.qpDatacenter = serialize(intersection(availableDatacenters, this.selectionDatacenter));
    82      });
    83  
    84      return availableDatacenters.sort().map(dc => ({ key: dc, label: dc }));
    85    }
    86  
    87    get optionsPrefix() {
    88      // A prefix is defined as the start of a job name up to the first - or .
    89      // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds
    90      const hasPrefix = /.[-._]/;
    91  
    92      // Collect and count all the prefixes
    93      const allNames = this.model.mapBy('job.name');
    94      const nameHistogram = allNames.reduce((hist, name) => {
    95        if (hasPrefix.test(name)) {
    96          const prefix = name.match(/(.+?)[-._]/)[1];
    97          hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1;
    98        }
    99        return hist;
   100      }, {});
   101  
   102      // Convert to an array
   103      const nameTable = Object.keys(nameHistogram).map(key => ({
   104        prefix: key,
   105        count: nameHistogram[key],
   106      }));
   107  
   108      // Only consider prefixes that match more than one name
   109      const prefixes = nameTable.filter(name => name.count > 1);
   110  
   111      // Remove any invalid prefixes from the query param/selection
   112      const availablePrefixes = prefixes.mapBy('prefix');
   113      scheduleOnce('actions', () => {
   114        // eslint-disable-next-line ember/no-side-effects
   115        this.qpPrefix = serialize(intersection(availablePrefixes, this.selectionPrefix));
   116      });
   117  
   118      // Sort, format, and include the count in the label
   119      return prefixes.sortBy('prefix').map(name => ({
   120        key: name.prefix,
   121        label: `${name.prefix} (${name.count})`,
   122      }));
   123    }
   124  
   125    get filteredSummaries() {
   126      const {
   127        selectionType: types,
   128        selectionStatus: statuses,
   129        selectionDatacenter: datacenters,
   130        selectionPrefix: prefixes,
   131      } = this;
   132  
   133      const shouldShowNamespaces = this.system.shouldShowNamespaces;
   134      const activeNamespace = shouldShowNamespaces ? this.system.activeNamespace.name : undefined;
   135  
   136      // A summary’s job must match ALL filter facets, but it can match ANY selection within a facet
   137      // Always return early to prevent unnecessary facet predicates.
   138      return this.summarySearch.listSearched.filter(summary => {
   139        const job = summary.get('job');
   140  
   141        if (job.isDestroying) {
   142          return false;
   143        }
   144  
   145        if (
   146          shouldShowNamespaces &&
   147          !this.includeAllNamespaces &&
   148          activeNamespace !== summary.jobNamespace
   149        ) {
   150          return false;
   151        }
   152  
   153        if (types.length && !types.includes(job.get('displayType'))) {
   154          return false;
   155        }
   156  
   157        if (statuses.length && !statuses.includes(job.get('status'))) {
   158          return false;
   159        }
   160  
   161        if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) {
   162          return false;
   163        }
   164  
   165        const name = job.get('name');
   166        if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) {
   167          return false;
   168        }
   169  
   170        return true;
   171      });
   172    }
   173  
   174    get activeRecommendationSummary() {
   175      if (this.router.currentRouteName === 'optimize.summary') {
   176        return this.summaryController.model;
   177      } else {
   178        return undefined;
   179      }
   180    }
   181  
   182    // This is a task because the accordion uses timeouts for animation
   183    // eslint-disable-next-line require-yield
   184    @(task(function*() {
   185      const currentSummaryIndex = this.filteredSummaries.indexOf(this.activeRecommendationSummary);
   186      const nextSummary = this.filteredSummaries.objectAt(currentSummaryIndex + 1);
   187  
   188      if (nextSummary) {
   189        this.transitionToSummary(nextSummary);
   190      } else {
   191        this.send('reachedEnd');
   192      }
   193    }).drop())
   194    proceed;
   195  
   196    @action
   197    transitionToSummary(summary) {
   198      this.transitionToRoute('optimize.summary', summary.slug, {
   199        queryParams: { jobNamespace: summary.jobNamespace },
   200      });
   201    }
   202  
   203    @action
   204    setFacetQueryParam(queryParam, selection) {
   205      this[queryParam] = serialize(selection);
   206      this.syncActiveSummary();
   207    }
   208  
   209    @action
   210    toggleIncludeAllNamespaces() {
   211      this.includeAllNamespaces = !this.includeAllNamespaces;
   212      this.syncActiveSummary();
   213    }
   214  
   215    @action
   216    syncActiveSummary() {
   217      scheduleOnce('actions', () => {
   218        if (
   219          !this.activeRecommendationSummary ||
   220          !this.filteredSummaries.includes(this.activeRecommendationSummary)
   221        ) {
   222          const firstFilteredSummary = this.filteredSummaries.objectAt(0);
   223  
   224          if (firstFilteredSummary) {
   225            this.transitionToSummary(firstFilteredSummary);
   226          } else {
   227            this.transitionToRoute('optimize');
   228          }
   229        }
   230      });
   231    }
   232  }
   233  
   234  @classic
   235  class RecommendationSummarySearch extends EmberObject.extend(Searchable) {
   236    @computed
   237    get fuzzySearchProps() {
   238      return ['slug'];
   239    }
   240  
   241    @alias('dataSource.model') listToSearch;
   242    @alias('dataSource.searchTerm') searchTerm;
   243  
   244    exactMatchEnabled = false;
   245    fuzzySearchEnabled = true;
   246    includeFuzzySearchMatches = true;
   247  }