github.com/hernad/nomad@v1.6.112/ui/app/components/das/recommendation-card.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import Component from '@glimmer/component';
     7  import { inject as service } from '@ember/service';
     8  import { tracked } from '@glimmer/tracking';
     9  import { action } from '@ember/object';
    10  import ResourcesDiffs from 'nomad-ui/utils/resources-diffs';
    11  import { htmlSafe } from '@ember/template';
    12  import { didCancel, task, timeout } from 'ember-concurrency';
    13  import Ember from 'ember';
    14  
    15  export default class DasRecommendationCardComponent extends Component {
    16    @service router;
    17  
    18    @tracked allCpuToggleActive = true;
    19    @tracked allMemoryToggleActive = true;
    20  
    21    @tracked activeTaskToggleRowIndex = 0;
    22  
    23    element = null;
    24  
    25    @tracked cardHeight;
    26    @tracked interstitialComponent;
    27    @tracked error;
    28  
    29    @tracked proceedPromiseResolve;
    30  
    31    get activeTaskToggleRow() {
    32      return this.taskToggleRows[this.activeTaskToggleRowIndex];
    33    }
    34  
    35    get activeTask() {
    36      return this.activeTaskToggleRow.task;
    37    }
    38  
    39    get narrative() {
    40      const summary = this.args.summary;
    41      const taskGroup = summary.taskGroup;
    42  
    43      const diffs = new ResourcesDiffs(
    44        taskGroup,
    45        taskGroup.count,
    46        this.args.summary.recommendations,
    47        this.args.summary.excludedRecommendations
    48      );
    49  
    50      const cpuDelta = diffs.cpu.delta;
    51      const memoryDelta = diffs.memory.delta;
    52  
    53      const aggregate = taskGroup.count > 1;
    54      const aggregateString = aggregate ? ' an aggregate' : '';
    55  
    56      if (cpuDelta || memoryDelta) {
    57        const deltasSameDirection =
    58          (cpuDelta < 0 && memoryDelta < 0) || (cpuDelta > 0 && memoryDelta > 0);
    59  
    60        let narrative = 'Applying the selected recommendations will';
    61  
    62        if (deltasSameDirection) {
    63          narrative += ` ${verbForDelta(cpuDelta)} ${aggregateString}`;
    64        }
    65  
    66        if (cpuDelta) {
    67          if (!deltasSameDirection) {
    68            narrative += ` ${verbForDelta(cpuDelta)} ${aggregateString}`;
    69          }
    70  
    71          narrative += ` <strong>${diffs.cpu.absoluteAggregateDiff} of CPU</strong>`;
    72        }
    73  
    74        if (cpuDelta && memoryDelta) {
    75          narrative += ' and';
    76        }
    77  
    78        if (memoryDelta) {
    79          if (!deltasSameDirection) {
    80            narrative += ` ${verbForDelta(memoryDelta)} ${aggregateString}`;
    81          }
    82  
    83          narrative += ` <strong>${diffs.memory.absoluteAggregateDiff} of memory</strong>`;
    84        }
    85  
    86        if (taskGroup.count === 1) {
    87          narrative += '.';
    88        } else {
    89          narrative += ` across <strong>${taskGroup.count} allocations</strong>.`;
    90        }
    91  
    92        return htmlSafe(narrative);
    93      } else {
    94        return '';
    95      }
    96    }
    97  
    98    get taskToggleRows() {
    99      const taskNameToTaskToggles = {};
   100  
   101      return this.args.summary.recommendations.reduce(
   102        (taskToggleRows, recommendation) => {
   103          let taskToggleRow = taskNameToTaskToggles[recommendation.task.name];
   104  
   105          if (!taskToggleRow) {
   106            taskToggleRow = {
   107              recommendations: [],
   108              task: recommendation.task,
   109            };
   110  
   111            taskNameToTaskToggles[recommendation.task.name] = taskToggleRow;
   112            taskToggleRows.push(taskToggleRow);
   113          }
   114  
   115          const isCpu = recommendation.resource === 'CPU';
   116          const rowResourceProperty = isCpu ? 'cpu' : 'memory';
   117  
   118          taskToggleRow[rowResourceProperty] = {
   119            recommendation,
   120            isActive:
   121              !this.args.summary.excludedRecommendations.includes(recommendation),
   122          };
   123  
   124          if (isCpu) {
   125            taskToggleRow.recommendations.unshift(recommendation);
   126          } else {
   127            taskToggleRow.recommendations.push(recommendation);
   128          }
   129  
   130          return taskToggleRows;
   131        },
   132        []
   133      );
   134    }
   135  
   136    get showToggleAllToggles() {
   137      return this.taskToggleRows.length > 1;
   138    }
   139  
   140    get allCpuToggleDisabled() {
   141      return !this.args.summary.recommendations.filterBy('resource', 'CPU')
   142        .length;
   143    }
   144  
   145    get allMemoryToggleDisabled() {
   146      return !this.args.summary.recommendations.filterBy('resource', 'MemoryMB')
   147        .length;
   148    }
   149  
   150    get cannotAccept() {
   151      return (
   152        this.args.summary.excludedRecommendations.length ==
   153        this.args.summary.recommendations.length
   154      );
   155    }
   156  
   157    get copyButtonLink() {
   158      const path = this.router.urlFor(
   159        'optimize.summary',
   160        this.args.summary.slug,
   161        {
   162          queryParams: { namespace: this.args.summary.jobNamespace },
   163        }
   164      );
   165      const { origin } = window.location;
   166  
   167      return `${origin}${path}`;
   168    }
   169  
   170    @action
   171    toggleAllRecommendationsForResource(resource) {
   172      let enabled;
   173  
   174      if (resource === 'CPU') {
   175        this.allCpuToggleActive = !this.allCpuToggleActive;
   176        enabled = this.allCpuToggleActive;
   177      } else {
   178        this.allMemoryToggleActive = !this.allMemoryToggleActive;
   179        enabled = this.allMemoryToggleActive;
   180      }
   181  
   182      this.args.summary.toggleAllRecommendationsForResource(resource, enabled);
   183    }
   184  
   185    @action
   186    accept() {
   187      this.storeCardHeight();
   188      this.args.summary
   189        .save()
   190        .then(
   191          () => this.onApplied.perform(),
   192          (e) => this.onError.perform(e)
   193        )
   194        .catch((e) => {
   195          if (!didCancel(e)) {
   196            throw e;
   197          }
   198        });
   199    }
   200  
   201    @action
   202    async dismiss() {
   203      this.storeCardHeight();
   204      const recommendations = await this.args.summary.recommendations;
   205  
   206      this.args.summary.excludedRecommendations.pushObjects(recommendations);
   207  
   208      this.args.summary
   209        .save()
   210        .then(
   211          () => this.onDismissed.perform(),
   212          (e) => this.onError.perform(e)
   213        )
   214        .catch((e) => {
   215          if (!didCancel(e)) {
   216            throw e;
   217          }
   218        });
   219    }
   220  
   221    @(task(function* () {
   222      this.interstitialComponent = 'accepted';
   223      yield timeout(Ember.testing ? 0 : 2000);
   224  
   225      this.args.proceed.perform();
   226      this.resetInterstitial();
   227    }).drop())
   228    onApplied;
   229  
   230    @(task(function* () {
   231      const { manuallyDismissed } = yield new Promise((resolve) => {
   232        this.proceedPromiseResolve = resolve;
   233        this.interstitialComponent = 'dismissed';
   234      });
   235  
   236      if (!manuallyDismissed) {
   237        yield timeout(Ember.testing ? 0 : 2000);
   238      }
   239  
   240      this.args.proceed.perform();
   241      this.resetInterstitial();
   242    }).drop())
   243    onDismissed;
   244  
   245    @(task(function* (error) {
   246      yield new Promise((resolve) => {
   247        this.proceedPromiseResolve = resolve;
   248        this.interstitialComponent = 'error';
   249        this.error = error.toString();
   250      });
   251  
   252      this.args.proceed.perform();
   253      this.resetInterstitial();
   254    }).drop())
   255    onError;
   256  
   257    get interstitialStyle() {
   258      return htmlSafe(`height: ${this.cardHeight}px`);
   259    }
   260  
   261    resetInterstitial() {
   262      if (!this.args.skipReset) {
   263        this.interstitialComponent = undefined;
   264        this.error = undefined;
   265      }
   266    }
   267  
   268    @action
   269    cardInserted(element) {
   270      this.element = element;
   271    }
   272  
   273    storeCardHeight() {
   274      this.cardHeight = this.element.clientHeight;
   275    }
   276  }
   277  
   278  function verbForDelta(delta) {
   279    if (delta > 0) {
   280      return 'add';
   281    } else {
   282      return 'save';
   283    }
   284  }