Skip to content

Commit

Permalink
Merge pull request #5318 from hashicorp/f-ui-clients-filtering
Browse files Browse the repository at this point in the history
UI: Clients filtering
  • Loading branch information
DingoEatingFuzz authored Feb 14, 2019
2 parents 83d9190 + 96bae7a commit f29c3e8
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 32 deletions.
94 changes: 93 additions & 1 deletion ui/app/controllers/clients/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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';
import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize';

export default Controller.extend(Sortable, Searchable, {
clientsController: controller('clients'),
Expand All @@ -15,6 +18,10 @@ export default Controller.extend(Sortable, Searchable, {
searchTerm: 'search',
sortProperty: 'sort',
sortDescending: 'desc',
qpClass: 'class',
qpStatus: 'status',
qpDatacenter: 'dc',
qpFlags: 'flags',
},

currentPage: 1,
Expand All @@ -25,12 +32,97 @@ export default Controller.extend(Sortable, Searchable, {

searchProps: computed(() => ['id', 'name', 'datacenter']),

listToSort: alias('nodes'),
qpClass: '',
qpStatus: '',
qpDatacenter: '',
qpFlags: '',

selectionClass: selection('qpClass'),
selectionStatus: selection('qpStatus'),
selectionDatacenter: selection('qpDatacenter'),
selectionFlags: selection('qpFlags'),

optionsClass: computed('nodes.[]', function() {
const classes = Array.from(new Set(this.get('nodes').mapBy('nodeClass'))).compact();

// Remove any invalid node classes from the query param/selection
scheduleOnce('actions', () => {
this.set('qpClass', serialize(intersection(classes, this.get('selectionClass'))));
});

return classes.sort().map(dc => ({ key: dc, label: dc }));
}),

optionsStatus: computed(() => [
{ key: 'initializing', label: 'Initializing' },
{ key: 'ready', label: 'Ready' },
{ key: 'down', label: 'Down' },
]),

optionsDatacenter: computed('nodes.[]', function() {
const datacenters = Array.from(new Set(this.get('nodes').mapBy('datacenter'))).compact();

// Remove any invalid datacenters from the query param/selection
scheduleOnce('actions', () => {
this.set(
'qpDatacenter',
serialize(intersection(datacenters, this.get('selectionDatacenter')))
);
});

return datacenters.sort().map(dc => ({ key: dc, label: dc }));
}),

optionsFlags: computed(() => [
{ key: 'ineligible', label: 'Ineligible' },
{ key: 'draining', label: 'Draining' },
]),

filteredNodes: computed(
'nodes.[]',
'selectionClass',
'selectionStatus',
'selectionDatacenter',
'selectionFlags',
function() {
const {
selectionClass: classes,
selectionStatus: statuses,
selectionDatacenter: datacenters,
selectionFlags: flags,
} = this.getProperties(
'selectionClass',
'selectionStatus',
'selectionDatacenter',
'selectionFlags'
);

const onlyIneligible = flags.includes('ineligible');
const onlyDraining = flags.includes('draining');

return this.get('nodes').filter(node => {
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
if (statuses.length && !statuses.includes(node.get('status'))) return false;
if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false;

if (onlyIneligible && node.get('isEligible')) return false;
if (onlyDraining && !node.get('isDraining')) return false;

return true;
});
}
),

listToSort: alias('filteredNodes'),
listToSearch: alias('listSorted'),
sortedNodes: alias('listSearched'),

isForbidden: alias('clientsController.isForbidden'),

setFacetQueryParam(queryParam, selection) {
this.set(queryParam, serialize(selection));
},

actions: {
gotoNode(node) {
this.transitionToRoute('clients.client', node);
Expand Down
35 changes: 8 additions & 27 deletions ui/app/controllers/jobs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,7 @@ 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));
});
import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize';

export default Controller.extend(Sortable, Searchable, {
system: service(),
Expand Down Expand Up @@ -56,10 +40,10 @@ export default Controller.extend(Sortable, Searchable, {
qpDatacenter: '',
qpPrefix: '',

selectionType: qpSelection('qpType'),
selectionStatus: qpSelection('qpStatus'),
selectionDatacenter: qpSelection('qpDatacenter'),
selectionPrefix: qpSelection('qpPrefix'),
selectionType: selection('qpType'),
selectionStatus: selection('qpStatus'),
selectionDatacenter: selection('qpDatacenter'),
selectionPrefix: selection('qpPrefix'),

optionsType: computed(() => [
{ key: 'batch', label: 'Batch' },
Expand Down Expand Up @@ -88,7 +72,7 @@ export default Controller.extend(Sortable, Searchable, {
scheduleOnce('actions', () => {
this.set(
'qpDatacenter',
qpSerialize(intersection(availableDatacenters, this.get('selectionDatacenter')))
serialize(intersection(availableDatacenters, this.get('selectionDatacenter')))
);
});

Expand Down Expand Up @@ -122,10 +106,7 @@ export default Controller.extend(Sortable, Searchable, {
// 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')))
);
this.set('qpPrefix', serialize(intersection(availablePrefixes, this.get('selectionPrefix'))));
});

// Sort, format, and include the count in the label
Expand Down Expand Up @@ -202,7 +183,7 @@ export default Controller.extend(Sortable, Searchable, {
isShowingDeploymentDetails: false,

setFacetQueryParam(queryParam, selection) {
this.set(queryParam, qpSerialize(selection));
this.set(queryParam, serialize(selection));
},

actions: {
Expand Down
1 change: 1 addition & 0 deletions ui/app/models/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default Model.extend({
// Available from list response
name: attr('string'),
datacenter: attr('string'),
nodeClass: attr('string'),
isDraining: attr('boolean'),
schedulingEligibility: attr('string'),
status: attr('string'),
Expand Down
41 changes: 37 additions & 4 deletions ui/app/templates/clients/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,44 @@
{{#if isForbidden}}
{{partial "partials/forbidden-message"}}
{{else}}
{{#if nodes.length}}
<div class="content">
<div>
<div class="columns">
{{#if nodes.length}}
<div class="column is-one-third">
{{search-box
searchTerm=(mut searchTerm)
onChange=(action resetPagination)
placeholder="Search clients..."}}
</div>
{{/if}}
<div class="column is-centered">
<div class="button-bar is-pulled-right">
{{multi-select-dropdown
data-test-class-facet
label="Class"
options=optionsClass
selection=selectionClass
onSelect=(action setFacetQueryParam "qpClass")}}
{{multi-select-dropdown
data-test-status-facet
label="Status"
options=optionsStatus
selection=selectionStatus
onSelect=(action setFacetQueryParam "qpStatus")}}
{{multi-select-dropdown
data-test-datacenter-facet
label="Datacenter"
options=optionsDatacenter
selection=selectionDatacenter
onSelect=(action setFacetQueryParam "qpDatacenter")}}
{{multi-select-dropdown
data-test-flags-facet
label="Flags"
options=optionsFlags
selection=selectionFlags
onSelect=(action setFacetQueryParam "qpFlags")}}
</div>
</div>
{{/if}}
</div>
{{#list-pagination
source=sortedNodes
size=pageSize
Expand Down Expand Up @@ -53,6 +81,11 @@
<p class="empty-message-body">
The cluster currently has no client nodes.
</p>
{{else if (eq filteredNodes.length 0)}}
<h3 data-test-empty-clients-list-headline class="empty-message-headline">No Matches</h3>
<p class="empty-message-body">
No clients match your current filter selection.
</p>
{{else if searchTerm}}
<h3 class="empty-message-headline" data-test-empty-clients-list-headline>No Matches</h3>
<p class="empty-message-body">No clients match the term <strong>{{searchTerm}}</strong></p>
Expand Down
20 changes: 20 additions & 0 deletions ui/app/utils/qp-serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { computed } from '@ember/object';

// An unattractive but robust way to encode query params
export const serialize = arr => (arr.length ? JSON.stringify(arr) : '');

export const deserialize = str => {
try {
return JSON.parse(str)
.compact()
.without('');
} catch (e) {
return [];
}
};

// A computed property macro for deserializing a query param
export const deserializedQueryParam = qpKey =>
computed(qpKey, function() {
return deserialize(this.get(qpKey));
});
2 changes: 2 additions & 0 deletions ui/mirage/factories/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import moment from 'moment';

const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
const NODE_STATUSES = ['initializing', 'ready', 'down'];
const NODE_CLASSES = provide(7, faker.company.bsBuzz.bind(faker.company));
const REF_DATE = new Date();

export default Factory.extend({
id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
name: i => `nomad@${HOSTS[i % HOSTS.length]}`,

datacenter: faker.list.random(...DATACENTERS),
nodeClass: faker.list.random(...NODE_CLASSES),
drain: faker.random.boolean,
status: faker.list.random(...NODE_STATUSES),
tls_enabled: faker.random.boolean,
Expand Down
Loading

0 comments on commit f29c3e8

Please sign in to comment.