github.com/hernad/nomad@v1.6.112/ui/app/controllers/evaluations/index.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import { getOwner } from '@ember/application'; 7 import Controller from '@ember/controller'; 8 import { action } from '@ember/object'; 9 import { tracked } from '@glimmer/tracking'; 10 import { schedule } from '@ember/runloop'; 11 import { inject as service } from '@ember/service'; 12 import { useMachine } from 'ember-statecharts'; 13 import { use } from 'ember-usable'; 14 import evaluationsMachine from '../../machines/evaluations'; 15 16 const ALL_NAMESPACE_WILDCARD = '*'; 17 18 export default class EvaluationsController extends Controller { 19 @service store; 20 @service userSettings; 21 22 // We use statecharts here to manage complex user flows for the sidebar logic 23 @use 24 statechart = useMachine(evaluationsMachine).withConfig({ 25 services: { 26 loadEvaluation: this.loadEvaluation, 27 }, 28 actions: { 29 updateEvaluationQueryParameter: this.updateEvaluationQueryParameter, 30 removeCurrentEvaluationQueryParameter: 31 this.removeCurrentEvaluationQueryParameter, 32 }, 33 guards: { 34 sidebarIsOpen: this._sidebarIsOpen, 35 }, 36 }); 37 38 queryParams = [ 39 'nextToken', 40 'currentEval', 41 'pageSize', 42 'status', 43 { qpNamespace: 'namespace' }, 44 'type', 45 'searchTerm', 46 ]; 47 @tracked currentEval = null; 48 49 @action 50 _sidebarIsOpen() { 51 return !!this.currentEval; 52 } 53 54 @action 55 async loadEvaluation(context, { evaluation }) { 56 let evaluationId; 57 if (evaluation?.id) { 58 evaluationId = evaluation.id; 59 } else { 60 evaluationId = this.currentEval; 61 } 62 63 return this.store.findRecord('evaluation', evaluationId, { 64 reload: true, 65 adapterOptions: { related: true }, 66 }); 67 } 68 69 @action 70 async handleEvaluationClick(evaluation, e) { 71 if ( 72 e instanceof MouseEvent || 73 (e instanceof KeyboardEvent && 74 (e.code === 'Enter' || e.code === 'Space')) || 75 !e 76 ) { 77 this.statechart.send('LOAD_EVALUATION', { evaluation }); 78 } 79 } 80 81 @action 82 notifyEvalChange([evaluation]) { 83 schedule('actions', this, () => { 84 this.statechart.send('CHANGE_EVAL', { evaluation }); 85 }); 86 } 87 88 @action 89 updateEvaluationQueryParameter(context, { evaluation }) { 90 this.currentEval = evaluation.id; 91 } 92 93 @action 94 removeCurrentEvaluationQueryParameter() { 95 this.currentEval = null; 96 } 97 98 get shouldDisableNext() { 99 return !this.model.meta?.nextToken; 100 } 101 102 get shouldDisablePrev() { 103 return !this.previousTokens.length; 104 } 105 106 get optionsEvaluationsStatus() { 107 return [ 108 { key: null, label: 'All' }, 109 { key: 'blocked', label: 'Blocked' }, 110 { key: 'pending', label: 'Pending' }, 111 { key: 'complete', label: 'Complete' }, 112 { key: 'failed', label: 'Failed' }, 113 { key: 'canceled', label: 'Canceled' }, 114 ]; 115 } 116 117 get optionsTriggeredBy() { 118 return [ 119 { key: null, label: 'All' }, 120 { key: 'job-register', label: 'Job Register' }, 121 { key: 'job-deregister', label: 'Job Deregister' }, 122 { key: 'periodic-job', label: 'Periodic Job' }, 123 { key: 'node-drain', label: 'Node Drain' }, 124 { key: 'node-update', label: 'Node Update' }, 125 { key: 'alloc-stop', label: 'Allocation Stop' }, 126 { key: 'scheduled', label: 'Scheduled' }, 127 { key: 'rolling-update', label: 'Rolling Update' }, 128 { key: 'deployment-watcher', label: 'Deployment Watcher' }, 129 { key: 'failed-follow-up', label: 'Failed Follow Up' }, 130 { key: 'max-disconnect-timeout', label: 'Max Disconnect Timeout' }, 131 { key: 'max-plan-attempts', label: 'Max Plan Attempts' }, 132 { key: 'alloc-failure', label: 'Allocation Failure' }, 133 { key: 'queued-allocs', label: 'Queued Allocations' }, 134 { key: 'preemption', label: 'Preemption' }, 135 { key: 'job-scaling', label: 'Job Scalling' }, 136 ]; 137 } 138 139 get optionsNamespaces() { 140 const namespaces = this.store.peekAll('namespace').map((namespace) => ({ 141 key: namespace.name, 142 label: namespace.name, 143 })); 144 145 // Create default namespace selection 146 namespaces.unshift({ 147 key: ALL_NAMESPACE_WILDCARD, 148 label: 'All (*)', 149 }); 150 151 return namespaces; 152 } 153 154 get optionsType() { 155 return [ 156 { key: null, label: 'All' }, 157 { key: 'client', label: 'Client' }, 158 { key: 'no client', label: 'No Client' }, 159 ]; 160 } 161 162 filters = ['status', 'qpNamespace', 'type', 'triggeredBy', 'searchTerm']; 163 164 get hasFiltersApplied() { 165 return this.filters.reduce((result, filter) => { 166 // By default we always set qpNamespace to the '*' wildcard 167 // We need to ensure that if namespace is the only filter, that we send the correct error message to the user 168 if (this[filter] && filter !== 'qpNamespace') { 169 result = true; 170 } 171 return result; 172 }, false); 173 } 174 175 get currentFilters() { 176 const result = []; 177 for (const filter of this.filters) { 178 const isNamespaceWildcard = 179 filter === 'qpNamespace' && this[filter] === '*'; 180 if (this[filter] && !isNamespaceWildcard) { 181 result.push({ [filter]: this[filter] }); 182 } 183 } 184 return result; 185 } 186 187 get noMatchText() { 188 let text = ''; 189 const cleanNames = { 190 status: 'Status', 191 qpNamespace: 'Namespace', 192 type: 'Type', 193 triggeredBy: 'Triggered By', 194 searchTerm: 'Search Term', 195 }; 196 if (this.hasFiltersApplied) { 197 for (let i = 0; i < this.currentFilters.length; i++) { 198 const filter = this.currentFilters[i]; 199 const [name] = Object.keys(filter); 200 const filterName = cleanNames[name]; 201 const filterValue = filter[name]; 202 if (this.currentFilters.length === 1) 203 return `${filterName}: ${filterValue}.`; 204 if (i !== 0 && i !== this.currentFilters.length - 1) 205 text = text.concat(`, ${filterName}: ${filterValue}`); 206 if (i === 0) text = text.concat(`${filterName}: ${filterValue}`); 207 if (i === this.currentFilters.length - 1) { 208 return text.concat(`, ${filterName}: ${filterValue}.`); 209 } 210 } 211 } 212 213 return text; 214 } 215 216 @tracked pageSize = this.userSettings.pageSize; 217 @tracked nextToken = null; 218 @tracked previousTokens = []; 219 @tracked status = null; 220 @tracked triggeredBy = null; 221 @tracked qpNamespace = ALL_NAMESPACE_WILDCARD; 222 @tracked type = null; 223 @tracked searchTerm = null; 224 225 @action 226 onChange(newPageSize) { 227 this.pageSize = newPageSize; 228 } 229 230 @action 231 onNext(nextToken) { 232 this.previousTokens = [...this.previousTokens, this.nextToken]; 233 this.nextToken = nextToken; 234 } 235 236 @action 237 onPrev() { 238 const lastToken = this.previousTokens.pop(); 239 this.previousTokens = [...this.previousTokens]; 240 this.nextToken = lastToken; 241 } 242 243 @action 244 refresh() { 245 const isDefaultParams = this.nextToken === null && this.status === null; 246 if (isDefaultParams) { 247 getOwner(this).lookup('route:evaluations.index').refresh(); 248 return; 249 } 250 251 this._resetTokens(); 252 this.status = null; 253 this.pageSize = this.userSettings.pageSize; 254 } 255 256 @action 257 setQueryParam(qp, selection) { 258 this._resetTokens(); 259 this[qp] = selection; 260 } 261 262 @action 263 toggle() { 264 this._resetTokens(); 265 this.shouldOnlyDisplayClientEvals = !this.shouldOnlyDisplayClientEvals; 266 } 267 268 @action 269 _resetTokens() { 270 this.nextToken = null; 271 this.previousTokens = []; 272 } 273 }