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 }