Skip to content

Commit

Permalink
Add phase 1 of global search (#8175)
Browse files Browse the repository at this point in the history
This introduces a DataCaches service so recently-updated collections don’t need
to be requeried within a minute, or based on the current route. It only searches
jobs and nodes. There are known bugs that will be addressed in upcoming PRs.
  • Loading branch information
backspace authored Jun 19, 2020
1 parent 8ba4f72 commit 62abb11
Show file tree
Hide file tree
Showing 14 changed files with 485 additions and 7 deletions.
172 changes: 172 additions & 0 deletions ui/app/components/global-search/control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { task } from 'ember-concurrency';
import EmberObject, { action, computed, set } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { run } from '@ember/runloop';
import Searchable from 'nomad-ui/mixins/searchable';
import classic from 'ember-classic-decorator';

const SLASH_KEY = 191;

@tagName('')
export default class GlobalSearchControl extends Component {
@service dataCaches;
@service router;
@service store;

searchString = null;

constructor() {
super(...arguments);

this.jobSearch = JobSearch.create({
dataSource: this,
});

this.nodeSearch = NodeSearch.create({
dataSource: this,
});
}

keyDownHandler(e) {
const targetElementName = e.target.nodeName.toLowerCase();

if (targetElementName != 'input' && targetElementName != 'textarea') {
if (e.keyCode === SLASH_KEY) {
e.preventDefault();
this.open();
}
}
}

didInsertElement() {
this.set('_keyDownHandler', this.keyDownHandler.bind(this));
document.addEventListener('keydown', this._keyDownHandler);
}

willDestroyElement() {
document.removeEventListener('keydown', this._keyDownHandler);
}

@task(function*(string) {
try {
set(this, 'searchString', string);

const jobs = yield this.dataCaches.fetch('job');
const nodes = yield this.dataCaches.fetch('node');

set(this, 'jobs', jobs.toArray());
set(this, 'nodes', nodes.toArray());

const jobResults = this.jobSearch.listSearched;
const nodeResults = this.nodeSearch.listSearched;

return [
{
groupName: `Jobs (${jobResults.length})`,
options: jobResults,
},
{
groupName: `Clients (${nodeResults.length})`,
options: nodeResults,
},
];
} catch (e) {
// eslint-disable-next-line
console.log('exception searching', e);
}
})
search;

@action
open() {
if (this.select) {
this.select.actions.open();
}
}

@action
selectOption(model) {
const itemModelName = model.constructor.modelName;

if (itemModelName === 'job') {
this.router.transitionTo('jobs.job', model.name, {
queryParams: { namespace: model.get('namespace.name') },
});
} else if (itemModelName === 'node') {
this.router.transitionTo('clients.client', model.id);
}
}

@action
storeSelect(select) {
if (select) {
this.select = select;
}
}

@action
openOnClickOrTab(select, { target }) {
// Bypass having to press enter to access search after clicking/tabbing
const targetClassList = target.classList;
const targetIsTrigger = targetClassList.contains('ember-power-select-trigger');

// Allow tabbing out of search
const triggerIsNotActive = !targetClassList.contains('ember-power-select-trigger--active');

if (targetIsTrigger && triggerIsNotActive) {
run.next(() => {
select.actions.open();
});
}
}

calculatePosition(trigger) {
const { top, left, width } = trigger.getBoundingClientRect();
return {
style: {
left,
width,
top,
},
};
}
}

@classic
class JobSearch extends EmberObject.extend(Searchable) {
@computed
get searchProps() {
return ['id', 'name'];
}

@computed
get fuzzySearchProps() {
return ['name'];
}

@alias('dataSource.jobs') listToSearch;
@alias('dataSource.searchString') searchTerm;

fuzzySearchEnabled = true;
}

@classic
class NodeSearch extends EmberObject.extend(Searchable) {
@computed
get searchProps() {
return ['id', 'name'];
}

@computed
get fuzzySearchProps() {
return ['name'];
}

@alias('dataSource.nodes') listToSearch;
@alias('dataSource.searchString') searchTerm;

fuzzySearchEnabled = true;
}
33 changes: 33 additions & 0 deletions ui/app/services/data-caches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Service, { inject as service } from '@ember/service';

export const COLLECTION_CACHE_DURATION = 60000; // one minute

export default class DataCachesService extends Service {
@service router;
@service store;
@service system;

collectionLastFetched = {};

async fetch(modelName) {
const modelNameToRoute = {
job: 'jobs',
node: 'clients',
};

const route = modelNameToRoute[modelName];
const lastFetched = this.collectionLastFetched[modelName];
const now = Date.now();

if (this.router.isActive(route)) {
// TODO Incorrect because it’s constantly being fetched by watchers, shouldn’t be marked as last fetched only on search
this.collectionLastFetched[modelName] = now;
return this.store.peekAll(modelName);
} else if (lastFetched && now - lastFetched < COLLECTION_CACHE_DURATION) {
return this.store.peekAll(modelName);
} else {
this.collectionLastFetched[modelName] = now;
return this.store.findAll(modelName);
}
}
}
2 changes: 2 additions & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
@import './components/exec-button';
@import './components/exec-window';
@import './components/fs-explorer';
@import './components/global-search-control';
@import './components/global-search-dropdown';
@import './components/gutter';
@import './components/gutter-toggle';
@import './components/image-file.scss';
Expand Down
37 changes: 37 additions & 0 deletions ui/app/styles/components/global-search-control.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.global-search {
width: 30em;

.ember-power-select-trigger {
background: $nomad-green-darker;

.icon {
margin-top: 1px;
margin-left: 2px;

fill: white;
opacity: 0.7;
}

.placeholder {
opacity: 0.7;
display: inline-block;
padding-left: 2px;
transform: translateY(-1px);
}

&.ember-power-select-trigger--active {
background: white;

.icon {
fill: black;
opacity: 1;
}
}
}

.ember-basic-dropdown-content-wormhole-origin {
position: absolute;
top: 0;
width: 100%;
}
}
45 changes: 45 additions & 0 deletions ui/app/styles/components/global-search-dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.global-search-dropdown {
background: transparent;
border: 0;
position: fixed;

.ember-power-select-search {
margin-left: $icon-dimensions;
border: 0;
}

input,
input:focus {
background: transparent;
border: 0;
outline: 0;
}

.ember-power-select-options {
background: white;
padding: 0.35rem;

&[role='listbox'] {
border: 1px solid $grey-blue;
box-shadow: 0 6px 8px -2px rgba($black, 0.05), 0 8px 4px -4px rgba($black, 0.1);
}

.ember-power-select-option {
padding: 0.2rem 0.4rem;
border-radius: $radius;

&[aria-current='true'] {
background: transparentize($blue, 0.8);
color: $blue;
}
}
}

.ember-power-select-group-name {
text-transform: uppercase;
display: inline;
color: darken($grey-blue, 20%);
font-size: $size-7;
font-weight: $weight-semibold;
}
}
7 changes: 4 additions & 3 deletions ui/app/styles/core/navbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
padding-left: 20px;
padding-right: 20px;
overflow: hidden;
align-items: center;
justify-content: space-between;

.navbar-item {
color: rgba($primary-invert, 0.8);
Expand Down Expand Up @@ -35,7 +37,6 @@
display: block;
position: absolute;
left: 0px;
top: 1.25em;
}
}
}
Expand All @@ -44,7 +45,7 @@
display: flex;
align-items: stretch;
justify-content: flex-end;
margin-left: auto;
margin-left: inherit;
}

