github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/components/das/recommendation-card.js (about)

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