From e919f00287fc4a97ded706a6b30ab5ba20c0f128 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Mon, 15 Jun 2020 14:48:30 -0500
Subject: [PATCH 01/24] Add inert search components

---
 ui/app/components/global-search/control.js    | 23 ++++++++++++
 ui/app/styles/components.scss                 |  2 +
 .../components/global-search-control.scss     | 37 +++++++++++++++++++
 .../components/global-search-dropdown.scss    | 32 ++++++++++++++++
 ui/app/styles/core/navbar.scss                |  8 ++--
 ui/app/templates/components/global-header.hbs |  1 +
 .../components/global-search/control.hbs      | 13 +++++++
 .../components/global-search/message.hbs      |  0
 .../components/global-search/trigger.hbs      |  4 ++
 9 files changed, 117 insertions(+), 3 deletions(-)
 create mode 100644 ui/app/components/global-search/control.js
 create mode 100644 ui/app/styles/components/global-search-control.scss
 create mode 100644 ui/app/styles/components/global-search-dropdown.scss
 create mode 100644 ui/app/templates/components/global-search/control.hbs
 create mode 100644 ui/app/templates/components/global-search/message.hbs
 create mode 100644 ui/app/templates/components/global-search/trigger.hbs

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
new file mode 100644
index 00000000000..a37efbcf99a
--- /dev/null
+++ b/ui/app/components/global-search/control.js
@@ -0,0 +1,23 @@
+import Component from '@ember/component';
+import { tagName } from '@ember-decorators/component';
+import { task } from 'ember-concurrency';
+import { action } from '@ember/object';
+
+@tagName('')
+export default class GlobalSearchControl extends Component {
+  @task(function*() {})
+  search;
+
+  @action select() {}
+
+  calculatePosition(trigger) {
+    const { top, left, width } = trigger.getBoundingClientRect();
+    return {
+      style: {
+        left,
+        width,
+        top,
+      },
+    };
+  }
+}
diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss
index 1bfd76d9b42..c0f81710ec7 100644
--- a/ui/app/styles/components.scss
+++ b/ui/app/styles/components.scss
@@ -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';
diff --git a/ui/app/styles/components/global-search-control.scss b/ui/app/styles/components/global-search-control.scss
new file mode 100644
index 00000000000..e42243b5021
--- /dev/null
+++ b/ui/app/styles/components/global-search-control.scss
@@ -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%;
+  }
+}
diff --git a/ui/app/styles/components/global-search-dropdown.scss b/ui/app/styles/components/global-search-dropdown.scss
new file mode 100644
index 00000000000..adbeaec857f
--- /dev/null
+++ b/ui/app/styles/components/global-search-dropdown.scss
@@ -0,0 +1,32 @@
+.global-search-dropdown {
+  background: transparent;
+  border: 0;
+  position: fixed;
+
+  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;
+      }
+    }
+  }
+}
diff --git a/ui/app/styles/core/navbar.scss b/ui/app/styles/core/navbar.scss
index 48a2a065d76..1ef5197921a 100644
--- a/ui/app/styles/core/navbar.scss
+++ b/ui/app/styles/core/navbar.scss
@@ -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);
@@ -35,7 +37,7 @@
           display: block;
           position: absolute;
           left: 0px;
-          top: 1.25em;
+          top: 1.25em; // FIXME positioning is off with .is-primary align-items: center
         }
       }
     }
@@ -44,7 +46,7 @@
       display: flex;
       align-items: stretch;
       justify-content: flex-end;
-      margin-left: auto;
+      margin-left: inherit;
     }
 
     .navbar-end > a.navbar-item {
@@ -100,7 +102,7 @@
       display: flex;
       align-items: center;
       justify-content: flex-end;
-      margin-left: auto;
+      margin-left: inherit;
     }
 
     .navbar-end > a.navbar-item {
diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs
index 23c5aa05ccf..4fe743ba46e 100644
--- a/ui/app/templates/components/global-header.hbs
+++ b/ui/app/templates/components/global-header.hbs
@@ -7,6 +7,7 @@
       {{partial "partials/nomad-logo"}}
     </LinkTo>
   </div>
+  {{global-search/control}}
   <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>
diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs
new file mode 100644
index 00000000000..c858a8883e4
--- /dev/null
+++ b/ui/app/templates/components/global-search/control.hbs
@@ -0,0 +1,13 @@
+  <PowerSelect
+    @tagName="div"
+    class="global-search"
+    data-test-search
+    @searchEnabled={{true}}
+    @search={{perform this.search}}
+    @onChange={{action 'select'}}
+    @dropdownClass="global-search-dropdown"
+    @calculatePosition={{this.calculatePosition}}
+    @searchMessageComponent="global-search/message"
+    @triggerComponent="global-search/trigger"
+    as |option|>
+  </PowerSelect>
\ No newline at end of file
diff --git a/ui/app/templates/components/global-search/message.hbs b/ui/app/templates/components/global-search/message.hbs
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ui/app/templates/components/global-search/trigger.hbs b/ui/app/templates/components/global-search/trigger.hbs
new file mode 100644
index 00000000000..aa52b18cd3b
--- /dev/null
+++ b/ui/app/templates/components/global-search/trigger.hbs
@@ -0,0 +1,4 @@
+{{x-icon "search" class="is-small"}}
+{{#unless select.isOpen}}
+  <span class='placeholder'>Search</span>
+{{/unless}}
\ No newline at end of file

From 73b6bb3bf565911c546cde15e149f125715fc021 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 09:24:39 -0500
Subject: [PATCH 02/24] Add preliminary job search
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This is an ungainly integration with the searchable mixin 😳
---
 ui/app/components/global-search/control.js    | 53 ++++++++++++++++++-
 .../components/global-search/control.hbs      |  1 +
 ui/tests/acceptance/search-test.js            | 28 ++++++++++
 ui/tests/pages/layout.js                      | 18 +++++++
 4 files changed, 98 insertions(+), 2 deletions(-)
 create mode 100644 ui/tests/acceptance/search-test.js

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index a37efbcf99a..ab2cfbcbdd2 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -1,11 +1,42 @@
 import Component from '@ember/component';
 import { tagName } from '@ember-decorators/component';
 import { task } from 'ember-concurrency';
-import { action } from '@ember/object';
+import EmberObject, { action, computed } from '@ember/object';
+import { alias } from '@ember/object/computed';
+import { inject as service } from '@ember/service';
+import Searchable from 'nomad-ui/mixins/searchable';
+import classic from 'ember-classic-decorator';
 
 @tagName('')
 export default class GlobalSearchControl extends Component {
-  @task(function*() {})
+  @service store;
+
+  searchString = null;
+
+  constructor() {
+    super(...arguments);
+
+    this.jobSearch = JobSearch.create({
+      something: this, // FIXME what’s a good name?
+    });
+  }
+
+  @task(function*(string) {
+    this.searchString = string;
+
+    // FIXME no need to fetch on every search!
+    const jobs = yield this.store.findAll('job');
+
+    this.jobs = jobs.toArray();
+
+    try {
+      const jobResults = this.jobSearch.listSearched;
+      return jobResults;
+    } catch (e) {
+      // eslint-disable-next-line
+      console.log('exception searching jobs', e);
+    }
+  })
   search;
 
   @action select() {}
@@ -21,3 +52,21 @@ export default class GlobalSearchControl extends Component {
     };
   }
 }
+
+@classic
+class JobSearch extends EmberObject.extend(Searchable) {
+  @computed
+  get searchProps() {
+    return ['id', 'name'];
+  }
+
+  @computed
+  get fuzzySearchProps() {
+    return ['name'];
+  }
+
+  @alias('something.jobs') listToSearch;
+  @alias('something.searchString') searchTerm;
+
+  fuzzySearchEnabled = true;
+}
diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs
index c858a8883e4..fc2f5f1202e 100644
--- a/ui/app/templates/components/global-search/control.hbs
+++ b/ui/app/templates/components/global-search/control.hbs
@@ -10,4 +10,5 @@
     @searchMessageComponent="global-search/message"
     @triggerComponent="global-search/trigger"
     as |option|>
+    {{option.name}}
   </PowerSelect>
\ No newline at end of file
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
new file mode 100644
index 00000000000..93215af7d3c
--- /dev/null
+++ b/ui/tests/acceptance/search-test.js
@@ -0,0 +1,28 @@
+import { module, test } from 'qunit';
+import { visit } from '@ember/test-helpers';
+import { setupApplicationTest } from 'ember-qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import PageLayout from 'nomad-ui/tests/pages/layout';
+import { selectSearch } from 'ember-power-select/test-support';
+
+module('Acceptance | search', function(hooks) {
+  setupApplicationTest(hooks);
+  setupMirage(hooks);
+
+  test('search searches jobs', async function(assert) {
+    server.create('node');
+    server.create('job', { id: 'vwxyz', namespaceId: 'default' });
+    server.create('job', { id: 'xyz', namespace: 'default' });
+    server.create('job', { id: 'abc', namespace: 'default' });
+
+    await visit('/');
+
+    await selectSearch(PageLayout.navbar.search.scope, 'xy');
+
+    PageLayout.navbar.search.as(search => {
+      assert.equal(search.options.length, 2);
+      assert.equal(search.options[0].text, 'xyz');
+      assert.equal(search.options[1].text, 'vwxyz');
+    });
+  });
+});
diff --git a/ui/tests/pages/layout.js b/ui/tests/pages/layout.js
index 257caf2e804..21a447fc0e6 100644
--- a/ui/tests/pages/layout.js
+++ b/ui/tests/pages/layout.js
@@ -12,6 +12,24 @@ export default create({
         label: text(),
       }),
     },
+
+    search: {
+      scope: '[data-test-search]',
+
+      click: clickable('.ember-power-select-trigger'),
+
+      options: collection('.ember-power-select-option', {
+        testContainer: '.ember-power-select-options',
+        resetScope: true,
+        label: text(),
+      }),
+
+      field: {
+        scope: '.ember-power-select-dropdown--active',
+        testContainer: 'html',
+        resetScope: true,
+      },
+    },
   },
 
   gutter: {

From 6b466790e9c1a8e4fb668a246e2839e598e90329 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 09:56:57 -0500
Subject: [PATCH 03/24] Remove need to activate search with enter

---
 ui/app/components/global-search/control.js      | 17 +++++++++++++++++
 .../components/global-search/control.hbs        |  1 +
 ui/tests/acceptance/search-test.js              | 10 ++++++++++
 ui/tests/pages/layout.js                        |  2 +-
 4 files changed, 29 insertions(+), 1 deletion(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index ab2cfbcbdd2..7f9319b5417 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -4,6 +4,7 @@ import { task } from 'ember-concurrency';
 import EmberObject, { action, computed } 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';
 
@@ -41,6 +42,22 @@ export default class GlobalSearchControl extends Component {
 
   @action 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 {
diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs
index fc2f5f1202e..759594085db 100644
--- a/ui/app/templates/components/global-search/control.hbs
+++ b/ui/app/templates/components/global-search/control.hbs
@@ -5,6 +5,7 @@
     @searchEnabled={{true}}
     @search={{perform this.search}}
     @onChange={{action 'select'}}
+    @onFocus={{action 'openOnClickOrTab'}}
     @dropdownClass="global-search-dropdown"
     @calculatePosition={{this.calculatePosition}}
     @searchMessageComponent="global-search/message"
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 93215af7d3c..f951fd429fd 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -25,4 +25,14 @@ module('Acceptance | search', function(hooks) {
       assert.equal(search.options[1].text, 'vwxyz');
     });
   });
+
+  test('clicking the search field starts search immediately', async function(assert) {
+    await visit('/');
+
+    assert.notOk(PageLayout.navbar.search.field.isPresent);
+
+    await PageLayout.navbar.search.click();
+
+    assert.ok(PageLayout.navbar.search.field.isPresent);
+  });
 });
diff --git a/ui/tests/pages/layout.js b/ui/tests/pages/layout.js
index 21a447fc0e6..f2d398f2b94 100644
--- a/ui/tests/pages/layout.js
+++ b/ui/tests/pages/layout.js
@@ -25,7 +25,7 @@ export default create({
       }),
 
       field: {
-        scope: '.ember-power-select-dropdown--active',
+        scope: '.ember-power-select-search input',
         testContainer: 'html',
         resetScope: true,
       },

From 8fca80e4a143b1130f90e99d23df36e2861eb43c Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 10:39:47 -0500
Subject: [PATCH 04/24] Add client search

