github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/controllers/jobs/index.js (about) 1 /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ 2 import { inject as service } from '@ember/service'; 3 import { alias, readOnly } from '@ember/object/computed'; 4 import Controller, { inject as controller } from '@ember/controller'; 5 import { action, computed } from '@ember/object'; 6 import { scheduleOnce } from '@ember/runloop'; 7 import intersection from 'lodash.intersection'; 8 import Sortable from 'nomad-ui/mixins/sortable'; 9 import Searchable from 'nomad-ui/mixins/searchable'; 10 import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; 11 import classic from 'ember-classic-decorator'; 12 13 @classic 14 export default class IndexController extends Controller.extend(Sortable, Searchable) { 15 @service system; 16 @service userSettings; 17 @controller('jobs') jobsController; 18 19 @alias('jobsController.isForbidden') isForbidden; 20 21 queryParams = [ 22 { 23 currentPage: 'page', 24 }, 25 { 26 searchTerm: 'search', 27 }, 28 { 29 sortProperty: 'sort', 30 }, 31 { 32 sortDescending: 'desc', 33 }, 34 { 35 qpType: 'type', 36 }, 37 { 38 qpStatus: 'status', 39 }, 40 { 41 qpDatacenter: 'dc', 42 }, 43 { 44 qpPrefix: 'prefix', 45 }, 46 ]; 47 48 currentPage = 1; 49 @readOnly('userSettings.pageSize') pageSize; 50 51 sortProperty = 'modifyIndex'; 52 sortDescending = true; 53 54 @computed 55 get searchProps() { 56 return ['id', 'name']; 57 } 58 59 @computed 60 get fuzzySearchProps() { 61 return ['name']; 62 } 63 64 fuzzySearchEnabled = true; 65 66 qpType = ''; 67 qpStatus = ''; 68 qpDatacenter = ''; 69 qpPrefix = ''; 70 71 @selection('qpType') selectionType; 72 @selection('qpStatus') selectionStatus; 73 @selection('qpDatacenter') selectionDatacenter; 74 @selection('qpPrefix') selectionPrefix; 75 76 @computed 77 get optionsType() { 78 return [ 79 { key: 'batch', label: 'Batch' }, 80 { key: 'parameterized', label: 'Parameterized' }, 81 { key: 'periodic', label: 'Periodic' }, 82 { key: 'service', label: 'Service' }, 83 { key: 'system', label: 'System' }, 84 ]; 85 } 86 87 @computed 88 get optionsStatus() { 89 return [ 90 { key: 'pending', label: 'Pending' }, 91 { key: 'running', label: 'Running' }, 92 { key: 'dead', label: 'Dead' }, 93 ]; 94 } 95 96 @computed('selectionDatacenter', 'visibleJobs.[]') 97 get optionsDatacenter() { 98 const flatten = (acc, val) => acc.concat(val); 99 const allDatacenters = new Set(this.visibleJobs.mapBy('datacenters').reduce(flatten, [])); 100 101 // Remove any invalid datacenters from the query param/selection 102 const availableDatacenters = Array.from(allDatacenters).compact(); 103 scheduleOnce('actions', () => { 104 // eslint-disable-next-line ember/no-side-effects 105 this.set( 106 'qpDatacenter', 107 serialize(intersection(availableDatacenters, this.selectionDatacenter)) 108 ); 109 }); 110 111 return availableDatacenters.sort().map(dc => ({ key: dc, label: dc })); 112 } 113 114 @computed('selectionPrefix', 'visibleJobs.[]') 115 get optionsPrefix() { 116 // A prefix is defined as the start of a job name up to the first - or . 117 // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds 118 const hasPrefix = /.[-._]/; 119 120 // Collect and count all the prefixes 121 const allNames = this.visibleJobs.mapBy('name'); 122 const nameHistogram = allNames.reduce((hist, name) => { 123 if (hasPrefix.test(name)) { 124 const prefix = name.match(/(.+?)[-._]/)[1]; 125 hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1; 126 } 127 return hist; 128 }, {}); 129 130 // Convert to an array 131 const nameTable = Object.keys(nameHistogram).map(key => ({ 132 prefix: key, 133 count: nameHistogram[key], 134 })); 135 136 // Only consider prefixes that match more than one name 137 const prefixes = nameTable.filter(name => name.count > 1); 138 139 // Remove any invalid prefixes from the query param/selection 140 const availablePrefixes = prefixes.mapBy('prefix'); 141 scheduleOnce('actions', () => { 142 // eslint-disable-next-line ember/no-side-effects 143 this.set('qpPrefix', serialize(intersection(availablePrefixes, this.selectionPrefix))); 144 }); 145 146 // Sort, format, and include the count in the label 147 return prefixes.sortBy('prefix').map(name => ({ 148 key: name.prefix, 149 label: `${name.prefix} (${name.count})`, 150 })); 151 } 152 153 /** 154 Visible jobs are those that match the selected namespace and aren't children 155 of periodic or parameterized jobs. 156 */ 157 @computed('model.@each.parent', 'system.{activeNamespace.id,namespaces.length}') 158 get visibleJobs() { 159 // Namespace related properties are ommitted from the dependent keys 160 // due to a prop invalidation bug caused by region switching. 161 const hasNamespaces = this.get('system.namespaces.length'); 162 const activeNamespace = this.get('system.activeNamespace.id') || 'default'; 163 164 return this.model 165 .compact() 166 .filter(job => !job.isNew) 167 .filter(job => !hasNamespaces || job.get('namespace.id') === activeNamespace) 168 .filter(job => !job.get('parent.content')); 169 } 170 171 @computed( 172 'visibleJobs.[]', 173 'selectionType', 174 'selectionStatus', 175 'selectionDatacenter', 176 'selectionPrefix' 177 ) 178 get filteredJobs() { 179 const { 180 selectionType: types, 181 selectionStatus: statuses, 182 selectionDatacenter: datacenters, 183 selectionPrefix: prefixes, 184 } = this; 185 186 // A job must match ALL filter facets, but it can match ANY selection within a facet 187 // Always return early to prevent unnecessary facet predicates. 188 return this.visibleJobs.filter(job => { 189 if (types.length && !types.includes(job.get('displayType'))) { 190 return false; 191 } 192 193 if (statuses.length && !statuses.includes(job.get('status'))) { 194 return false; 195 } 196 197 if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) { 198 return false; 199 } 200 201 const name = job.get('name'); 202 if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) { 203 return false; 204 } 205 206 return true; 207 }); 208 } 209 210 @alias('filteredJobs') listToSort; 211 @alias('listSorted') listToSearch; 212 @alias('listSearched') sortedJobs; 213 214 isShowingDeploymentDetails = false; 215 216 setFacetQueryParam(queryParam, selection) { 217 this.set(queryParam, serialize(selection)); 218 } 219 220 @action 221 gotoJob(job) { 222 this.transitionToRoute('jobs.job', job.get('plainId')); 223 } 224 }