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 }