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 }