github.com/hernad/nomad@v1.6.112/ui/app/controllers/clients/client/index.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  /* eslint-disable ember/no-observers */
     7  /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */
     8  import { alias } from '@ember/object/computed';
     9  import Controller from '@ember/controller';
    10  import { action, computed } from '@ember/object';
    11  import { observes } from '@ember-decorators/object';
    12  import { scheduleOnce } from '@ember/runloop';
    13  import { task } from 'ember-concurrency';
    14  import intersection from 'lodash.intersection';
    15  import Sortable from 'nomad-ui/mixins/sortable';
    16  import Searchable from 'nomad-ui/mixins/searchable';
    17  import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
    18  import {
    19    serialize,
    20    deserializedQueryParam as selection,
    21  } from 'nomad-ui/utils/qp-serialize';
    22  import classic from 'ember-classic-decorator';
    23  import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
    24  import { inject as service } from '@ember/service';
    25  import { tracked } from '@glimmer/tracking';
    26  
    27  @classic
    28  export default class ClientController extends Controller.extend(
    29    Sortable,
    30    Searchable
    31  ) {
    32    @service notifications;
    33  
    34    queryParams = [
    35      {
    36        currentPage: 'page',
    37      },
    38      {
    39        searchTerm: 'search',
    40      },
    41      {
    42        sortProperty: 'sort',
    43      },
    44      {
    45        sortDescending: 'desc',
    46      },
    47      {
    48        onlyPreemptions: 'preemptions',
    49      },
    50      {
    51        qpNamespace: 'namespace',
    52      },
    53      {
    54        qpJob: 'job',
    55      },
    56      {
    57        qpStatus: 'status',
    58      },
    59      'activeTask',
    60    ];
    61  
    62    // Set in the route
    63    flagAsDraining = false;
    64  
    65    qpNamespace = '';
    66    qpJob = '';
    67    qpStatus = '';
    68    currentPage = 1;
    69    pageSize = 8;
    70    activeTask = null;
    71  
    72    sortProperty = 'modifyIndex';
    73    sortDescending = true;
    74  
    75    @localStorageProperty('nomadShowSubTasks', false) showSubTasks;
    76  
    77    @action
    78    toggleShowSubTasks(e) {
    79      e.preventDefault();
    80      this.set('showSubTasks', !this.get('showSubTasks'));
    81    }
    82  
    83    @computed()
    84    get searchProps() {
    85      return ['shortId', 'name'];
    86    }
    87  
    88    onlyPreemptions = false;
    89  
    90    @computed('model.allocations.[]', 'preemptions.[]', 'onlyPreemptions')
    91    get visibleAllocations() {
    92      return this.onlyPreemptions ? this.preemptions : this.model.allocations;
    93    }
    94  
    95    @computed(
    96      'visibleAllocations.[]',
    97      'selectionNamespace',
    98      'selectionJob',
    99      'selectionStatus'
   100    )
   101    get filteredAllocations() {
   102      const { selectionNamespace, selectionJob, selectionStatus } = this;
   103  
   104      return this.visibleAllocations.filter((alloc) => {
   105        if (
   106          selectionNamespace.length &&
   107          !selectionNamespace.includes(alloc.get('namespace'))
   108        ) {
   109          return false;
   110        }
   111        if (
   112          selectionJob.length &&
   113          !selectionJob.includes(alloc.get('plainJobId'))
   114        ) {
   115          return false;
   116        }
   117        if (
   118          selectionStatus.length &&
   119          !selectionStatus.includes(alloc.clientStatus)
   120        ) {
   121          return false;
   122        }
   123        return true;
   124      });
   125    }
   126  
   127    @alias('filteredAllocations') listToSort;
   128    @alias('listSorted') listToSearch;
   129    @alias('listSearched') sortedAllocations;
   130  
   131    @selection('qpNamespace') selectionNamespace;
   132    @selection('qpJob') selectionJob;
   133    @selection('qpStatus') selectionStatus;
   134  
   135    eligibilityError = null;
   136    stopDrainError = null;
   137    drainError = null;
   138    showDrainNotification = false;
   139    showDrainUpdateNotification = false;
   140    showDrainStoppedNotification = false;
   141  
   142    @computed('model.allocations.@each.wasPreempted')
   143    get preemptions() {
   144      return this.model.allocations.filterBy('wasPreempted');
   145    }
   146  
   147    @computed('model.events.@each.time')
   148    get sortedEvents() {
   149      return this.get('model.events').sortBy('time').reverse();
   150    }
   151  
   152    @computed('model.drivers.@each.name')
   153    get sortedDrivers() {
   154      return this.get('model.drivers').sortBy('name');
   155    }
   156  
   157    @computed('model.hostVolumes.@each.name')
   158    get sortedHostVolumes() {
   159      return this.model.hostVolumes.sortBy('name');
   160    }
   161  
   162    @(task(function* (value) {
   163      try {
   164        yield value ? this.model.setEligible() : this.model.setIneligible();
   165      } catch (err) {
   166        const error = messageFromAdapterError(err) || 'Could not set eligibility';
   167        this.set('eligibilityError', error);
   168      }
   169    }).drop())
   170    setEligibility;
   171  
   172    @(task(function* () {
   173      try {
   174        this.set('flagAsDraining', false);
   175        yield this.model.cancelDrain();
   176        this.set('showDrainStoppedNotification', true);
   177      } catch (err) {
   178        this.set('flagAsDraining', true);
   179        const error = messageFromAdapterError(err) || 'Could not stop drain';
   180        this.set('stopDrainError', error);
   181      }
   182    }).drop())
   183    stopDrain;
   184  
   185    @(task(function* () {
   186      try {
   187        yield this.model.forceDrain({
   188          IgnoreSystemJobs: this.model.drainStrategy.ignoreSystemJobs,
   189        });
   190      } catch (err) {
   191        const error = messageFromAdapterError(err) || 'Could not force drain';
   192        this.set('drainError', error);
   193      }
   194    }).drop())
   195    forceDrain;
   196  
   197    @observes('model.isDraining')
   198    triggerDrainNotification() {
   199      if (!this.model.isDraining && this.flagAsDraining) {
   200        this.set('showDrainNotification', true);
   201      }
   202  
   203      this.set('flagAsDraining', this.model.isDraining);
   204    }
   205  
   206    @action
   207    gotoAllocation(allocation) {
   208      this.transitionToRoute('allocations.allocation', allocation.id);
   209    }
   210  
   211    @action
   212    setPreemptionFilter(value) {
   213      this.set('onlyPreemptions', value);
   214    }
   215  
   216    @action
   217    drainNotify(isUpdating) {
   218      this.set('showDrainUpdateNotification', isUpdating);
   219    }
   220  
   221    @action
   222    setDrainError(err) {
   223      const error = messageFromAdapterError(err) || 'Could not run drain';
   224      this.set('drainError', error);
   225    }
   226  
   227    get optionsAllocationStatus() {
   228      return [
   229        { key: 'pending', label: 'Pending' },
   230        { key: 'running', label: 'Running' },
   231        { key: 'complete', label: 'Complete' },
   232        { key: 'failed', label: 'Failed' },
   233        { key: 'lost', label: 'Lost' },
   234        { key: 'unknown', label: 'Unknown' },
   235      ];
   236    }
   237  
   238    @computed('model.allocations.[]', 'selectionJob', 'selectionNamespace')
   239    get optionsJob() {
   240      // Only show options for jobs in the selected namespaces, if any.
   241      const ns = this.selectionNamespace;
   242      const jobs = Array.from(
   243        new Set(
   244          this.model.allocations
   245            .filter((a) => ns.length === 0 || ns.includes(a.namespace))
   246            .mapBy('plainJobId')
   247        )
   248      ).compact();
   249  
   250      // Update query param when the list of jobs changes.
   251      scheduleOnce('actions', () => {
   252        // eslint-disable-next-line ember/no-side-effects
   253        this.set('qpJob', serialize(intersection(jobs, this.selectionJob)));
   254      });
   255  
   256      return jobs.sort().map((job) => ({ key: job, label: job }));
   257    }
   258  
   259    @computed('model.allocations.[]', 'selectionNamespace')
   260    get optionsNamespace() {
   261      const ns = Array.from(
   262        new Set(this.model.allocations.mapBy('namespace'))
   263      ).compact();
   264  
   265      // Update query param when the list of namespaces changes.
   266      scheduleOnce('actions', () => {
   267        // eslint-disable-next-line ember/no-side-effects
   268        this.set(
   269          'qpNamespace',
   270          serialize(intersection(ns, this.selectionNamespace))
   271        );
   272      });
   273  
   274      return ns.sort().map((n) => ({ key: n, label: n }));
   275    }
   276  
   277    setFacetQueryParam(queryParam, selection) {
   278      this.set(queryParam, serialize(selection));
   279    }
   280  
   281    @action
   282    setActiveTaskQueryParam(task) {
   283      if (task) {
   284        this.set('activeTask', `${task.allocation.id}-${task.name}`);
   285      } else {
   286        this.set('activeTask', null);
   287      }
   288    }
   289  
   290    // #region metadata
   291  
   292    @tracked editingMetadata = false;
   293  
   294    get hasMeta() {
   295      return (
   296        this.model.meta?.structured && Object.keys(this.model.meta?.structured)
   297      );
   298    }
   299  
   300    @tracked newMetaData = {
   301      key: '',
   302      value: '',
   303    };
   304  
   305    @action resetNewMetaData() {
   306      this.newMetaData = {
   307        key: '',
   308        value: '',
   309      };
   310    }
   311  
   312    @action validateMetadata(event) {
   313      if (event.key === 'Escape') {
   314        this.resetNewMetaData();
   315        this.editingMetadata = false;
   316      }
   317    }
   318  
   319    @action async addDynamicMetaData({ key, value }, e) {
   320      try {
   321        e.preventDefault();
   322        await this.model.addMeta({ [key]: value });
   323  
   324        this.notifications.add({
   325          title: 'Metadata added',
   326          message: `${key} successfully saved`,
   327          color: 'success',
   328        });
   329      } catch (err) {
   330        const error =
   331          messageFromAdapterError(err) || 'Could not save new dynamic metadata';
   332        this.notifications.add({
   333          title: `Error saving Metadata`,
   334          message: error,
   335          color: 'critical',
   336          sticky: true,
   337        });
   338      }
   339    }
   340    // #endregion metadata
   341  }