---
 ui/app/components/global-search/control.js    | 39 ++++++++++++++++++-
 .../components/global-search-dropdown.scss    |  8 ++++
 ui/tests/acceptance/search-test.js            | 23 ++++++++---
 ui/tests/pages/layout.js                      |  8 +++-
 4 files changed, 69 insertions(+), 9 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 7f9319b5417..338e969cc0f 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -20,6 +20,10 @@ export default class GlobalSearchControl extends Component {
     this.jobSearch = JobSearch.create({
       something: this, // FIXME what’s a good name?
     });
+
+    this.nodeSearch = NodeSearch.create({
+      something: this,
+    });
   }
 
   @task(function*(string) {
@@ -27,15 +31,28 @@ export default class GlobalSearchControl extends Component {
 
     // FIXME no need to fetch on every search!
     const jobs = yield this.store.findAll('job');
+    const nodes = yield this.store.findAll('node');
 
     this.jobs = jobs.toArray();
+    this.nodes = nodes.toArray();
 
     try {
       const jobResults = this.jobSearch.listSearched;
-      return jobResults;
+      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 jobs', e);
+      console.log('exception searching', e);
     }
   })
   search;
@@ -87,3 +104,21 @@ class JobSearch extends EmberObject.extend(Searchable) {
 
   fuzzySearchEnabled = true;
 }
+
+@classic
+class NodeSearch extends EmberObject.extend(Searchable) {
+  @computed
+  get searchProps() {
+    return ['id', 'name'];
+  }
+
+  @computed
+  get fuzzySearchProps() {
+    return ['name'];
+  }
+
+  @alias('something.nodes') listToSearch;
+  @alias('something.searchString') searchTerm;
+
+  fuzzySearchEnabled = true;
+}
diff --git a/ui/app/styles/components/global-search-dropdown.scss b/ui/app/styles/components/global-search-dropdown.scss
index adbeaec857f..7994e6397ac 100644
--- a/ui/app/styles/components/global-search-dropdown.scss
+++ b/ui/app/styles/components/global-search-dropdown.scss
@@ -29,4 +29,12 @@
       }
     }
   }
+
+  .ember-power-select-group-name {
+    text-transform: uppercase;
+    display: inline;
+    color: darken($grey-blue, 20%);
+    font-size: $size-7;
+    font-weight: $weight-semibold;
+  }
 }
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index f951fd429fd..6f186b4d487 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -9,8 +9,10 @@ module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
   setupMirage(hooks);
 
