-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5236 from hashicorp/f-ui-jobs-filtering
UI: Faceted search on the jobs list page
- Loading branch information
Showing
8 changed files
with
499 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,28 @@ import { inject as service } from '@ember/service'; | |
import { alias } from '@ember/object/computed'; | ||
import Controller, { inject as controller } from '@ember/controller'; | ||
import { computed } from '@ember/object'; | ||
import { scheduleOnce } from '@ember/runloop'; | ||
import intersection from 'lodash.intersection'; | ||
import Sortable from 'nomad-ui/mixins/sortable'; | ||
import Searchable from 'nomad-ui/mixins/searchable'; | ||
|
||
// An unattractive but robust way to encode query params | ||
const qpSerialize = arr => (arr.length ? JSON.stringify(arr) : ''); | ||
const qpDeserialize = str => { | ||
try { | ||
return JSON.parse(str) | ||
.compact() | ||
.without(''); | ||
} catch (e) { | ||
return []; | ||
} | ||
}; | ||
|
||
const qpSelection = qpKey => | ||
computed(qpKey, function() { | ||
return qpDeserialize(this.get(qpKey)); | ||
}); | ||
|
||
export default Controller.extend(Sortable, Searchable, { | ||
system: service(), | ||
jobsController: controller('jobs'), | ||
|
@@ -16,6 +35,10 @@ export default Controller.extend(Sortable, Searchable, { | |
searchTerm: 'search', | ||
sortProperty: 'sort', | ||
sortDescending: 'desc', | ||
qpType: 'type', | ||
qpStatus: 'status', | ||
qpDatacenter: 'dc', | ||
qpPrefix: 'prefix', | ||
}, | ||
|
||
currentPage: 1, | ||
|
@@ -28,11 +51,95 @@ export default Controller.extend(Sortable, Searchable, { | |
fuzzySearchProps: computed(() => ['name']), | ||
fuzzySearchEnabled: true, | ||
|
||
qpType: '', | ||
qpStatus: '', | ||
qpDatacenter: '', | ||
qpPrefix: '', | ||
|
||
selectionType: qpSelection('qpType'), | ||
selectionStatus: qpSelection('qpStatus'), | ||
selectionDatacenter: qpSelection('qpDatacenter'), | ||
selectionPrefix: qpSelection('qpPrefix'), | ||
|
||
optionsType: computed(() => [ | ||
{ key: 'batch', label: 'Batch' }, | ||
{ key: 'parameterized', label: 'Parameterized' }, | ||
{ key: 'periodic', label: 'Periodic' }, | ||
{ key: 'service', label: 'Service' }, | ||
{ key: 'system', label: 'System' }, | ||
]), | ||
|
||
optionsStatus: computed(() => [ | ||
{ key: 'pending', label: 'Pending' }, | ||
{ key: 'running', label: 'Running' }, | ||
{ key: 'dead', label: 'Dead' }, | ||
]), | ||
|
||
optionsDatacenter: computed('visibleJobs.[]', function() { | ||
const flatten = (acc, val) => acc.concat(val); | ||
const allDatacenters = new Set( | ||
this.get('visibleJobs') | ||
.mapBy('datacenters') | ||
.reduce(flatten, []) | ||
); | ||
|
||
// Remove any invalid datacenters from the query param/selection | ||
const availableDatacenters = Array.from(allDatacenters).compact(); | ||
scheduleOnce('actions', () => { | ||
this.set( | ||
'qpDatacenter', | ||
qpSerialize(intersection(availableDatacenters, this.get('selectionDatacenter'))) | ||
); | ||
}); | ||
|
||
return availableDatacenters.sort().map(dc => ({ key: dc, label: dc })); | ||
}), | ||
|
||
optionsPrefix: computed('visibleJobs.[]', function() { | ||
// A prefix is defined as the start of a job name up to the first - or . | ||
// ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds | ||
const hasPrefix = /.[-._]/; | ||
|
||
// Collect and count all the prefixes | ||
const allNames = this.get('visibleJobs').mapBy('name'); | ||
const nameHistogram = allNames.reduce((hist, name) => { | ||
if (hasPrefix.test(name)) { | ||
const prefix = name.match(/(.+?)[-.]/)[1]; | ||
hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1; | ||
} | ||
return hist; | ||
}, {}); | ||
|
||
// Convert to an array | ||
const nameTable = Object.keys(nameHistogram).map(key => ({ | ||
prefix: key, | ||
count: nameHistogram[key], | ||
})); | ||
|
||
// Only consider prefixes that match more than one name | ||
const prefixes = nameTable.filter(name => name.count > 1); | ||
|
||
// Remove any invalid prefixes from the query param/selection | ||
const availablePrefixes = prefixes.mapBy('prefix'); | ||
scheduleOnce('actions', () => { | ||
this.set( | ||
'qpPrefix', | ||
qpSerialize(intersection(availablePrefixes, this.get('selectionPrefix'))) | ||
); | ||
}); | ||
|
||
// Sort, format, and include the count in the label | ||
return prefixes.sortBy('prefix').map(name => ({ | ||
key: name.prefix, | ||
label: `${name.prefix} (${name.count})`, | ||
})); | ||
}), | ||
|
||
/** | ||
Filtered jobs are those that match the selected namespace and aren't children | ||
Visible jobs are those that match the selected namespace and aren't children | ||
of periodic or parameterized jobs. | ||
*/ | ||
filteredJobs: computed('model.[]', '[email protected]', function() { | ||
visibleJobs: computed('model.[]', '[email protected]', function() { | ||
// Namespace related properties are ommitted from the dependent keys | ||
// due to a prop invalidation bug caused by region switching. | ||
const hasNamespaces = this.get('system.namespaces.length'); | ||
|
@@ -44,12 +151,60 @@ export default Controller.extend(Sortable, Searchable, { | |
.filter(job => !job.get('parent.content')); | ||
}), | ||
|
||
filteredJobs: computed( | ||
'visibleJobs.[]', | ||
'selectionType', | ||
'selectionStatus', | ||
'selectionDatacenter', | ||
'selectionPrefix', | ||
function() { | ||
const { | ||
selectionType: types, | ||
selectionStatus: statuses, | ||
selectionDatacenter: datacenters, | ||
selectionPrefix: prefixes, | ||
} = this.getProperties( | ||
'selectionType', | ||
'selectionStatus', | ||
'selectionDatacenter', | ||
'selectionPrefix' | ||
); | ||
|
||
// A job must match ALL filter facets, but it can match ANY selection within a facet | ||
// Always return early to prevent unnecessary facet predicates. | ||
return this.get('visibleJobs').filter(job => { | ||
if (types.length && !types.includes(job.get('displayType'))) { | ||
return false; | ||
} | ||
|
||
if (statuses.length && !statuses.includes(job.get('status'))) { | ||
return false; | ||
} | ||
|
||
if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) { | ||
return false; | ||
} | ||
|
||
const name = job.get('name'); | ||
if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) { | ||
return false; | ||
} | ||
|
||
return true; | ||
}); | ||
} | ||
), | ||
|
||
listToSort: alias('filteredJobs'), | ||
listToSearch: alias('listSorted'), | ||
sortedJobs: alias('listSearched'), | ||
|
||
isShowingDeploymentDetails: false, | ||
|
||
setFacetQueryParam(queryParam, selection) { | ||
this.set(queryParam, qpSerialize(selection)); | ||
}, | ||
|
||
actions: { | ||
gotoJob(job) { | ||
this.transitionToRoute('jobs.job', job.get('plainId')); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,4 +142,10 @@ | |
} | ||
} | ||
} | ||
|
||
.dropdown-empty { | ||
display: block; | ||
padding: 8px 12px; | ||
color: $grey-light; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.