Skip to content

Commit

Permalink
Merge pull request #5235 from hashicorp/f-ui-faceted-search
Browse files Browse the repository at this point in the history
UI: Faceted Search
  • Loading branch information
DingoEatingFuzz authored Mar 12, 2019
2 parents a2130bc + 8c5d064 commit cba97c2
Show file tree
Hide file tree
Showing 32 changed files with 1,669 additions and 21 deletions.
9 changes: 9 additions & 0 deletions ui/app/breakpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Breakpoints are meant to match Bulma's breakpoints and any additional custom breakpoints
// https://github.com/jgthms/bulma/blob/6ad2e3df0589e5d6ff7a9c03ee1c78a546bedeaf/sass/utilities/initial-variables.sass#L48-L59
// https://github.com/jgthms/bulma/blob/6ad2e3df0589e5d6ff7a9c03ee1c78a546bedeaf/sass/utilities/mixins.sass#L71-L130
export default {
mobile: '(max-width: 768px)',
tablet: '(min-width: 769px)',
desktop: '(min-width: 1088px)',
gutterless: '(max-width: 960px)',
};
45 changes: 45 additions & 0 deletions ui/app/components/freestyle/sg-multi-select-dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
options1: computed(() => [
{ key: 'option-1', label: 'Option One' },
{ key: 'option-2', label: 'Option Two' },
{ key: 'option-3', label: 'Option Three' },
{ key: 'option-4', label: 'Option Four' },
{ key: 'option-5', label: 'Option Five' },
]),

selection1: computed(() => ['option-2', 'option-4', 'option-5']),

optionsMany: computed(() =>
Array(100)
.fill(null)
.map((_, i) => ({ label: `Option ${i}`, key: `option-${i}` }))
),
selectionMany: computed(() => []),

optionsDatacenter: computed(() => [
{ key: 'pdx-1', label: 'pdx-1' },
{ key: 'jfk-1', label: 'jfk-1' },
{ key: 'jfk-2', label: 'jfk-2' },
{ key: 'muc-1', label: 'muc-1' },
]),
selectionDatacenter: computed(() => ['jfk-1', 'jfk-2']),

optionsType: computed(() => [
{ key: 'batch', label: 'Batch' },
{ key: 'service', label: 'Service' },
{ key: 'system', label: 'System' },
{ key: 'periodic', label: 'Periodic' },
{ key: 'parameterized', label: 'Parameterized' },
]),
selectionType: computed(() => ['system', 'service']),

optionsStatus: computed(() => [
{ key: 'pending', label: 'Pending' },
{ key: 'running', label: 'Running' },
{ key: 'dead', label: 'Dead' },
]),
selectionStatus: computed(() => []),
});
98 changes: 98 additions & 0 deletions ui/app/components/multi-select-dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { run } from '@ember/runloop';

const TAB = 9;
const ESC = 27;
const SPACE = 32;
const ARROW_UP = 38;
const ARROW_DOWN = 40;

export default Component.extend({
classNames: ['dropdown'],

options: computed(() => []),
selection: computed(() => []),

onSelect() {},

isOpen: false,
dropdown: null,

capture(dropdown) {
// It's not a good idea to grab a dropdown reference like this, but it's necessary
// in order to invoke dropdown.actions.close in traverseList as well as
// dropdown.actions.reposition when the label or selection length changes.
this.set('dropdown', dropdown);
},

didReceiveAttrs() {
const dropdown = this.get('dropdown');
if (this.get('isOpen') && dropdown) {
run.scheduleOnce('afterRender', () => {
dropdown.actions.reposition();
});
}
},

actions: {
toggle({ key }) {
const newSelection = this.get('selection').slice();
if (newSelection.includes(key)) {
newSelection.removeObject(key);
} else {
newSelection.addObject(key);
}
this.get('onSelect')(newSelection);
},

openOnArrowDown(dropdown, e) {
this.capture(dropdown);

if (!this.get('isOpen') && e.keyCode === ARROW_DOWN) {
dropdown.actions.open(e);
e.preventDefault();
} else if (this.get('isOpen') && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns');
const firstElement = document.querySelector(`#${optionsId} .dropdown-option`);

if (firstElement) {
firstElement.focus();
e.preventDefault();
}
}
},

traverseList(option, e) {
if (e.keyCode === ESC) {
// Close the dropdown
const dropdown = this.get('dropdown');
if (dropdown) {
dropdown.actions.close(e);
// Return focus to the trigger so tab works as expected
const trigger = this.element.querySelector('.dropdown-trigger');
if (trigger) trigger.focus();
e.preventDefault();
this.set('dropdown', null);
}
} else if (e.keyCode === ARROW_UP) {
// previous item
const prev = e.target.previousElementSibling;
if (prev) {
prev.focus();
e.preventDefault();
}
} else if (e.keyCode === ARROW_DOWN) {
// next item
const next = e.target.nextElementSibling;
if (next) {
next.focus();
e.preventDefault();
}
} else if (e.keyCode === SPACE) {
this.send('toggle', option);
e.preventDefault();
}
},
},
});
5 changes: 5 additions & 0 deletions ui/app/components/search-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export default Component.extend({
this.set('_searchTerm', e.target.value);
run.debounce(this, updateSearch, this.get('debounce'));
},

clear() {
this.set('_searchTerm', '');
run.debounce(this, updateSearch, this.get('debounce'));
},
},
});

Expand Down
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
Loading

0 comments on commit cba97c2

Please sign in to comment.