-  test('search searches jobs', async function(assert) {
-    server.create('node');
+  test('search searches jobs and nodes', async function(assert) {
+    server.create('node', { name: 'xyz' });
+    server.create('node', { name: 'aaa' });
+
     server.create('job', { id: 'vwxyz', namespaceId: 'default' });
     server.create('job', { id: 'xyz', namespace: 'default' });
     server.create('job', { id: 'abc', namespace: 'default' });
@@ -20,9 +22,20 @@ module('Acceptance | search', function(hooks) {
     await selectSearch(PageLayout.navbar.search.scope, 'xy');
 
     PageLayout.navbar.search.as(search => {
-      assert.equal(search.options.length, 2);
-      assert.equal(search.options[0].text, 'xyz');
-      assert.equal(search.options[1].text, 'vwxyz');
+      assert.equal(search.groups.length, 2);
+
+      search.groups[0].as(jobs => {
+        assert.equal(jobs.name, 'Jobs (2)');
+        assert.equal(jobs.options.length, 2);
+        assert.equal(jobs.options[0].text, 'xyz');
+        assert.equal(jobs.options[1].text, 'vwxyz');
+      });
+
+      search.groups[1].as(clients => {
+        assert.equal(clients.name, 'Clients (1)');
+        assert.equal(clients.options.length, 1);
+        assert.equal(clients.options[0].text, 'xyz');
+      });
     });
   });
 
diff --git a/ui/tests/pages/layout.js b/ui/tests/pages/layout.js
index f2d398f2b94..315646aaecc 100644
--- a/ui/tests/pages/layout.js
+++ b/ui/tests/pages/layout.js
@@ -18,10 +18,14 @@ export default create({
 
       click: clickable('.ember-power-select-trigger'),
 
-      options: collection('.ember-power-select-option', {
+      groups: collection('.ember-power-select-group', {
         testContainer: '.ember-power-select-options',
         resetScope: true,
-        label: text(),
+        name: text('.ember-power-select-group-name'),
+
+        options: collection('.ember-power-select-option', {
+          label: text(),
+        }),
       }),
 
       field: {

From 00ced6ffc0b0002cac5b8a4855ccae1dce66670b Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 11:11:52 -0500
Subject: [PATCH 05/24] Add navigation upon search selection

---
 ui/app/components/global-search/control.js | 11 ++++++++++-
 ui/tests/acceptance/search-test.js         | 15 ++++++++++++---
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 338e969cc0f..7d7a6548242 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -10,6 +10,7 @@ import classic from 'ember-classic-decorator';
 
 @tagName('')
 export default class GlobalSearchControl extends Component {
+  @service router;
   @service store;
 
   searchString = null;
@@ -57,7 +58,15 @@ export default class GlobalSearchControl extends Component {
   })
   search;
 
-  @action select() {}
+  @action select(model) {
+    const itemModelName = model.constructor.modelName;
+
+    if (itemModelName === 'job') {
+      this.router.transitionTo('jobs.job', model.name);
+    } else if (itemModelName === 'node') {
+      this.router.transitionTo('clients.client', model.id);
+    }
+  }
 
   @action
   openOnClickOrTab(select, { target }) {
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 6f186b4d487..556f91974d1 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -1,5 +1,5 @@
 import { module, test } from 'qunit';
-import { visit } from '@ember/test-helpers';
+import { currentURL, visit } from '@ember/test-helpers';
 import { setupApplicationTest } from 'ember-qunit';
 import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
@@ -9,9 +9,9 @@ module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
   setupMirage(hooks);
 
-  test('search searches jobs and nodes', async function(assert) {
+  test('search searches jobs and nodes and navigates to chosen items', async function(assert) {
     server.create('node', { name: 'xyz' });
-    server.create('node', { name: 'aaa' });
+    const otherNode = server.create('node', { name: 'aaa' });
 
     server.create('job', { id: 'vwxyz', namespaceId: 'default' });
     server.create('job', { id: 'xyz', namespace: 'default' });
@@ -37,6 +37,15 @@ module('Acceptance | search', function(hooks) {
         assert.equal(clients.options[0].text, 'xyz');
       });
     });
+
+    await PageLayout.navbar.search.groups[0].options[0].click();
+    assert.equal(currentURL(), '/jobs/xyz');
+
+    // Search only works once for unknown reasons!
+    // await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
+
+    // await PageLayout.navbar.search.groups[0].options[0].click();
+    // assert.equal(currentURL(), `/clients/${otherNode.id}`);
   });
 
   test('clicking the search field starts search immediately', async function(assert) {

From a7ded9bb9e05d1887efe88b3b99f3a53029bad77 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 11:14:28 -0500
Subject: [PATCH 06/24] Add indent to fix search icon overlap

---
 ui/app/styles/components/global-search-dropdown.scss | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/ui/app/styles/components/global-search-dropdown.scss b/ui/app/styles/components/global-search-dropdown.scss
index 7994e6397ac..4e9c2f5ef3e 100644
--- a/ui/app/styles/components/global-search-dropdown.scss
+++ b/ui/app/styles/components/global-search-dropdown.scss
@@ -3,6 +3,11 @@
   border: 0;
   position: fixed;
 
+  .ember-power-select-search {
+    margin-left: $icon-dimensions;
+    border: 0;
+  }
+
   input,
   input:focus {
     background: transparent;

From b24f73bc2b60b3abd26d87d8d64710871fc78a8c Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 11:21:48 -0500
Subject: [PATCH 07/24] Remove currently-unused variable

---
 ui/tests/acceptance/search-test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 556f91974d1..fae05de17d1 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -11,7 +11,7 @@ module('Acceptance | search', function(hooks) {
 
   test('search searches jobs and nodes and navigates to chosen items', async function(assert) {
     server.create('node', { name: 'xyz' });
-    const otherNode = server.create('node', { name: 'aaa' });
+    server.create('node', { name: 'aaa' });
 
     server.create('job', { id: 'vwxyz', namespaceId: 'default' });
     server.create('job', { id: 'xyz', namespace: 'default' });

From 180f54a3f75029ef1cf35b937b4add80b34a0df9 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 12:13:10 -0500
Subject: [PATCH 08/24] Add / shortcut to open search
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

I’d prefer for this to be covered by automated tests but
it seems like it only half-works… the placeholder in
the field disappears with the use of keyDown but the
field doesn’t look active. Actually pressing / does
work though.
---
 ui/app/components/global-search/control.js    |  17 +-
 .../components/global-search/control.hbs      |   6 +-
 ui/config/environment.js                      |   4 +
 ui/package.json                               |   1 +
 ui/tests/acceptance/search-test.js            |  12 +-
 ui/yarn.lock                                  | 378 +++++++++++++-----
 6 files changed, 305 insertions(+), 113 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 7d7a6548242..dffd372d362 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -58,7 +58,15 @@ export default class GlobalSearchControl extends Component {
   })
   search;
 
-  @action select(model) {
+  @action
+  open() {
+    if (this.select) {
+      this.select.actions.open();
+    }
+  }
+
+  @action
+  selectOption(model) {
     const itemModelName = model.constructor.modelName;
 
     if (itemModelName === 'job') {
@@ -68,6 +76,13 @@ export default class GlobalSearchControl extends Component {
     }
   }
 
+  @action
+  storeSelect(select) {
+    if (select) {
+      this.select = select;
+    }
+  }
+
   @action
   openOnClickOrTab(select, { target }) {
     // Bypass having to press enter to access search after clicking/tabbing
diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs
index 759594085db..72395b8b299 100644
--- a/ui/app/templates/components/global-search/control.hbs
+++ b/ui/app/templates/components/global-search/control.hbs
@@ -4,12 +4,14 @@
     data-test-search
     @searchEnabled={{true}}
     @search={{perform this.search}}
-    @onChange={{action 'select'}}
+    @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>
\ No newline at end of file
+  </PowerSelect>
+  {{on-key '/' (action 'open')}}
\ No newline at end of file
diff --git a/ui/config/environment.js b/ui/config/environment.js
index 9d758e0a62c..d464ba5a0b8 100644
--- a/ui/config/environment.js
+++ b/ui/config/environment.js
@@ -30,6 +30,10 @@ module.exports = function(environment) {
       mirageWithTokens: true,
       mirageWithRegions: true,
     },
+
+    emberKeyboard: {
+      disableInputsInitializer: true,
+    },
   };
 
   if (environment === 'development') {
diff --git a/ui/package.json b/ui/package.json
index b4158b09f68..80956783b42 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -82,6 +82,7 @@
     "ember-fetch": "^6.5.0",
     "ember-inflector": "^3.0.0",
     "ember-inline-svg": "^0.3.0",
+    "ember-keyboard": "^6.0.0",
     "ember-load-initializers": "^2.0.0",
     "ember-maybe-import-regenerator": "^0.1.6",
     "ember-moment": "^7.8.1",
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index fae05de17d1..78d8b394bd1 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -1,9 +1,10 @@
-import { module, test } from 'qunit';
+import { module, skip, test } from 'qunit';
 import { currentURL, visit } from '@ember/test-helpers';
 import { setupApplicationTest } from 'ember-qunit';
 import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
 import { selectSearch } from 'ember-power-select/test-support';
+import { keyDown } from 'ember-keyboard/test-support/test-helpers';
 
 module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
@@ -57,4 +58,13 @@ module('Acceptance | search', function(hooks) {
 
     assert.ok(PageLayout.navbar.search.field.isPresent);
   });
+
+  // This DOES work but keyDown doesn’t seem quite right…
+  skip('pressing slash starts a search', async function(assert) {
+    await visit('/');
+
+    assert.notOk(PageLayout.navbar.search.field.isPresent);
+    await keyDown('/');
+    assert.ok(PageLayout.navbar.search.field.isPresent);
+  });
 });
diff --git a/ui/yarn.lock b/ui/yarn.lock
index f90cf1b363a..f16a9672b2e 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -52,6 +52,28 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.10.2", "@babel/core@^7.8.3", "@babel/core@^7.9.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a"
+  integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.1"
+    "@babel/generator" "^7.10.2"
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helpers" "^7.10.1"
+    "@babel/parser" "^7.10.2"
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.2"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
 "@babel/core@^7.2.2":
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30"
@@ -92,28 +114,6 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/core@^7.8.3", "@babel/core@^7.9.0":
-  version "7.10.2"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a"
-  integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==
-  dependencies:
-    "@babel/code-frame" "^7.10.1"
-    "@babel/generator" "^7.10.2"
-    "@babel/helper-module-transforms" "^7.10.1"
-    "@babel/helpers" "^7.10.1"
-    "@babel/parser" "^7.10.2"
-    "@babel/template" "^7.10.1"
-    "@babel/traverse" "^7.10.1"
-    "@babel/types" "^7.10.2"
-    convert-source-map "^1.7.0"
-    debug "^4.1.0"
-    gensync "^1.0.0-beta.1"
-    json5 "^2.1.2"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
-    semver "^5.4.1"
-    source-map "^0.5.0"
-
 "@babel/generator@^7.10.1", "@babel/generator@^7.10.2":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9"
@@ -663,6 +663,15 @@
     "@babel/helper-create-class-features-plugin" "^7.4.0"
     "@babel/helper-plugin-utils" "^7.0.0"
 
+"@babel/plugin-proposal-decorators@^7.10.1", "@babel/plugin-proposal-decorators@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.1.tgz#9373c2d8db45345c6e30452ad77b469758e5c8f7"
+  integrity sha512-xBfteh352MTke2U1NpclzMDmAmCdQ2fBZjhZQQfGTjXw6qcRYMkt528sA1U8o0ThDCSeuETXIj5bOGdxN+5gkw==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-decorators" "^7.10.1"
+
 "@babel/plugin-proposal-decorators@^7.3.0":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0"
@@ -672,15 +681,6 @@
     "@babel/helper-plugin-utils" "^7.0.0"
     "@babel/plugin-syntax-decorators" "^7.2.0"
 
-"@babel/plugin-proposal-decorators@^7.8.3":
-  version "7.10.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.1.tgz#9373c2d8db45345c6e30452ad77b469758e5c8f7"
-  integrity sha512-xBfteh352MTke2U1NpclzMDmAmCdQ2fBZjhZQQfGTjXw6qcRYMkt528sA1U8o0ThDCSeuETXIj5bOGdxN+5gkw==
-  dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.10.1"
-    "@babel/helper-plugin-utils" "^7.10.1"
-    "@babel/plugin-syntax-decorators" "^7.10.1"
-
 "@babel/plugin-proposal-dynamic-import@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0"
@@ -1424,6 +1424,16 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
+"@babel/plugin-transform-runtime@^7.10.1", "@babel/plugin-transform-runtime@^7.9.0":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1"
+  integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    resolve "^1.8.1"
+    semver "^5.5.1"
+
 "@babel/plugin-transform-runtime@^7.2.0":
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.0.tgz#45242c2c9281158c5f06d25beebac63e498a284e"
@@ -1434,16 +1444,6 @@
     resolve "^1.8.1"
     semver "^5.5.1"
 
-"@babel/plugin-transform-runtime@^7.9.0":
-  version "7.10.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1"
-  integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw==
-  dependencies:
-    "@babel/helper-module-imports" "^7.10.1"
-    "@babel/helper-plugin-utils" "^7.10.1"
-    resolve "^1.8.1"
-    semver "^5.5.1"
-
 "@babel/plugin-transform-shorthand-properties@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3"
@@ -1525,7 +1525,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
-"@babel/plugin-transform-typescript@^7.9.0":
+"@babel/plugin-transform-typescript@^7.10.1", "@babel/plugin-transform-typescript@^7.9.0":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz#2c54daea231f602468686d9faa76f182a94507a6"
   integrity sha512-v+QWKlmCnsaimLeqq9vyCsVRMViZG1k2SZTlcZvB+TqyH570Zsij8nvVUZzOASCRiQFUxkLrn9Wg/kH0zgy5OQ==
@@ -1583,7 +1583,7 @@
     core-js "^2.6.5"
     regenerator-runtime "^0.13.2"
 
-"@babel/polyfill@^7.8.3", "@babel/polyfill@^7.8.7":
+"@babel/polyfill@^7.10.1", "@babel/polyfill@^7.8.3", "@babel/polyfill@^7.8.7":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.10.1.tgz#d56d4c8be8dd6ec4dce2649474e9b707089f739f"
   integrity sha512-TviueJ4PBW5p48ra8IMtLXVkDucrlOZAIZ+EXqS3Ot4eukHbWiqcn7DcqpA1k5PcKtmJ4Xl9xwdv6yQvvcA+3g==
@@ -1647,63 +1647,7 @@
     js-levenshtein "^1.1.3"
     semver "^5.5.0"
 
-"@babel/preset-env@^7.4.5":
-  version "7.6.2"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3"
-  integrity sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==
-  dependencies:
-    "@babel/helper-module-imports" "^7.0.0"
-    "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
-    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
-    "@babel/plugin-proposal-json-strings" "^7.2.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
-    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
-    "@babel/plugin-syntax-async-generators" "^7.2.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
-    "@babel/plugin-syntax-json-strings" "^7.2.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-transform-arrow-functions" "^7.2.0"
-    "@babel/plugin-transform-async-to-generator" "^7.5.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
-    "@babel/plugin-transform-block-scoping" "^7.6.2"
-    "@babel/plugin-transform-classes" "^7.5.5"
-    "@babel/plugin-transform-computed-properties" "^7.2.0"
-    "@babel/plugin-transform-destructuring" "^7.6.0"
-    "@babel/plugin-transform-dotall-regex" "^7.6.2"
-    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
-    "@babel/plugin-transform-for-of" "^7.4.4"
-    "@babel/plugin-transform-function-name" "^7.4.4"
-    "@babel/plugin-transform-literals" "^7.2.0"
-    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
-    "@babel/plugin-transform-modules-amd" "^7.5.0"
-    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
-    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
-    "@babel/plugin-transform-modules-umd" "^7.2.0"
-    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.2"
-    "@babel/plugin-transform-new-target" "^7.4.4"
-    "@babel/plugin-transform-object-super" "^7.5.5"
-    "@babel/plugin-transform-parameters" "^7.4.4"
-    "@babel/plugin-transform-property-literals" "^7.2.0"
-    "@babel/plugin-transform-regenerator" "^7.4.5"
-    "@babel/plugin-transform-reserved-words" "^7.2.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
-    "@babel/plugin-transform-spread" "^7.6.2"
-    "@babel/plugin-transform-sticky-regex" "^7.2.0"
-    "@babel/plugin-transform-template-literals" "^7.4.4"
-    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
-    "@babel/plugin-transform-unicode-regex" "^7.6.2"
-    "@babel/types" "^7.6.0"
-    browserslist "^4.6.0"
-    core-js-compat "^3.1.1"
-    invariant "^2.2.2"
-    js-levenshtein "^1.1.3"
-    semver "^5.5.0"
-
-"@babel/preset-env@^7.9.0":
+"@babel/preset-env@^7.10.2", "@babel/preset-env@^7.9.0":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.2.tgz#715930f2cf8573b0928005ee562bed52fb65fdfb"
   integrity sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA==
@@ -1773,6 +1717,62 @@
     levenary "^1.1.1"
     semver "^5.5.0"
 
+"@babel/preset-env@^7.4.5":
+  version "7.6.2"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3"
+  integrity sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
+    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
+    "@babel/plugin-proposal-json-strings" "^7.2.0"
+    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
+    "@babel/plugin-syntax-async-generators" "^7.2.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
+    "@babel/plugin-syntax-json-strings" "^7.2.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+    "@babel/plugin-transform-arrow-functions" "^7.2.0"
+    "@babel/plugin-transform-async-to-generator" "^7.5.0"
+    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
+    "@babel/plugin-transform-block-scoping" "^7.6.2"
+    "@babel/plugin-transform-classes" "^7.5.5"
+    "@babel/plugin-transform-computed-properties" "^7.2.0"
+    "@babel/plugin-transform-destructuring" "^7.6.0"
+    "@babel/plugin-transform-dotall-regex" "^7.6.2"
+    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
+    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
+    "@babel/plugin-transform-for-of" "^7.4.4"
+    "@babel/plugin-transform-function-name" "^7.4.4"
+    "@babel/plugin-transform-literals" "^7.2.0"
+    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
+    "@babel/plugin-transform-modules-amd" "^7.5.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
+    "@babel/plugin-transform-modules-umd" "^7.2.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.2"
+    "@babel/plugin-transform-new-target" "^7.4.4"
+    "@babel/plugin-transform-object-super" "^7.5.5"
+    "@babel/plugin-transform-parameters" "^7.4.4"
+    "@babel/plugin-transform-property-literals" "^7.2.0"
+    "@babel/plugin-transform-regenerator" "^7.4.5"
+    "@babel/plugin-transform-reserved-words" "^7.2.0"
+    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
+    "@babel/plugin-transform-spread" "^7.6.2"
+    "@babel/plugin-transform-sticky-regex" "^7.2.0"
+    "@babel/plugin-transform-template-literals" "^7.4.4"
+    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
+    "@babel/plugin-transform-unicode-regex" "^7.6.2"
+    "@babel/types" "^7.6.0"
+    browserslist "^4.6.0"
+    core-js-compat "^3.1.1"
+    invariant "^2.2.2"
+    js-levenshtein "^1.1.3"
+    semver "^5.5.0"
+
 "@babel/preset-modules@^0.1.3":
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
@@ -1798,6 +1798,13 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
+"@babel/runtime@^7.10.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
+  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.2.0":
   version "7.5.4"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
@@ -1812,13 +1819,6 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
-"@babel/runtime@^7.8.4", "@babel/runtime@^7.9.0":
-  version "7.10.2"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
-  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
 "@babel/runtime@^7.8.7":
   version "7.9.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
@@ -2076,6 +2076,11 @@
   dependencies:
     ember-cli-babel "^7.1.3"
 
+"@ember/edition-utils@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@ember/edition-utils/-/edition-utils-1.2.0.tgz#a039f542dc14c8e8299c81cd5abba95e2459cfa6"
+  integrity sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==
+
 "@ember/optional-features@^0.7.0":
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/@ember/optional-features/-/optional-features-0.7.0.tgz#f65a858007020ddfb8342f586112750c32abd2d9"
@@ -4050,7 +4055,7 @@ babel-plugin-debug-macros@^0.3.0:
   dependencies:
     semver "^5.3.0"
 
-babel-plugin-debug-macros@^0.3.2:
+babel-plugin-debug-macros@^0.3.2, babel-plugin-debug-macros@^0.3.3:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.3.3.tgz#29c3449d663f61c7385f5b8c72d8015b069a5cb7"
   integrity sha512-E+NI8TKpxJDBbVkdWkwHrKgJi696mnRL8XYrOPYw82veNHPDORM9WIQifl6TpIo8PNy2tU2skPqbfkmHXrHKQA==
@@ -4133,6 +4138,11 @@ babel-plugin-htmlbars-inline-precompile@^1.0.0:
   resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-1.0.0.tgz#a9d2f6eaad8a3f3d361602de593a8cbef8179c22"
   integrity sha512-4jvKEHR1bAX03hBDZ94IXsYCj3bwk9vYsn6ux6JZNL2U5pvzCWjqyrGahfsGNrhERyxw8IqcirOi9Q6WCo3dkQ==
 
+babel-plugin-htmlbars-inline-precompile@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-3.1.0.tgz#85085b50385277f2b331ebd54e22fa91aadc24e8"
+  integrity sha512-ar6c4YVX6OV7Dzpq7xRyllQrHwVEzJf41qysYULnD6tu6TS+y1FxT2VcEvMC6b9Rq9xoHMzvB79HO3av89JCGg==
+
 babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.4.5:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz#41f7ead616fc36f6a93180e89697f69f51671181"
@@ -5002,6 +5012,23 @@ broccoli-babel-transpiler@^7.4.0:
     rsvp "^4.8.4"
     workerpool "^3.1.1"
 
+broccoli-babel-transpiler@^7.5.0:
+  version "7.5.0"
+  resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-7.5.0.tgz#6137e2c9324eebe1d83b6a01c7b445bfc5612d49"
+  integrity sha512-CrLrI8HX7mDqVR+/r8WiTZuQQh6hGMGwal9jyKk+kpk7t/4hqL6oQ06FRt81kazbHm4bil4WJ+kGB+aOfAx+XA==
+  dependencies:
+    "@babel/core" "^7.10.2"
+    "@babel/polyfill" "^7.10.1"
+    broccoli-funnel "^2.0.2"
+    broccoli-merge-trees "^3.0.2"
+    broccoli-persistent-filter "^2.2.1"
+    clone "^2.1.2"
+    hash-for-dep "^1.4.7"
+    heimdalljs-logger "^0.1.9"
+    json-stable-stringify "^1.0.1"
+    rsvp "^4.8.4"
+    workerpool "^3.1.1"
+
 broccoli-builder@^0.18.14:
   version "0.18.14"
   resolved "https://registry.yarnpkg.com/broccoli-builder/-/broccoli-builder-0.18.14.tgz#4b79e2f844de11a4e1b816c3f49c6df4776c312d"
@@ -5302,6 +5329,11 @@ broccoli-module-unification-reexporter@^1.0.0:
     mkdirp "^0.5.1"
     walk-sync "^0.3.2"
 
+broccoli-node-api@^1.6.0, broccoli-node-api@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz#391aa6edecd2a42c63c111b4162956b2fa288cb6"
+  integrity sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==
+
 broccoli-node-info@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-1.1.0.tgz#3aa2e31e07e5bdb516dd25214f7c45ba1c459412"
@@ -5312,6 +5344,13 @@ broccoli-node-info@^2.1.0:
   resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-2.1.0.tgz#ca84560e8570ff78565bea1699866ddbf58ad644"
   integrity sha512-l6qDuboJThHfRVVWQVaTs++bFdrFTP0gJXgsWenczc1PavRVUmL1Eyb2swTAXXMpDOnr2zhNOBLx4w9AxkqbPQ==
 
+broccoli-output-wrapper@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/broccoli-output-wrapper/-/broccoli-output-wrapper-2.0.0.tgz#f1e0b9b2f259a67fd41a380141c3c20b096828e6"
+  integrity sha512-V/ozejo+snzNf75i/a6iTmp71k+rlvqjE3+jYfimuMwR1tjNNRdtfno+NGNQB2An9bIAeqZnKhMDurAznHAdtA==
+  dependencies:
+    heimdalljs-logger "^0.1.10"
+
 broccoli-persistent-filter@^1.1.5, broccoli-persistent-filter@^1.1.6, broccoli-persistent-filter@^1.4.3:
   version "1.4.6"
   resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.6.tgz#80762d19000880a77da33c34373299c0f6a3e615"
@@ -5400,6 +5439,19 @@ broccoli-plugin@^2.0.0:
     rimraf "^2.3.4"
     symlink-or-copy "^1.1.8"
 
+broccoli-plugin@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-3.1.0.tgz#54ba6dd90a42ec3db5624063292610e326b1e542"
+  integrity sha512-7w7FP8WJYjLvb0eaw27LO678TGGaom++49O1VYIuzjhXjK5kn2+AMlDm7CaUFw4F7CLGoVQeZ84d8gICMJa4lA==
+  dependencies:
+    broccoli-node-api "^1.6.0"
+    broccoli-output-wrapper "^2.0.0"
+    fs-merger "^3.0.1"
+    promise-map-series "^0.2.1"
+    quick-temp "^0.1.3"
+    rimraf "^2.3.4"
+    symlink-or-copy "^1.1.8"
+
 broccoli-rollup@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/broccoli-rollup/-/broccoli-rollup-2.1.1.tgz#0b77dc4b7560a53e998ea85f3b56772612d4988d"
@@ -7616,6 +7668,38 @@ ember-cli-babel@^7.11.1:
     rimraf "^3.0.1"
     semver "^5.5.0"
 
+ember-cli-babel@^7.19.0:
+  version "7.21.0"
+  resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.21.0.tgz#c79e888876aee87dfc3260aee7cb580b74264bbc"
+  integrity sha512-jHVi9melAibo0DrAG3GAxid+29xEyjBoU53652B4qcu3Xp58feZGTH/JGXovH7TjvbeNn65zgNyoV3bk1onULw==
+  dependencies:
+    "@babel/core" "^7.10.2"
+    "@babel/helper-compilation-targets" "^7.10.2"
+    "@babel/plugin-proposal-class-properties" "^7.10.1"
+    "@babel/plugin-proposal-decorators" "^7.10.1"
+    "@babel/plugin-transform-modules-amd" "^7.10.1"
+    "@babel/plugin-transform-runtime" "^7.10.1"
+    "@babel/plugin-transform-typescript" "^7.10.1"
+    "@babel/polyfill" "^7.10.1"
+    "@babel/preset-env" "^7.10.2"
+    "@babel/runtime" "^7.10.2"
+    amd-name-resolver "^1.2.1"
+    babel-plugin-debug-macros "^0.3.3"
+    babel-plugin-ember-data-packages-polyfill "^0.1.2"
+    babel-plugin-ember-modules-api-polyfill "^2.13.4"
+    babel-plugin-module-resolver "^3.1.1"
+    broccoli-babel-transpiler "^7.5.0"
+    broccoli-debug "^0.6.4"
+    broccoli-funnel "^2.0.1"
+    broccoli-source "^1.1.0"
+    clone "^2.1.2"
+    ember-cli-babel-plugin-helpers "^1.1.0"
+    ember-cli-version-checker "^4.1.0"
+    ensure-posix-path "^1.0.2"
+    fixturify-project "^1.10.0"
+    rimraf "^3.0.1"
+    semver "^5.5.0"
+
 ember-cli-broccoli-sane-watcher@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ember-cli-broccoli-sane-watcher/-/ember-cli-broccoli-sane-watcher-3.0.0.tgz#dc1812c047e1ceec4413d3c41b51a9ffc61b4cfe"
@@ -7720,6 +7804,26 @@ ember-cli-htmlbars@^3.0.1, ember-cli-htmlbars@^3.1.0:
     json-stable-stringify "^1.0.1"
     strip-bom "^3.0.0"
 
+ember-cli-htmlbars@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.3.1.tgz#4af8adc21ab3c4953f768956b7f7d207782cb175"
+  integrity sha512-CW6AY/yzjeVqoRtItOKj3hcYzc5dWPRETmeCzr2Iqjt5vxiVtpl0z5VTqHqIlT5fsFx6sGWBQXNHIe+ivYsxXQ==
+  dependencies:
+    "@ember/edition-utils" "^1.2.0"
+    babel-plugin-htmlbars-inline-precompile "^3.0.1"
+    broccoli-debug "^0.6.5"
+    broccoli-persistent-filter "^2.3.1"
+    broccoli-plugin "^3.1.0"
+    common-tags "^1.8.0"
+    ember-cli-babel-plugin-helpers "^1.1.0"
+    fs-tree-diff "^2.0.1"
+    hash-for-dep "^1.5.1"
+    heimdalljs-logger "^0.1.10"
+    json-stable-stringify "^1.0.1"
+    semver "^6.3.0"
+    strip-bom "^4.0.0"
+    walk-sync "^2.0.2"
+
 ember-cli-import-polyfill@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/ember-cli-import-polyfill/-/ember-cli-import-polyfill-0.2.0.tgz#c1a08a8affb45c97b675926272fe78cf4ca166f2"
@@ -8043,6 +8147,15 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.2.0:
     ember-cli-version-checker "^2.1.1"
     semver "^5.4.1"
 
+ember-compatibility-helpers@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.1.tgz#87c92c4303f990ff455c28ca39fb3ee11441aa16"
+  integrity sha512-6wzYvnhg1ihQUT5yGqnLtleq3Nv5KNv79WhrEuNU9SwR4uIxCO+KpyC7r3d5VI0EM7/Nmv9Nd0yTkzmTMdVG1A==
+  dependencies:
+    babel-plugin-debug-macros "^0.2.0"
+    ember-cli-version-checker "^2.1.1"
+    semver "^5.4.1"
+
 ember-composable-helpers@^2.0.3:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-2.3.1.tgz#db98ad8b55d053e2ac216b9da091c9e7a3b9f453"
@@ -8198,6 +8311,16 @@ ember-inline-svg@^0.3.0:
     svgo "~1.2.2"
     walk-sync "^0.3.1"
 
+ember-keyboard@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-6.0.0.tgz#0bf929bfa1a5935109e59c1b816e65ad2db0383b"
+  integrity sha512-QJAhs8173hqy8e/Wj/98+5tJ43I5UKq35lsx3w8icHL5iisfXo8s89gOtn1C2015Y4JoMNsVOkNSCnBfhx1UQg==
+  dependencies:
+    ember-cli-babel "^7.19.0"
+    ember-cli-htmlbars "^4.3.1"
+    ember-compatibility-helpers "^1.2.1"
+    ember-modifier "^1.0.3"
+
 ember-load-initializers@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.0.tgz#b402815ab9c823ff48a1369b52633721987e72d4"
@@ -8233,7 +8356,7 @@ ember-maybe-in-element@^0.4.0:
   dependencies:
     ember-cli-babel "^7.1.0"
 
-ember-modifier-manager-polyfill@^1.1.0:
+ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda"
   integrity sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==
@@ -8242,6 +8365,17 @@ ember-modifier-manager-polyfill@^1.1.0:
     ember-cli-version-checker "^2.1.2"
     ember-compatibility-helpers "^1.2.0"
 
+ember-modifier@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-1.0.3.tgz#ab18250666aad17c0d9170feb178e954148eb4ed"
+  integrity sha512-vWuFyvdkULUyasvEXxe5lcfuPZV/Uqe+b0IQ1yU+TY1RSJnFdVUu/CVHT8Bu4HUJInqzAihwPMTwty7fypzi5Q==
+  dependencies:
+    ember-cli-babel "^7.11.1"
+    ember-cli-is-package-missing "^1.0.0"
+    ember-cli-normalize-entity-name "^1.0.0"
+    ember-cli-string-utils "^1.1.0"
+    ember-modifier-manager-polyfill "^1.2.0"
+
 ember-moment@^7.8.1:
   version "7.8.1"
   resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-7.8.1.tgz#6f77cf941d1a92e231b2f4b810e113b2fae50c5f"
@@ -9547,6 +9681,18 @@ fs-extra@^8.0.1:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
+fs-merger@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.1.0.tgz#f30f74f6c70b2ff7333ec074f3d2f22298152f3b"
+  integrity sha512-RZ9JtqugaE8Rkt7idO5NSwcxEGSDZpLmVFjtVQUm3f+bWun7JAU6fKyU6ZJUeUnKdJwGx8uaro+K4QQfOR7vpA==
+  dependencies:
+    broccoli-node-api "^1.7.0"
+    broccoli-node-info "^2.1.0"
+    fs-extra "^8.0.1"
+    fs-tree-diff "^2.0.1"
+    rimraf "^2.6.3"
+    walk-sync "^2.0.2"
+
 fs-minipass@^1.2.5:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
@@ -15484,6 +15630,11 @@ strip-bom@^3.0.0:
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -16416,6 +16567,15 @@ walk-sync@^1.0.0, walk-sync@^1.1.3:
     ensure-posix-path "^1.1.0"
     matcher-collection "^1.1.1"
 
+walk-sync@^2.0.2:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.1.0.tgz#e214248e5f5cf497ddd87db5a2a02e47e29c6501"
+  integrity sha512-KpH9Xw64LNSx7/UI+3guRZvJWlDxVA4+KKb/4puRoVrG8GkvZRxnF3vhxdjgpoKJGL2TVg1OrtkXIE/VuGPLHQ==
+  dependencies:
+    "@types/minimatch" "^3.0.3"
+    ensure-posix-path "^1.1.0"
+    matcher-collection "^2.0.0"
+
 walker@~1.0.5:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"

From e7cc5f59d6d14d8351e4105f4f1ea2cf9cd55d47 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Tue, 16 Jun 2020 13:16:44 -0500
Subject: [PATCH 09/24] Add test for navigating to nodes

---
 ui/tests/acceptance/search-test.js | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 78d8b394bd1..9caf85ec81f 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -12,7 +12,7 @@ module('Acceptance | search', function(hooks) {
 
   test('search searches jobs and nodes and navigates to chosen items', async function(assert) {
     server.create('node', { name: 'xyz' });
-    server.create('node', { name: 'aaa' });
+    const otherNode = server.create('node', { name: 'aaa' });
 
     server.create('job', { id: 'vwxyz', namespaceId: 'default' });
     server.create('job', { id: 'xyz', namespace: 'default' });
@@ -42,11 +42,10 @@ module('Acceptance | search', function(hooks) {
     await PageLayout.navbar.search.groups[0].options[0].click();
     assert.equal(currentURL(), '/jobs/xyz');
 
-    // Search only works once for unknown reasons!
-    // await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
+    await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
 
-    // await PageLayout.navbar.search.groups[0].options[0].click();
-    // assert.equal(currentURL(), `/clients/${otherNode.id}`);
+    await PageLayout.navbar.search.groups[1].options[0].click();
+    assert.equal(currentURL(), `/clients/${otherNode.id}`);
   });
 
   test('clicking the search field starts search immediately', async function(assert) {

From 94f42d02c1c92f09db03de0d93f3d929e8eb1fa3 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Wed, 17 Jun 2020 08:33:02 -0500
Subject: [PATCH 10/24] Expand scope of try to catch more errors

Has it always been that errors are silently swallowed?
Mysterious
---
 ui/app/components/global-search/control.js | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index dffd372d362..5405ee42e0d 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -28,16 +28,16 @@ export default class GlobalSearchControl extends Component {
   }
 
   @task(function*(string) {
-    this.searchString = string;
+    try {
+      this.searchString = string;
 
-    // FIXME no need to fetch on every search!
-    const jobs = yield this.store.findAll('job');
-    const nodes = yield this.store.findAll('node');
+      // FIXME no need to fetch on every search!
+      const jobs = yield this.store.findAll('job');
+      const nodes = yield this.store.findAll('node');
 
-    this.jobs = jobs.toArray();
-    this.nodes = nodes.toArray();
+      this.jobs = jobs.toArray();
+      this.nodes = nodes.toArray();
 
-    try {
       const jobResults = this.jobSearch.listSearched;
       const nodeResults = this.nodeSearch.listSearched;
 

From de076d62081d9944045960d2339528661eeabeaa Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Wed, 17 Jun 2020 08:36:56 -0500
Subject: [PATCH 11/24] Change to use set for search properties
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

With the expanded try, I found that the reason for search
only working once was that it was required to use set for
these properties, I guess because they’re used by
computed properties.
---
 ui/app/components/global-search/control.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 5405ee42e0d..b1eba783c9b 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -1,7 +1,7 @@
 import Component from '@ember/component';
 import { tagName } from '@ember-decorators/component';
 import { task } from 'ember-concurrency';
-import EmberObject, { action, computed } from '@ember/object';
+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';
@@ -29,14 +29,14 @@ export default class GlobalSearchControl extends Component {
 
   @task(function*(string) {
     try {
-      this.searchString = string;
+      set(this, 'searchString', string);
 
       // FIXME no need to fetch on every search!
       const jobs = yield this.store.findAll('job');
       const nodes = yield this.store.findAll('node');
 
-      this.jobs = jobs.toArray();
-      this.nodes = nodes.toArray();
+      set(this, 'jobs', jobs.toArray());
+      set(this, 'nodes', nodes.toArray());
 
       const jobResults = this.jobSearch.listSearched;
       const nodeResults = this.nodeSearch.listSearched;

From db0ceaca8eda967a23723b3b7b626da335560e0e Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Wed, 17 Jun 2020 09:08:28 -0500
Subject: [PATCH 12/24] Change interface for search classes

This is still unwieldly but maybe a bit more sensible now.
---
 ui/app/components/global-search/control.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index b1eba783c9b..8614f38ddcd 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -19,11 +19,11 @@ export default class GlobalSearchControl extends Component {
     super(...arguments);
 
     this.jobSearch = JobSearch.create({
-      something: this, // FIXME what’s a good name?
+      dataSource: this,
     });
 
     this.nodeSearch = NodeSearch.create({
-      something: this,
+      dataSource: this,
     });
   }
 
@@ -123,8 +123,8 @@ class JobSearch extends EmberObject.extend(Searchable) {
     return ['name'];
   }
 
-  @alias('something.jobs') listToSearch;
-  @alias('something.searchString') searchTerm;
+  @alias('dataSource.jobs') listToSearch;
+  @alias('dataSource.searchString') searchTerm;
 
   fuzzySearchEnabled = true;
 }
@@ -141,8 +141,8 @@ class NodeSearch extends EmberObject.extend(Searchable) {
     return ['name'];
   }
 
-  @alias('something.nodes') listToSearch;
-  @alias('something.searchString') searchTerm;
+  @alias('dataSource.nodes') listToSearch;
+  @alias('dataSource.searchString') searchTerm;
 
   fuzzySearchEnabled = true;
 }

From a2131767e9faf1d4a998d69e2cfc3b7af0347ce4 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Wed, 17 Jun 2020 11:57:13 -0500
Subject: [PATCH 13/24] Stop control from capturing / in input fields
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

I’ve adapted the listener from @DingoEatingFuzz’s keyboard event-
handling here, obviating the need for ember-keyboard:
https://github.com/hashicorp/nomad/pull/8177/files#diff-eb4d61e1e080830b844012c6617c6fa2

The filtering to prevent stealing events from input fields
is adapted from here:
https://stackoverflow.com/a/4171758
---
 ui/app/components/global-search/control.js    |  23 ++
 .../components/global-search/control.hbs      |   1 -
 ui/config/environment.js                      |   4 -
 ui/package.json                               |   1 -
 ui/tests/acceptance/search-test.js            |  32 +-
 ui/yarn.lock                                  | 378 +++++-------------
 6 files changed, 158 insertions(+), 281 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 8614f38ddcd..2d5f1d30746 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -8,6 +8,8 @@ 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 router;
@@ -27,6 +29,27 @@ export default class GlobalSearchControl extends Component {
     });
   }
 
+  keyDownHandler(e) {
+    const targetElementName = e.target.nodeName.toLowerCase();
+
+    // FIXME are more and/or other-approach exceptions needed?
+    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);
diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs
index 72395b8b299..d1f2c5c949f 100644
--- a/ui/app/templates/components/global-search/control.hbs
+++ b/ui/app/templates/components/global-search/control.hbs
@@ -14,4 +14,3 @@
     as |option|>
     {{option.name}}
   </PowerSelect>
-  {{on-key '/' (action 'open')}}
\ No newline at end of file
diff --git a/ui/config/environment.js b/ui/config/environment.js
index d464ba5a0b8..9d758e0a62c 100644
--- a/ui/config/environment.js
+++ b/ui/config/environment.js
@@ -30,10 +30,6 @@ module.exports = function(environment) {
       mirageWithTokens: true,
       mirageWithRegions: true,
     },
-
-    emberKeyboard: {
-      disableInputsInitializer: true,
-    },
   };
 
   if (environment === 'development') {
diff --git a/ui/package.json b/ui/package.json
index 80956783b42..b4158b09f68 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -82,7 +82,6 @@
     "ember-fetch": "^6.5.0",
     "ember-inflector": "^3.0.0",
     "ember-inline-svg": "^0.3.0",
-    "ember-keyboard": "^6.0.0",
     "ember-load-initializers": "^2.0.0",
     "ember-maybe-import-regenerator": "^0.1.6",
     "ember-moment": "^7.8.1",
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 9caf85ec81f..5f43ba1c0af 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -1,10 +1,9 @@
-import { module, skip, test } from 'qunit';
-import { currentURL, visit } from '@ember/test-helpers';
+import { module, test } from 'qunit';
+import { click, currentURL, triggerEvent, visit } from '@ember/test-helpers';
 import { setupApplicationTest } from 'ember-qunit';
 import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
 import { selectSearch } from 'ember-power-select/test-support';
-import { keyDown } from 'ember-keyboard/test-support/test-helpers';
 
 module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
@@ -58,12 +57,33 @@ module('Acceptance | search', function(hooks) {
     assert.ok(PageLayout.navbar.search.field.isPresent);
   });
 
-  // This DOES work but keyDown doesn’t seem quite right…
-  skip('pressing slash starts a search', async function(assert) {
+  test('pressing slash starts a search', async function(assert) {
     await visit('/');
 
     assert.notOk(PageLayout.navbar.search.field.isPresent);
-    await keyDown('/');
+
+    await triggerEvent('.page-layout', 'keydown', {
+      keyCode: 191, // slash
+    });
+
     assert.ok(PageLayout.navbar.search.field.isPresent);
   });
+
+  test('pressing slash when an input element is focused does not start a search', async function(assert) {
+    server.create('node');
+    server.create('job');
+
+    await visit('/');
+
+    assert.notOk(PageLayout.navbar.search.field.isPresent);
+
+    // FIXME use page objects for this and below? 🤔
+    await click('.search-box input');
+
+    await triggerEvent('.search-box input', 'keydown', {
+      keyCode: 191, // slash
+    });
+
+    assert.notOk(PageLayout.navbar.search.field.isPresent);
+  });
 });
diff --git a/ui/yarn.lock b/ui/yarn.lock
index f16a9672b2e..f90cf1b363a 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -52,28 +52,6 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/core@^7.10.2", "@babel/core@^7.8.3", "@babel/core@^7.9.0":
-  version "7.10.2"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a"
-  integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==
-  dependencies:
-    "@babel/code-frame" "^7.10.1"
-    "@babel/generator" "^7.10.2"
-    "@babel/helper-module-transforms" "^7.10.1"
-    "@babel/helpers" "^7.10.1"
-    "@babel/parser" "^7.10.2"
-    "@babel/template" "^7.10.1"
-    "@babel/traverse" "^7.10.1"
-    "@babel/types" "^7.10.2"
-    convert-source-map "^1.7.0"
-    debug "^4.1.0"
-    gensync "^1.0.0-beta.1"
-    json5 "^2.1.2"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
-    semver "^5.4.1"
-    source-map "^0.5.0"
-
 "@babel/core@^7.2.2":
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30"
@@ -114,6 +92,28 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.8.3", "@babel/core@^7.9.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.2.tgz#bd6786046668a925ac2bd2fd95b579b92a23b36a"
+  integrity sha512-KQmV9yguEjQsXqyOUGKjS4+3K8/DlOCE2pZcq4augdQmtTy5iv5EHtmMSJ7V4c1BIPjuwtZYqYLCq9Ga+hGBRQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.1"
+    "@babel/generator" "^7.10.2"
+    "@babel/helper-module-transforms" "^7.10.1"
+    "@babel/helpers" "^7.10.1"
+    "@babel/parser" "^7.10.2"
+    "@babel/template" "^7.10.1"
+    "@babel/traverse" "^7.10.1"
+    "@babel/types" "^7.10.2"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
 "@babel/generator@^7.10.1", "@babel/generator@^7.10.2":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9"
@@ -663,15 +663,6 @@
     "@babel/helper-create-class-features-plugin" "^7.4.0"
     "@babel/helper-plugin-utils" "^7.0.0"
 
-"@babel/plugin-proposal-decorators@^7.10.1", "@babel/plugin-proposal-decorators@^7.8.3":
-  version "7.10.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.1.tgz#9373c2d8db45345c6e30452ad77b469758e5c8f7"
-  integrity sha512-xBfteh352MTke2U1NpclzMDmAmCdQ2fBZjhZQQfGTjXw6qcRYMkt528sA1U8o0ThDCSeuETXIj5bOGdxN+5gkw==
-  dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.10.1"
-    "@babel/helper-plugin-utils" "^7.10.1"
-    "@babel/plugin-syntax-decorators" "^7.10.1"
-
 "@babel/plugin-proposal-decorators@^7.3.0":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0"
@@ -681,6 +672,15 @@
     "@babel/helper-plugin-utils" "^7.0.0"
     "@babel/plugin-syntax-decorators" "^7.2.0"
 
+"@babel/plugin-proposal-decorators@^7.8.3":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.1.tgz#9373c2d8db45345c6e30452ad77b469758e5c8f7"
+  integrity sha512-xBfteh352MTke2U1NpclzMDmAmCdQ2fBZjhZQQfGTjXw6qcRYMkt528sA1U8o0ThDCSeuETXIj5bOGdxN+5gkw==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    "@babel/plugin-syntax-decorators" "^7.10.1"
+
 "@babel/plugin-proposal-dynamic-import@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0"
@@ -1424,16 +1424,6 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
-"@babel/plugin-transform-runtime@^7.10.1", "@babel/plugin-transform-runtime@^7.9.0":
-  version "7.10.1"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1"
-  integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw==
-  dependencies:
-    "@babel/helper-module-imports" "^7.10.1"
-    "@babel/helper-plugin-utils" "^7.10.1"
-    resolve "^1.8.1"
-    semver "^5.5.1"
-
 "@babel/plugin-transform-runtime@^7.2.0":
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.0.tgz#45242c2c9281158c5f06d25beebac63e498a284e"
@@ -1444,6 +1434,16 @@
     resolve "^1.8.1"
     semver "^5.5.1"
 
+"@babel/plugin-transform-runtime@^7.9.0":
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.1.tgz#fd1887f749637fb2ed86dc278e79eb41df37f4b1"
+  integrity sha512-4w2tcglDVEwXJ5qxsY++DgWQdNJcCCsPxfT34wCUwIf2E7dI7pMpH8JczkMBbgBTNzBX62SZlNJ9H+De6Zebaw==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.1"
+    "@babel/helper-plugin-utils" "^7.10.1"
+    resolve "^1.8.1"
+    semver "^5.5.1"
+
 "@babel/plugin-transform-shorthand-properties@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3"
@@ -1525,7 +1525,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
-"@babel/plugin-transform-typescript@^7.10.1", "@babel/plugin-transform-typescript@^7.9.0":
+"@babel/plugin-transform-typescript@^7.9.0":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz#2c54daea231f602468686d9faa76f182a94507a6"
   integrity sha512-v+QWKlmCnsaimLeqq9vyCsVRMViZG1k2SZTlcZvB+TqyH570Zsij8nvVUZzOASCRiQFUxkLrn9Wg/kH0zgy5OQ==
@@ -1583,7 +1583,7 @@
     core-js "^2.6.5"
     regenerator-runtime "^0.13.2"
 
-"@babel/polyfill@^7.10.1", "@babel/polyfill@^7.8.3", "@babel/polyfill@^7.8.7":
+"@babel/polyfill@^7.8.3", "@babel/polyfill@^7.8.7":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.10.1.tgz#d56d4c8be8dd6ec4dce2649474e9b707089f739f"
   integrity sha512-TviueJ4PBW5p48ra8IMtLXVkDucrlOZAIZ+EXqS3Ot4eukHbWiqcn7DcqpA1k5PcKtmJ4Xl9xwdv6yQvvcA+3g==
@@ -1647,7 +1647,63 @@
     js-levenshtein "^1.1.3"
     semver "^5.5.0"
 
-"@babel/preset-env@^7.10.2", "@babel/preset-env@^7.9.0":
+"@babel/preset-env@^7.4.5":
+  version "7.6.2"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3"
+  integrity sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
+    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
+    "@babel/plugin-proposal-json-strings" "^7.2.0"
+    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
+    "@babel/plugin-syntax-async-generators" "^7.2.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
+    "@babel/plugin-syntax-json-strings" "^7.2.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+    "@babel/plugin-transform-arrow-functions" "^7.2.0"
+    "@babel/plugin-transform-async-to-generator" "^7.5.0"
+    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
+    "@babel/plugin-transform-block-scoping" "^7.6.2"
+    "@babel/plugin-transform-classes" "^7.5.5"
+    "@babel/plugin-transform-computed-properties" "^7.2.0"
+    "@babel/plugin-transform-destructuring" "^7.6.0"
+    "@babel/plugin-transform-dotall-regex" "^7.6.2"
+    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
+    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
+    "@babel/plugin-transform-for-of" "^7.4.4"
+    "@babel/plugin-transform-function-name" "^7.4.4"
+    "@babel/plugin-transform-literals" "^7.2.0"
+    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
+    "@babel/plugin-transform-modules-amd" "^7.5.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
+    "@babel/plugin-transform-modules-umd" "^7.2.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.2"
+    "@babel/plugin-transform-new-target" "^7.4.4"
+    "@babel/plugin-transform-object-super" "^7.5.5"
+    "@babel/plugin-transform-parameters" "^7.4.4"
+    "@babel/plugin-transform-property-literals" "^7.2.0"
+    "@babel/plugin-transform-regenerator" "^7.4.5"
+    "@babel/plugin-transform-reserved-words" "^7.2.0"
+    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
+    "@babel/plugin-transform-spread" "^7.6.2"
+    "@babel/plugin-transform-sticky-regex" "^7.2.0"
+    "@babel/plugin-transform-template-literals" "^7.4.4"
+    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
+    "@babel/plugin-transform-unicode-regex" "^7.6.2"
+    "@babel/types" "^7.6.0"
+    browserslist "^4.6.0"
+    core-js-compat "^3.1.1"
+    invariant "^2.2.2"
+    js-levenshtein "^1.1.3"
+    semver "^5.5.0"
+
+"@babel/preset-env@^7.9.0":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.2.tgz#715930f2cf8573b0928005ee562bed52fb65fdfb"
   integrity sha512-MjqhX0RZaEgK/KueRzh+3yPSk30oqDKJ5HP5tqTSB1e2gzGS3PLy7K0BIpnp78+0anFuSwOeuCf1zZO7RzRvEA==
@@ -1717,62 +1773,6 @@
     levenary "^1.1.1"
     semver "^5.5.0"
 
-"@babel/preset-env@^7.4.5":
-  version "7.6.2"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3"
-  integrity sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==
-  dependencies:
-    "@babel/helper-module-imports" "^7.0.0"
-    "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
-    "@babel/plugin-proposal-dynamic-import" "^7.5.0"
-    "@babel/plugin-proposal-json-strings" "^7.2.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.6.2"
-    "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-proposal-unicode-property-regex" "^7.6.2"
-    "@babel/plugin-syntax-async-generators" "^7.2.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.2.0"
-    "@babel/plugin-syntax-json-strings" "^7.2.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-    "@babel/plugin-transform-arrow-functions" "^7.2.0"
-    "@babel/plugin-transform-async-to-generator" "^7.5.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
-    "@babel/plugin-transform-block-scoping" "^7.6.2"
-    "@babel/plugin-transform-classes" "^7.5.5"
-    "@babel/plugin-transform-computed-properties" "^7.2.0"
-    "@babel/plugin-transform-destructuring" "^7.6.0"
-    "@babel/plugin-transform-dotall-regex" "^7.6.2"
-    "@babel/plugin-transform-duplicate-keys" "^7.5.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
-    "@babel/plugin-transform-for-of" "^7.4.4"
-    "@babel/plugin-transform-function-name" "^7.4.4"
-    "@babel/plugin-transform-literals" "^7.2.0"
-    "@babel/plugin-transform-member-expression-literals" "^7.2.0"
-    "@babel/plugin-transform-modules-amd" "^7.5.0"
-    "@babel/plugin-transform-modules-commonjs" "^7.6.0"
-    "@babel/plugin-transform-modules-systemjs" "^7.5.0"
-    "@babel/plugin-transform-modules-umd" "^7.2.0"
-    "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.2"
-    "@babel/plugin-transform-new-target" "^7.4.4"
-    "@babel/plugin-transform-object-super" "^7.5.5"
-    "@babel/plugin-transform-parameters" "^7.4.4"
-    "@babel/plugin-transform-property-literals" "^7.2.0"
-    "@babel/plugin-transform-regenerator" "^7.4.5"
-    "@babel/plugin-transform-reserved-words" "^7.2.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.2.0"
-    "@babel/plugin-transform-spread" "^7.6.2"
-    "@babel/plugin-transform-sticky-regex" "^7.2.0"
-    "@babel/plugin-transform-template-literals" "^7.4.4"
-    "@babel/plugin-transform-typeof-symbol" "^7.2.0"
-    "@babel/plugin-transform-unicode-regex" "^7.6.2"
-    "@babel/types" "^7.6.0"
-    browserslist "^4.6.0"
-    core-js-compat "^3.1.1"
-    invariant "^2.2.2"
-    js-levenshtein "^1.1.3"
-    semver "^5.5.0"
-
 "@babel/preset-modules@^0.1.3":
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
@@ -1798,13 +1798,6 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
-"@babel/runtime@^7.10.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.0":
-  version "7.10.2"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
-  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
 "@babel/runtime@^7.2.0":
   version "7.5.4"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
@@ -1819,6 +1812,13 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
+"@babel/runtime@^7.8.4", "@babel/runtime@^7.9.0":
+  version "7.10.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
+  integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.8.7":
   version "7.9.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
@@ -2076,11 +2076,6 @@
   dependencies:
     ember-cli-babel "^7.1.3"
 
-"@ember/edition-utils@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@ember/edition-utils/-/edition-utils-1.2.0.tgz#a039f542dc14c8e8299c81cd5abba95e2459cfa6"
-  integrity sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==
-
 "@ember/optional-features@^0.7.0":
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/@ember/optional-features/-/optional-features-0.7.0.tgz#f65a858007020ddfb8342f586112750c32abd2d9"
@@ -4055,7 +4050,7 @@ babel-plugin-debug-macros@^0.3.0:
   dependencies:
     semver "^5.3.0"
 
-babel-plugin-debug-macros@^0.3.2, babel-plugin-debug-macros@^0.3.3:
+babel-plugin-debug-macros@^0.3.2:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.3.3.tgz#29c3449d663f61c7385f5b8c72d8015b069a5cb7"
   integrity sha512-E+NI8TKpxJDBbVkdWkwHrKgJi696mnRL8XYrOPYw82veNHPDORM9WIQifl6TpIo8PNy2tU2skPqbfkmHXrHKQA==
@@ -4138,11 +4133,6 @@ babel-plugin-htmlbars-inline-precompile@^1.0.0:
   resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-1.0.0.tgz#a9d2f6eaad8a3f3d361602de593a8cbef8179c22"
   integrity sha512-4jvKEHR1bAX03hBDZ94IXsYCj3bwk9vYsn6ux6JZNL2U5pvzCWjqyrGahfsGNrhERyxw8IqcirOi9Q6WCo3dkQ==
 
-babel-plugin-htmlbars-inline-precompile@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-3.1.0.tgz#85085b50385277f2b331ebd54e22fa91aadc24e8"
-  integrity sha512-ar6c4YVX6OV7Dzpq7xRyllQrHwVEzJf41qysYULnD6tu6TS+y1FxT2VcEvMC6b9Rq9xoHMzvB79HO3av89JCGg==
-
 babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.4.5:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz#41f7ead616fc36f6a93180e89697f69f51671181"
@@ -5012,23 +5002,6 @@ broccoli-babel-transpiler@^7.4.0:
     rsvp "^4.8.4"
     workerpool "^3.1.1"
 
-broccoli-babel-transpiler@^7.5.0:
-  version "7.5.0"
-  resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-7.5.0.tgz#6137e2c9324eebe1d83b6a01c7b445bfc5612d49"
-  integrity sha512-CrLrI8HX7mDqVR+/r8WiTZuQQh6hGMGwal9jyKk+kpk7t/4hqL6oQ06FRt81kazbHm4bil4WJ+kGB+aOfAx+XA==
-  dependencies:
-    "@babel/core" "^7.10.2"
-    "@babel/polyfill" "^7.10.1"
-    broccoli-funnel "^2.0.2"
-    broccoli-merge-trees "^3.0.2"
-    broccoli-persistent-filter "^2.2.1"
-    clone "^2.1.2"
-    hash-for-dep "^1.4.7"
-    heimdalljs-logger "^0.1.9"
-    json-stable-stringify "^1.0.1"
-    rsvp "^4.8.4"
-    workerpool "^3.1.1"
-
 broccoli-builder@^0.18.14:
   version "0.18.14"
   resolved "https://registry.yarnpkg.com/broccoli-builder/-/broccoli-builder-0.18.14.tgz#4b79e2f844de11a4e1b816c3f49c6df4776c312d"
@@ -5329,11 +5302,6 @@ broccoli-module-unification-reexporter@^1.0.0:
     mkdirp "^0.5.1"
     walk-sync "^0.3.2"
 
-broccoli-node-api@^1.6.0, broccoli-node-api@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz#391aa6edecd2a42c63c111b4162956b2fa288cb6"
-  integrity sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==
-
 broccoli-node-info@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-1.1.0.tgz#3aa2e31e07e5bdb516dd25214f7c45ba1c459412"
@@ -5344,13 +5312,6 @@ broccoli-node-info@^2.1.0:
   resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-2.1.0.tgz#ca84560e8570ff78565bea1699866ddbf58ad644"
   integrity sha512-l6qDuboJThHfRVVWQVaTs++bFdrFTP0gJXgsWenczc1PavRVUmL1Eyb2swTAXXMpDOnr2zhNOBLx4w9AxkqbPQ==
 
-broccoli-output-wrapper@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/broccoli-output-wrapper/-/broccoli-output-wrapper-2.0.0.tgz#f1e0b9b2f259a67fd41a380141c3c20b096828e6"
-  integrity sha512-V/ozejo+snzNf75i/a6iTmp71k+rlvqjE3+jYfimuMwR1tjNNRdtfno+NGNQB2An9bIAeqZnKhMDurAznHAdtA==
-  dependencies:
-    heimdalljs-logger "^0.1.10"
-
 broccoli-persistent-filter@^1.1.5, broccoli-persistent-filter@^1.1.6, broccoli-persistent-filter@^1.4.3:
   version "1.4.6"
   resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.6.tgz#80762d19000880a77da33c34373299c0f6a3e615"
@@ -5439,19 +5400,6 @@ broccoli-plugin@^2.0.0:
     rimraf "^2.3.4"
     symlink-or-copy "^1.1.8"
 
-broccoli-plugin@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-3.1.0.tgz#54ba6dd90a42ec3db5624063292610e326b1e542"
-  integrity sha512-7w7FP8WJYjLvb0eaw27LO678TGGaom++49O1VYIuzjhXjK5kn2+AMlDm7CaUFw4F7CLGoVQeZ84d8gICMJa4lA==
-  dependencies:
-    broccoli-node-api "^1.6.0"
-    broccoli-output-wrapper "^2.0.0"
-    fs-merger "^3.0.1"
-    promise-map-series "^0.2.1"
-    quick-temp "^0.1.3"
-    rimraf "^2.3.4"
-    symlink-or-copy "^1.1.8"
-
 broccoli-rollup@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/broccoli-rollup/-/broccoli-rollup-2.1.1.tgz#0b77dc4b7560a53e998ea85f3b56772612d4988d"
@@ -7668,38 +7616,6 @@ ember-cli-babel@^7.11.1:
     rimraf "^3.0.1"
     semver "^5.5.0"
 
-ember-cli-babel@^7.19.0:
-  version "7.21.0"
-  resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.21.0.tgz#c79e888876aee87dfc3260aee7cb580b74264bbc"
-  integrity sha512-jHVi9melAibo0DrAG3GAxid+29xEyjBoU53652B4qcu3Xp58feZGTH/JGXovH7TjvbeNn65zgNyoV3bk1onULw==
-  dependencies:
-    "@babel/core" "^7.10.2"
-    "@babel/helper-compilation-targets" "^7.10.2"
-    "@babel/plugin-proposal-class-properties" "^7.10.1"
-    "@babel/plugin-proposal-decorators" "^7.10.1"
-    "@babel/plugin-transform-modules-amd" "^7.10.1"
-    "@babel/plugin-transform-runtime" "^7.10.1"
-    "@babel/plugin-transform-typescript" "^7.10.1"
-    "@babel/polyfill" "^7.10.1"
-    "@babel/preset-env" "^7.10.2"
-    "@babel/runtime" "^7.10.2"
-    amd-name-resolver "^1.2.1"
-    babel-plugin-debug-macros "^0.3.3"
-    babel-plugin-ember-data-packages-polyfill "^0.1.2"
-    babel-plugin-ember-modules-api-polyfill "^2.13.4"
-    babel-plugin-module-resolver "^3.1.1"
-    broccoli-babel-transpiler "^7.5.0"
-    broccoli-debug "^0.6.4"
-    broccoli-funnel "^2.0.1"
-    broccoli-source "^1.1.0"
-    clone "^2.1.2"
-    ember-cli-babel-plugin-helpers "^1.1.0"
-    ember-cli-version-checker "^4.1.0"
-    ensure-posix-path "^1.0.2"
-    fixturify-project "^1.10.0"
-    rimraf "^3.0.1"
-    semver "^5.5.0"
-
 ember-cli-broccoli-sane-watcher@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ember-cli-broccoli-sane-watcher/-/ember-cli-broccoli-sane-watcher-3.0.0.tgz#dc1812c047e1ceec4413d3c41b51a9ffc61b4cfe"
@@ -7804,26 +7720,6 @@ ember-cli-htmlbars@^3.0.1, ember-cli-htmlbars@^3.1.0:
     json-stable-stringify "^1.0.1"
     strip-bom "^3.0.0"
 
-ember-cli-htmlbars@^4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.3.1.tgz#4af8adc21ab3c4953f768956b7f7d207782cb175"
-  integrity sha512-CW6AY/yzjeVqoRtItOKj3hcYzc5dWPRETmeCzr2Iqjt5vxiVtpl0z5VTqHqIlT5fsFx6sGWBQXNHIe+ivYsxXQ==
-  dependencies:
-    "@ember/edition-utils" "^1.2.0"
-    babel-plugin-htmlbars-inline-precompile "^3.0.1"
-    broccoli-debug "^0.6.5"
-    broccoli-persistent-filter "^2.3.1"
-    broccoli-plugin "^3.1.0"
-    common-tags "^1.8.0"
-    ember-cli-babel-plugin-helpers "^1.1.0"
-    fs-tree-diff "^2.0.1"
-    hash-for-dep "^1.5.1"
-    heimdalljs-logger "^0.1.10"
-    json-stable-stringify "^1.0.1"
-    semver "^6.3.0"
-    strip-bom "^4.0.0"
-    walk-sync "^2.0.2"
-
 ember-cli-import-polyfill@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/ember-cli-import-polyfill/-/ember-cli-import-polyfill-0.2.0.tgz#c1a08a8affb45c97b675926272fe78cf4ca166f2"
@@ -8147,15 +8043,6 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.2.0:
     ember-cli-version-checker "^2.1.1"
     semver "^5.4.1"
 
-ember-compatibility-helpers@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.1.tgz#87c92c4303f990ff455c28ca39fb3ee11441aa16"
-  integrity sha512-6wzYvnhg1ihQUT5yGqnLtleq3Nv5KNv79WhrEuNU9SwR4uIxCO+KpyC7r3d5VI0EM7/Nmv9Nd0yTkzmTMdVG1A==
-  dependencies:
-    babel-plugin-debug-macros "^0.2.0"
-    ember-cli-version-checker "^2.1.1"
-    semver "^5.4.1"
-
 ember-composable-helpers@^2.0.3:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-2.3.1.tgz#db98ad8b55d053e2ac216b9da091c9e7a3b9f453"
@@ -8311,16 +8198,6 @@ ember-inline-svg@^0.3.0:
     svgo "~1.2.2"
     walk-sync "^0.3.1"
 
-ember-keyboard@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-6.0.0.tgz#0bf929bfa1a5935109e59c1b816e65ad2db0383b"
-  integrity sha512-QJAhs8173hqy8e/Wj/98+5tJ43I5UKq35lsx3w8icHL5iisfXo8s89gOtn1C2015Y4JoMNsVOkNSCnBfhx1UQg==
-  dependencies:
-    ember-cli-babel "^7.19.0"
-    ember-cli-htmlbars "^4.3.1"
-    ember-compatibility-helpers "^1.2.1"
-    ember-modifier "^1.0.3"
-
 ember-load-initializers@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.0.tgz#b402815ab9c823ff48a1369b52633721987e72d4"
@@ -8356,7 +8233,7 @@ ember-maybe-in-element@^0.4.0:
   dependencies:
     ember-cli-babel "^7.1.0"
 
-ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0:
+ember-modifier-manager-polyfill@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda"
   integrity sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==
@@ -8365,17 +8242,6 @@ ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0:
     ember-cli-version-checker "^2.1.2"
     ember-compatibility-helpers "^1.2.0"
 
-ember-modifier@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-1.0.3.tgz#ab18250666aad17c0d9170feb178e954148eb4ed"
-  integrity sha512-vWuFyvdkULUyasvEXxe5lcfuPZV/Uqe+b0IQ1yU+TY1RSJnFdVUu/CVHT8Bu4HUJInqzAihwPMTwty7fypzi5Q==
-  dependencies:
-    ember-cli-babel "^7.11.1"
-    ember-cli-is-package-missing "^1.0.0"
-    ember-cli-normalize-entity-name "^1.0.0"
-    ember-cli-string-utils "^1.1.0"
-    ember-modifier-manager-polyfill "^1.2.0"
-
 ember-moment@^7.8.1:
   version "7.8.1"
   resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-7.8.1.tgz#6f77cf941d1a92e231b2f4b810e113b2fae50c5f"
@@ -9681,18 +9547,6 @@ fs-extra@^8.0.1:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-merger@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.1.0.tgz#f30f74f6c70b2ff7333ec074f3d2f22298152f3b"
-  integrity sha512-RZ9JtqugaE8Rkt7idO5NSwcxEGSDZpLmVFjtVQUm3f+bWun7JAU6fKyU6ZJUeUnKdJwGx8uaro+K4QQfOR7vpA==
-  dependencies:
-    broccoli-node-api "^1.7.0"
-    broccoli-node-info "^2.1.0"
-    fs-extra "^8.0.1"
-    fs-tree-diff "^2.0.1"
-    rimraf "^2.6.3"
-    walk-sync "^2.0.2"
-
 fs-minipass@^1.2.5:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
@@ -15630,11 +15484,6 @@ strip-bom@^3.0.0:
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
-strip-bom@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
-  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
-
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -16567,15 +16416,6 @@ walk-sync@^1.0.0, walk-sync@^1.1.3:
     ensure-posix-path "^1.1.0"
     matcher-collection "^1.1.1"
 
-walk-sync@^2.0.2:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.1.0.tgz#e214248e5f5cf497ddd87db5a2a02e47e29c6501"
-  integrity sha512-KpH9Xw64LNSx7/UI+3guRZvJWlDxVA4+KKb/4puRoVrG8GkvZRxnF3vhxdjgpoKJGL2TVg1OrtkXIE/VuGPLHQ==
-  dependencies:
-    "@types/minimatch" "^3.0.3"
-    ensure-posix-path "^1.1.0"
-    matcher-collection "^2.0.0"
-
 walker@~1.0.5:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"

From 2fa2ef8311c4b7dfa57b7fcba32057546200e1d7 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Wed, 17 Jun 2020 12:43:22 -0500
Subject: [PATCH 14/24] Add preliminary use of cached collections
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This still means a query will be sent for every search, but
if we’re in a route we know has a watched collection, the
cached collection will be used.
---
 ui/app/components/global-search/control.js | 20 ++++++++++--
 ui/tests/acceptance/search-test.js         | 36 +++++++++++++++++++++-
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 2d5f1d30746..fcb9bd56984 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -55,8 +55,8 @@ export default class GlobalSearchControl extends Component {
       set(this, 'searchString', string);
 
       // FIXME no need to fetch on every search!
-      const jobs = yield this.store.findAll('job');
-      const nodes = yield this.store.findAll('node');
+      const jobs = yield this.fetchJobs();
+      const nodes = yield this.fetchNodes();
 
       set(this, 'jobs', jobs.toArray());
       set(this, 'nodes', nodes.toArray());
@@ -132,6 +132,22 @@ export default class GlobalSearchControl extends Component {
       },
     };
   }
+
+  async fetchJobs() {
+    if (this.router.isActive('jobs')) {
+      return this.store.peekAll('job');
+    } else {
+      return this.store.findAll('job');
+    }
+  }
+
+  async fetchNodes() {
+    if (this.router.isActive('clients')) {
+      return this.store.peekAll('node');
+    } else {
+      return this.store.findAll('node');
+    }
+  }
 }
 
 @classic
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 5f43ba1c0af..4ce0e755284 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -5,11 +5,15 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
 import { selectSearch } from 'ember-power-select/test-support';
 
+function getRequestCount(server, url) {
+  return server.pretender.handledRequests.filterBy('url', url).length;
+}
+
 module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
   setupMirage(hooks);
 
-  test('search searches jobs and nodes and navigates to chosen items', async function(assert) {
+  test('search searches jobs and nodes with route-based caching and navigates to chosen items', async function(assert) {
     server.create('node', { name: 'xyz' });
     const otherNode = server.create('node', { name: 'aaa' });
 
@@ -19,6 +23,9 @@ module('Acceptance | search', function(hooks) {
 
     await visit('/');
 
+    let presearchJobsRequestCount = getRequestCount(server, '/v1/jobs');
+    let presearchNodesRequestCount = getRequestCount(server, '/v1/nodes');
+
     await selectSearch(PageLayout.navbar.search.scope, 'xy');
 
     PageLayout.navbar.search.as(search => {
@@ -38,6 +45,17 @@ module('Acceptance | search', function(hooks) {
       });
     });
 
+    assert.equal(
+      presearchJobsRequestCount,
+      getRequestCount(server, '/v1/jobs'),
+      'no new jobs request should be sent when in the jobs hierarchy'
+    );
+    assert.equal(
+      presearchNodesRequestCount + 1,
+      getRequestCount(server, '/v1/nodes'),
+      'a nodes request should happen when not in the clients hierarchy'
+    );
+
     await PageLayout.navbar.search.groups[0].options[0].click();
     assert.equal(currentURL(), '/jobs/xyz');
 
@@ -45,6 +63,22 @@ module('Acceptance | search', function(hooks) {
 
     await PageLayout.navbar.search.groups[1].options[0].click();
     assert.equal(currentURL(), `/clients/${otherNode.id}`);
+
+    presearchJobsRequestCount = getRequestCount(server, '/v1/jobs');
+    presearchNodesRequestCount = getRequestCount(server, '/v1/nodes');
+
+    await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
+
+    assert.equal(
+      presearchJobsRequestCount + 1,
+      getRequestCount(server, '/v1/jobs'),
+      'a jobs request should happen when not not in the jobs hierarchy'
+    );
+    assert.equal(
+      presearchNodesRequestCount,
+      getRequestCount(server, '/v1/nodes'),
+      'no new nodes request should happen when in the clients hierarchy'
+    );
   });
 
   test('clicking the search field starts search immediately', async function(assert) {

From def4851845a5d6467c1608d47fb90a8f3f7732eb Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 08:47:29 -0500
Subject: [PATCH 15/24] Remove manual vertical positioning of divider

Since the container now has align-items: center, this is
no longer needed.
---
 ui/app/styles/core/navbar.scss | 1 -
 1 file changed, 1 deletion(-)

diff --git a/ui/app/styles/core/navbar.scss b/ui/app/styles/core/navbar.scss
index 1ef5197921a..9018db8afb8 100644
--- a/ui/app/styles/core/navbar.scss
+++ b/ui/app/styles/core/navbar.scss
@@ -37,7 +37,6 @@
           display: block;
           position: absolute;
           left: 0px;
-          top: 1.25em; // FIXME positioning is off with .is-primary align-items: center
         }
       }
     }

From a9f97cb868d9d2a006083e25f2961b1015075b52 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 10:42:48 -0500
Subject: [PATCH 16/24] Add time-based caching
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This isn’t perfect as it only updates the last-fetched time
of a watched collection when a search is performed. It also
has the cache duration as 5s which is probably too short.
---
 ui/app/components/global-search/control.js | 22 ++------------
 ui/app/services/data-caches.js             | 34 ++++++++++++++++++++++
 ui/tests/acceptance/search-test.js         | 32 ++++++++++++++++----
 3 files changed, 63 insertions(+), 25 deletions(-)
 create mode 100644 ui/app/services/data-caches.js

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index fcb9bd56984..6ef42e2da3a 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -12,6 +12,7 @@ const SLASH_KEY = 191;
 
 @tagName('')
 export default class GlobalSearchControl extends Component {
+  @service dataCaches;
   @service router;
   @service store;
 
@@ -54,9 +55,8 @@ export default class GlobalSearchControl extends Component {
     try {
       set(this, 'searchString', string);
 
-      // FIXME no need to fetch on every search!
-      const jobs = yield this.fetchJobs();
-      const nodes = yield this.fetchNodes();
+      const jobs = yield this.dataCaches.fetch('job');
+      const nodes = yield this.dataCaches.fetch('node');
 
       set(this, 'jobs', jobs.toArray());
       set(this, 'nodes', nodes.toArray());
@@ -132,22 +132,6 @@ export default class GlobalSearchControl extends Component {
       },
     };
   }
-
-  async fetchJobs() {
-    if (this.router.isActive('jobs')) {
-      return this.store.peekAll('job');
-    } else {
-      return this.store.findAll('job');
-    }
-  }
-
-  async fetchNodes() {
-    if (this.router.isActive('clients')) {
-      return this.store.peekAll('node');
-    } else {
-      return this.store.findAll('node');
-    }
-  }
 }
 
 @classic
diff --git a/ui/app/services/data-caches.js b/ui/app/services/data-caches.js
new file mode 100644
index 00000000000..98c100088ca
--- /dev/null
+++ b/ui/app/services/data-caches.js
@@ -0,0 +1,34 @@
+import Service, { inject as service } from '@ember/service';
+
+export const COLLECTION_CACHE_DURATION = 5000;
+
+export default class DataCachesService extends Service {
+  @service router;
+  @service store;
+  @service system;
+
+  collectionLastFetched = {};
+
+  async fetch(modelName) {
+    // Could this be dynamically generated based on use of watchers? 🤔
+    const modelNameToRoute = {
+      job: 'jobs',
+      node: 'clients',
+    };
+
+    const route = modelNameToRoute[modelName];
+    const lastFetched = this.collectionLastFetched[modelName];
+    const now = Date.now();
+
+    if (this.router.isActive(route)) {
+      // FIXME 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);
+    }
+  }
+}
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 4ce0e755284..304778f1f4e 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -4,6 +4,9 @@ import { setupApplicationTest } from 'ember-qunit';
 import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
 import { selectSearch } from 'ember-power-select/test-support';
+import sinon from 'sinon';
+
+import { COLLECTION_CACHE_DURATION } from 'nomad-ui/services/data-caches';
 
 function getRequestCount(server, url) {
   return server.pretender.handledRequests.filterBy('url', url).length;
@@ -13,7 +16,7 @@ module('Acceptance | search', function(hooks) {
   setupApplicationTest(hooks);
   setupMirage(hooks);
 
-  test('search searches jobs and nodes with route-based caching and navigates to chosen items', async function(assert) {
+  test('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) {
     server.create('node', { name: 'xyz' });
     const otherNode = server.create('node', { name: 'aaa' });
 
@@ -23,6 +26,11 @@ module('Acceptance | search', function(hooks) {
 
     await visit('/');
 
+    const clock = sinon.useFakeTimers({
+      now: new Date(),
+      shouldAdvanceTime: true,
+    });
+
     let presearchJobsRequestCount = getRequestCount(server, '/v1/jobs');
     let presearchNodesRequestCount = getRequestCount(server, '/v1/nodes');
 
@@ -46,13 +54,13 @@ module('Acceptance | search', function(hooks) {
     });
 
     assert.equal(
-      presearchJobsRequestCount,
       getRequestCount(server, '/v1/jobs'),
+      presearchJobsRequestCount,
       'no new jobs request should be sent when in the jobs hierarchy'
     );
     assert.equal(
-      presearchNodesRequestCount + 1,
       getRequestCount(server, '/v1/nodes'),
+      presearchNodesRequestCount + 1,
       'a nodes request should happen when not in the clients hierarchy'
     );
 
@@ -67,18 +75,30 @@ module('Acceptance | search', function(hooks) {
     presearchJobsRequestCount = getRequestCount(server, '/v1/jobs');
     presearchNodesRequestCount = getRequestCount(server, '/v1/nodes');
 
-    await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
+    await selectSearch(PageLayout.navbar.search.scope, 'zzzzzzzzzzz');
 
     assert.equal(
-      presearchJobsRequestCount + 1,
       getRequestCount(server, '/v1/jobs'),
-      'a jobs request should happen when not not in the jobs hierarchy'
+      presearchJobsRequestCount,
+      'a jobs request should not because the cache hasn’t expired'
     );
     assert.equal(
       presearchNodesRequestCount,
       getRequestCount(server, '/v1/nodes'),
       'no new nodes request should happen when in the clients hierarchy'
     );
+
+    clock.tick(COLLECTION_CACHE_DURATION * 2);
+
+    await selectSearch(PageLayout.navbar.search.scope, otherNode.id.substr(0, 3));
+
+    assert.equal(
+      getRequestCount(server, '/v1/jobs'),
+      presearchJobsRequestCount + 1,
+      'a jobs request should happen because the cache has expired'
+    );
+
+    clock.restore();
   });
 
   test('clicking the search field starts search immediately', async function(assert) {

From 95904b7fdd13e0c8093ac65d638c8429034b2a2d Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 10:59:02 -0500
Subject: [PATCH 17/24] Fix invocation of search component

---
 ui/app/templates/components/global-header.hbs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs
index 4fe743ba46e..ef05d0a5a4d 100644
--- a/ui/app/templates/components/global-header.hbs
+++ b/ui/app/templates/components/global-header.hbs
@@ -7,7 +7,7 @@
       {{partial "partials/nomad-logo"}}
     </LinkTo>
   </div>
-  {{global-search/control}}
+  <GlobalSearch::Control />
   <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>

From 44c4f1c82a06fe0cfc6172501e0f847cdf5a3f15 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 11:00:08 -0500
Subject: [PATCH 18/24] Add media query to hide search on mobile

---
 ui/app/templates/components/global-header.hbs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs
index ef05d0a5a4d..e129a42c321 100644
--- a/ui/app/templates/components/global-header.hbs
+++ b/ui/app/templates/components/global-header.hbs
@@ -7,7 +7,9 @@
       {{partial "partials/nomad-logo"}}
     </LinkTo>
   </div>
-  <GlobalSearch::Control />
+  {{#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>

From 93602c0c88065ab5527155c7587b61eb292e2382 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 12:32:36 -0500
Subject: [PATCH 19/24] Change cache duration

---
 ui/app/services/data-caches.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/app/services/data-caches.js b/ui/app/services/data-caches.js
index 98c100088ca..fe7fe7b82a8 100644
--- a/ui/app/services/data-caches.js
+++ b/ui/app/services/data-caches.js
@@ -1,6 +1,6 @@
 import Service, { inject as service } from '@ember/service';
 
-export const COLLECTION_CACHE_DURATION = 5000;
+export const COLLECTION_CACHE_DURATION = 60000; // one minute
 
 export default class DataCachesService extends Service {
   @service router;

From fd14f250de7fc9b03fcfe28359e04f0e26d7659a Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 12:34:12 -0500
Subject: [PATCH 20/24] Fix assertion description

---
 ui/tests/acceptance/search-test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index 304778f1f4e..c488476944c 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -80,7 +80,7 @@ module('Acceptance | search', function(hooks) {
     assert.equal(
       getRequestCount(server, '/v1/jobs'),
       presearchJobsRequestCount,
-      'a jobs request should not because the cache hasn’t expired'
+      'a jobs request should not happen because the cache hasn’t expired'
     );
     assert.equal(
       presearchNodesRequestCount,

From 379ad0c160b781110be3583f8cf77fc46a94555a Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 14:29:32 -0500
Subject: [PATCH 21/24] Remove/update notes

---
 ui/app/components/global-search/control.js | 1 -
 ui/app/services/data-caches.js             | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 6ef42e2da3a..3b1b68129ad 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -33,7 +33,6 @@ export default class GlobalSearchControl extends Component {
   keyDownHandler(e) {
     const targetElementName = e.target.nodeName.toLowerCase();
 
-    // FIXME are more and/or other-approach exceptions needed?
     if (targetElementName != 'input' && targetElementName != 'textarea') {
       if (e.keyCode === SLASH_KEY) {
         e.preventDefault();
diff --git a/ui/app/services/data-caches.js b/ui/app/services/data-caches.js
index fe7fe7b82a8..c72433e881b 100644
--- a/ui/app/services/data-caches.js
+++ b/ui/app/services/data-caches.js
@@ -21,7 +21,7 @@ export default class DataCachesService extends Service {
     const now = Date.now();
 
     if (this.router.isActive(route)) {
-      // FIXME Incorrect because it’s constantly being fetched by watchers, shouldn’t be marked as last fetched only on search
+      // 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) {

From 894de232ec6a0f42e3bbbff8db6648dc7038778a Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 14:46:19 -0500
Subject: [PATCH 22/24] Remove/update notes

---
 ui/app/services/data-caches.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/ui/app/services/data-caches.js b/ui/app/services/data-caches.js
index c72433e881b..7617c61c583 100644
--- a/ui/app/services/data-caches.js
+++ b/ui/app/services/data-caches.js
@@ -10,7 +10,6 @@ export default class DataCachesService extends Service {
   collectionLastFetched = {};
 
   async fetch(modelName) {
-    // Could this be dynamically generated based on use of watchers? 🤔
     const modelNameToRoute = {
       job: 'jobs',
       node: 'clients',

From 5749ec1cc8cfe73df23aa978138b1873c70db280 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Thu, 18 Jun 2020 14:47:05 -0500
Subject: [PATCH 23/24] Change non-slash-stealing test to use page objects

---
 ui/tests/acceptance/jobs-list-test.js |  4 ++--
 ui/tests/acceptance/search-test.js    | 11 ++++-------
 ui/tests/pages/jobs/list.js           |  7 +++++--
 3 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js
index 1a23555abcb..bf0d7b1a55a 100644
--- a/ui/tests/acceptance/jobs-list-test.js
+++ b/ui/tests/acceptance/jobs-list-test.js
@@ -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');
   });
@@ -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');
   });
diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js
index c488476944c..3a8240223b7 100644
--- a/ui/tests/acceptance/search-test.js
+++ b/ui/tests/acceptance/search-test.js
@@ -1,8 +1,9 @@
 import { module, test } from 'qunit';
-import { click, currentURL, triggerEvent, visit } from '@ember/test-helpers';
+import { currentURL, triggerEvent, visit } from '@ember/test-helpers';
 import { setupApplicationTest } from 'ember-qunit';
 import { setupMirage } from 'ember-cli-mirage/test-support';
 import PageLayout from 'nomad-ui/tests/pages/layout';
+import JobsList from 'nomad-ui/tests/pages/jobs/list';
 import { selectSearch } from 'ember-power-select/test-support';
 import sinon from 'sinon';
 
@@ -131,12 +132,8 @@ module('Acceptance | search', function(hooks) {
 
     assert.notOk(PageLayout.navbar.search.field.isPresent);
 
-    // FIXME use page objects for this and below? 🤔
-    await click('.search-box input');
-
-    await triggerEvent('.search-box input', 'keydown', {
-      keyCode: 191, // slash
-    });
+    await JobsList.search.click();
+    await JobsList.search.keydown({ keyCode: 191 });
 
     assert.notOk(PageLayout.navbar.search.field.isPresent);
   });
diff --git a/ui/tests/pages/jobs/list.js b/ui/tests/pages/jobs/list.js
index 2cad7087e0f..425ede744b8 100644
--- a/ui/tests/pages/jobs/list.js
+++ b/ui/tests/pages/jobs/list.js
@@ -3,10 +3,10 @@ import {
   create,
   collection,
   clickable,
-  fillable,
   isPresent,
   property,
   text,
+  triggerable,
   visitable,
 } from 'ember-cli-page-object';
 
@@ -18,7 +18,10 @@ export default create({
 
   visit: visitable('/jobs'),
 
-  search: fillable('[data-test-jobs-search] input'),
+  search: {
+    scope: '[data-test-jobs-search] input',
+    keydown: triggerable('keydown'),
+  },
 
   runJobButton: {
     scope: '[data-test-run-job]',

From e4e7c78ddbcd445d228adfee9a0e989d42989ce5 Mon Sep 17 00:00:00 2001
From: Buck Doyle <buck@hashicorp.com>
Date: Fri, 19 Jun 2020 08:25:34 -0500
Subject: [PATCH 24/24] Add namespace to job navigation

---
 ui/app/components/global-search/control.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js
index 3b1b68129ad..1a061ba8943 100644
--- a/ui/app/components/global-search/control.js
+++ b/ui/app/components/global-search/control.js
@@ -92,7 +92,9 @@ export default class GlobalSearchControl extends Component {
     const itemModelName = model.constructor.modelName;
 
     if (itemModelName === 'job') {
-      this.router.transitionTo('jobs.job', model.name);
+      this.router.transitionTo('jobs.job', model.name, {
+        queryParams: { namespace: model.get('namespace.name') },
+      });
     } else if (itemModelName === 'node') {
       this.router.transitionTo('clients.client', model.id);
     }