github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/controllers/optimize.js (about) 1 /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ 2 import Controller from '@ember/controller'; 3 import { action } from '@ember/object'; 4 import { tracked } from '@glimmer/tracking'; 5 import { inject as controller } from '@ember/controller'; 6 import { inject as service } from '@ember/service'; 7 import { scheduleOnce } from '@ember/runloop'; 8 import { task } from 'ember-concurrency'; 9 import intersection from 'lodash.intersection'; 10 import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; 11 12 import EmberObject, { computed } from '@ember/object'; 13 import { alias } from '@ember/object/computed'; 14 import Searchable from 'nomad-ui/mixins/searchable'; 15 import classic from 'ember-classic-decorator'; 16 17 export default class OptimizeController extends Controller { 18 @controller('optimize/summary') summaryController; 19 @service router; 20 @service system; 21 22 queryParams = [ 23 { 24 includeAllNamespaces: 'all-namespaces', 25 }, 26 { 27 searchTerm: 'search', 28 }, 29 { 30 qpType: 'type', 31 }, 32 { 33 qpStatus: 'status', 34 }, 35 { 36 qpDatacenter: 'dc', 37 }, 38 { 39 qpPrefix: 'prefix', 40 }, 41 ]; 42 43 constructor() { 44 super(...arguments); 45 46 this.summarySearch = RecommendationSummarySearch.create({ 47 dataSource: this, 48 }); 49 } 50 51 @tracked searchTerm = ''; 52 53 @tracked qpType = ''; 54 @tracked qpStatus = ''; 55 @tracked qpDatacenter = ''; 56 @tracked qpPrefix = ''; 57 58 @tracked includeAllNamespaces = true; 59 60 @selection('qpType') selectionType; 61 @selection('qpStatus') selectionStatus; 62 @selection('qpDatacenter') selectionDatacenter; 63 @selection('qpPrefix') selectionPrefix; 64 65 optionsType = [{ key: 'service', label: 'Service' }, { key: 'system', label: 'System' }]; 66 67 optionsStatus = [ 68 { key: 'pending', label: 'Pending' }, 69 { key: 'running', label: 'Running' }, 70 { key: 'dead', label: 'Dead' }, 71 ]; 72 73 get optionsDatacenter() { 74 const flatten = (acc, val) => acc.concat(val); 75 const allDatacenters = new Set(this.model.mapBy('job.datacenters').reduce(flatten, [])); 76 77 // Remove any invalid datacenters from the query param/selection 78 const availableDatacenters = Array.from(allDatacenters).compact(); 79 scheduleOnce('actions', () => { 80 // eslint-disable-next-line ember/no-side-effects 81 this.qpDatacenter = serialize(intersection(availableDatacenters, this.selectionDatacenter)); 82 }); 83 84 return availableDatacenters.sort().map(dc => ({ key: dc, label: dc })); 85 } 86 87 get optionsPrefix() { 88 // A prefix is defined as the start of a job name up to the first - or . 89 // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds 90 const hasPrefix = /.[-._]/; 91 92 // Collect and count all the prefixes 93 const allNames = this.model.mapBy('job.name'); 94 const nameHistogram = allNames.reduce((hist, name) => { 95 if (hasPrefix.test(name)) { 96 const prefix = name.match(/(.+?)[-._]/)[1]; 97 hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1; 98 } 99 return hist; 100 }, {}); 101 102 // Convert to an array 103 const nameTable = Object.keys(nameHistogram).map(key => ({ 104 prefix: key, 105 count: nameHistogram[key], 106 })); 107 108 // Only consider prefixes that match more than one name 109 const prefixes = nameTable.filter(name => name.count > 1); 110 111 // Remove any invalid prefixes from the query param/selection 112 const availablePrefixes = prefixes.mapBy('prefix'); 113 scheduleOnce('actions', () => { 114 // eslint-disable-next-line ember/no-side-effects 115 this.qpPrefix = serialize(intersection(availablePrefixes, this.selectionPrefix)); 116 }); 117 118 // Sort, format, and include the count in the label 119 return prefixes.sortBy('prefix').map(name => ({ 120 key: name.prefix, 121 label: `${name.prefix} (${name.count})`, 122 })); 123 } 124 125 get filteredSummaries() { 126 const { 127 selectionType: types, 128 selectionStatus: statuses, 129 selectionDatacenter: datacenters, 130 selectionPrefix: prefixes, 131 } = this; 132 133 const shouldShowNamespaces = this.system.shouldShowNamespaces; 134 const activeNamespace = shouldShowNamespaces ? this.system.activeNamespace.name : undefined; 135 136 // A summary’s job must match ALL filter facets, but it can match ANY selection within a facet 137 // Always return early to prevent unnecessary facet predicates. 138 return this.summarySearch.listSearched.filter(summary => { 139 const job = summary.get('job'); 140 141 if (job.isDestroying) { 142 return false; 143 } 144 145 if ( 146 shouldShowNamespaces && 147 !this.includeAllNamespaces && 148 activeNamespace !== summary.jobNamespace 149 ) { 150 return false; 151 } 152 153 if (types.length && !types.includes(job.get('displayType'))) { 154 return false; 155 } 156 157 if (statuses.length && !statuses.includes(job.get('status'))) { 158 return false; 159 } 160 161 if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) { 162 return false; 163 } 164 165 const name = job.get('name'); 166 if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) { 167 return false; 168 } 169 170 return true; 171 }); 172 } 173 174 get activeRecommendationSummary() { 175 if (this.router.currentRouteName === 'optimize.summary') { 176 return this.summaryController.model; 177 } else { 178 return undefined; 179 } 180 } 181 182 // This is a task because the accordion uses timeouts for animation 183 // eslint-disable-next-line require-yield 184 @(task(function*() { 185 const currentSummaryIndex = this.filteredSummaries.indexOf(this.activeRecommendationSummary); 186 const nextSummary = this.filteredSummaries.objectAt(currentSummaryIndex + 1); 187 188 if (nextSummary) { 189 this.transitionToSummary(nextSummary); 190 } else { 191 this.send('reachedEnd'); 192 } 193 }).drop()) 194 proceed; 195 196 @action 197 transitionToSummary(summary) { 198 this.transitionToRoute('optimize.summary', summary.slug, { 199 queryParams: { jobNamespace: summary.jobNamespace }, 200 }); 201 } 202 203 @action 204 setFacetQueryParam(queryParam, selection) { 205 this[queryParam] = serialize(selection); 206 this.syncActiveSummary(); 207 } 208 209 @action 210 toggleIncludeAllNamespaces() { 211 this.includeAllNamespaces = !this.includeAllNamespaces; 212 this.syncActiveSummary(); 213 } 214 215 @action 216 syncActiveSummary() { 217 scheduleOnce('actions', () => { 218 if ( 219 !this.activeRecommendationSummary || 220 !this.filteredSummaries.includes(this.activeRecommendationSummary) 221 ) { 222 const firstFilteredSummary = this.filteredSummaries.objectAt(0); 223 224 if (firstFilteredSummary) { 225 this.transitionToSummary(firstFilteredSummary); 226 } else { 227 this.transitionToRoute('optimize'); 228 } 229 } 230 }); 231 } 232 } 233 234 @classic 235 class RecommendationSummarySearch extends EmberObject.extend(Searchable) { 236 @computed 237 get fuzzySearchProps() { 238 return ['slug']; 239 } 240 241 @alias('dataSource.model') listToSearch; 242 @alias('dataSource.searchTerm') searchTerm; 243 244 exactMatchEnabled = false; 245 fuzzySearchEnabled = true; 246 includeFuzzySearchMatches = true; 247 }