.navbar-end > a.navbar-item {
Expand Down Expand Up @@ -100,7 +101,7 @@
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
margin-left: inherit;
}

.navbar-end > a.navbar-item {
Expand Down
3 changes: 3 additions & 0 deletions ui/app/templates/components/global-header.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
{{partial "partials/nomad-logo"}}
</LinkTo>
</div>
{{#unless (media "isMobile")}}
<GlobalSearch::Control />
{{/unless}}
<div class="navbar-end">
<a href="https://nomadproject.io/docs" class="navbar-item">Documentation</a>
<LinkTo @route="settings.tokens" class="navbar-item">ACL Tokens</LinkTo>
Expand Down
16 changes: 16 additions & 0 deletions ui/app/templates/components/global-search/control.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<PowerSelect
@tagName="div"
class="global-search"
data-test-search
@searchEnabled={{true}}
@search={{perform this.search}}
@onChange={{action 'selectOption'}}
@onFocus={{action 'openOnClickOrTab'}}
@dropdownClass="global-search-dropdown"
@calculatePosition={{this.calculatePosition}}
@searchMessageComponent="global-search/message"
@triggerComponent="global-search/trigger"
@registerAPI={{action 'storeSelect'}}
as |option|>
{{option.name}}
</PowerSelect>
Empty file.
4 changes: 4 additions & 0 deletions ui/app/templates/components/global-search/trigger.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{x-icon "search" class="is-small"}}
{{#unless select.isOpen}}
<span class='placeholder'>Search</span>
{{/unless}}
4 changes: 2 additions & 2 deletions ui/tests/acceptance/jobs-list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ module('Acceptance | jobs list', function(hooks) {

await JobsList.visit();

await JobsList.search('dog');
await JobsList.search.fillIn('dog');
assert.ok(JobsList.isEmpty, 'The empty message is shown');
assert.equal(JobsList.emptyState.headline, 'No Matches', 'The message is appropriate');
});
Expand All @@ -168,7 +168,7 @@ module('Acceptance | jobs list', function(hooks) {

assert.equal(currentURL(), '/jobs?page=2', 'Page query param captures page=2');

await JobsList.search('foobar');
await JobsList.search.fillIn('foobar');

assert.equal(currentURL(), '/jobs?search=foobar', 'No page query param');
});
Expand Down
Loading

0 comments on commit 62abb11

Please sign in to comment.