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