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