diff --git a/ui/app/components/app-breadcrumbs.js b/ui/app/components/app-breadcrumbs.js
new file mode 100644
index 000000000000..28a08a85dd95
--- /dev/null
+++ b/ui/app/components/app-breadcrumbs.js
@@ -0,0 +1,11 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { reads } from '@ember/object/computed';
+
+export default Component.extend({
+ breadcrumbsService: service('breadcrumbs'),
+
+ tagName: '',
+
+ breadcrumbs: reads('breadcrumbsService.breadcrumbs'),
+});
diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js
index d2922c2afed2..01f63b2cf7c2 100644
--- a/ui/app/components/job-page/abstract.js
+++ b/ui/app/components/job-page/abstract.js
@@ -1,7 +1,5 @@
import Component from '@ember/component';
-import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
-import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
export default Component.extend({
system: service(),
@@ -20,23 +18,6 @@ export default Component.extend({
// Set to a { title, description } to surface an error
errorMessage: null,
- breadcrumbs: computed('job.{name,id}', function() {
- const job = this.get('job');
- return [
- { label: 'Jobs', args: ['jobs'] },
- {
- label: job.get('name'),
- args: [
- 'jobs.job',
- job,
- qpBuilder({
- jobNamespace: job.get('namespace.name') || 'default',
- }),
- ],
- },
- ];
- }),
-
actions: {
clearErrorMessage() {
this.set('errorMessage', null);
diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js
deleted file mode 100644
index 4f44f27bf2ac..000000000000
--- a/ui/app/controllers/allocations/allocation.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Controller from '@ember/controller';
-import { computed } from '@ember/object';
-import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
-
-export default Controller.extend({
- breadcrumbs: computed('model.job', function() {
- return [
- { label: 'Jobs', args: ['jobs'] },
- {
- label: this.get('model.job.name'),
- args: [
- 'jobs.job',
- this.get('model.job.plainId'),
- qpBuilder({
- jobNamespace: this.get('model.job.namespace.name') || 'default',
- }),
- ],
- },
- {
- label: this.get('model.taskGroupName'),
- args: [
- 'jobs.job.task-group',
- this.get('model.job'),
- this.get('model.taskGroupName'),
- qpBuilder({
- jobNamespace: this.get('model.namespace.name') || 'default',
- }),
- ],
- },
- {
- label: this.get('model.shortId'),
- args: ['allocations.allocation', this.get('model')],
- },
- ];
- }),
-});
diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js
index 1131d6b7fab4..b6fd778bae21 100644
--- a/ui/app/controllers/allocations/allocation/index.js
+++ b/ui/app/controllers/allocations/allocation/index.js
@@ -1,11 +1,9 @@
import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import Sortable from 'nomad-ui/mixins/sortable';
import { lazyClick } from 'nomad-ui/helpers/lazy-click';
export default Controller.extend(Sortable, {
- allocationController: controller('allocations.allocation'),
-
queryParams: {
sortProperty: 'sort',
sortDescending: 'desc',
@@ -14,8 +12,6 @@ export default Controller.extend(Sortable, {
sortProperty: 'name',
sortDescending: false,
- breadcrumbs: alias('allocationController.breadcrumbs'),
-
listToSort: alias('model.states'),
sortedStates: alias('listSorted'),
diff --git a/ui/app/controllers/allocations/allocation/task.js b/ui/app/controllers/allocations/allocation/task.js
deleted file mode 100644
index 616df57a3a5d..000000000000
--- a/ui/app/controllers/allocations/allocation/task.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Controller, { inject as controller } from '@ember/controller';
-import { computed } from '@ember/object';
-
-export default Controller.extend({
- allocationController: controller('allocations.allocation'),
-
- breadcrumbs: computed('allocationController.breadcrumbs.[]', 'model.name', function() {
- return this.get('allocationController.breadcrumbs').concat([
- {
- label: this.get('model.name'),
- args: ['allocations.allocation.task', this.get('model.allocation'), this.get('model')],
- },
- ]);
- }),
-});
diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js
index e45f85230ee2..c020bc4a2b61 100644
--- a/ui/app/controllers/allocations/allocation/task/index.js
+++ b/ui/app/controllers/allocations/allocation/task/index.js
@@ -1,12 +1,8 @@
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
export default Controller.extend({
- taskController: controller('allocations.allocation.task'),
-
- breadcrumbs: alias('taskController.breadcrumbs'),
-
network: alias('model.resources.networks.firstObject'),
ports: computed('network.reservedPorts.[]', 'network.dynamicPorts.[]', function() {
return (this.get('network.reservedPorts') || [])
diff --git a/ui/app/controllers/allocations/allocation/task/logs.js b/ui/app/controllers/allocations/allocation/task/logs.js
deleted file mode 100644
index 6ce1642b542e..000000000000
--- a/ui/app/controllers/allocations/allocation/task/logs.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Controller, { inject as controller } from '@ember/controller';
-import { alias } from '@ember/object/computed';
-
-export default Controller.extend({
- taskController: controller('allocations.allocation.task'),
- breadcrumbs: alias('taskController.breadcrumbs'),
-});
diff --git a/ui/app/controllers/jobs/job.js b/ui/app/controllers/jobs/job.js
deleted file mode 100644
index 29c87a2a2e49..000000000000
--- a/ui/app/controllers/jobs/job.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import Controller from '@ember/controller';
-import { computed } from '@ember/object';
-import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
-
-export default Controller.extend({
- breadcrumbs: computed('model.{name,id}', function() {
- return [
- { label: 'Jobs', args: ['jobs'] },
- {
- label: this.get('model.name'),
- args: [
- 'jobs.job',
- this.get('model.plainId'),
- qpBuilder({
- jobNamespace: this.get('model.namespace.name') || 'default',
- }),
- ],
- },
- ];
- }),
-});
diff --git a/ui/app/controllers/jobs/job/definition.js b/ui/app/controllers/jobs/job/definition.js
index b105b72b0633..f209d3547ad6 100644
--- a/ui/app/controllers/jobs/job/definition.js
+++ b/ui/app/controllers/jobs/job/definition.js
@@ -1,11 +1,4 @@
-import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
-export default Controller.extend(WithNamespaceResetting, {
- jobController: controller('jobs.job'),
-
- job: alias('model.job'),
-
- breadcrumbs: alias('jobController.breadcrumbs'),
-});
+export default Controller.extend(WithNamespaceResetting);
diff --git a/ui/app/controllers/jobs/job/deployments.js b/ui/app/controllers/jobs/job/deployments.js
index 0540c98b1293..f209d3547ad6 100644
--- a/ui/app/controllers/jobs/job/deployments.js
+++ b/ui/app/controllers/jobs/job/deployments.js
@@ -1,12 +1,4 @@
-import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
-export default Controller.extend(WithNamespaceResetting, {
- jobController: controller('jobs.job'),
-
- job: alias('model'),
- deployments: alias('model.deployments'),
-
- breadcrumbs: alias('jobController.breadcrumbs'),
-});
+export default Controller.extend(WithNamespaceResetting);
diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js
index 8dc0ae650d09..617874ba5798 100644
--- a/ui/app/controllers/jobs/job/index.js
+++ b/ui/app/controllers/jobs/job/index.js
@@ -1,13 +1,10 @@
import { inject as service } from '@ember/service';
-import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
export default Controller.extend(WithNamespaceResetting, {
system: service(),
- jobController: controller('jobs.job'),
-
queryParams: {
currentPage: 'page',
sortProperty: 'sort',
@@ -19,9 +16,6 @@ export default Controller.extend(WithNamespaceResetting, {
sortProperty: 'name',
sortDescending: false,
- breadcrumbs: alias('jobController.breadcrumbs'),
- job: alias('model'),
-
actions: {
gotoTaskGroup(taskGroup) {
this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup);
diff --git a/ui/app/controllers/jobs/job/loading.js b/ui/app/controllers/jobs/job/loading.js
deleted file mode 100644
index 2251e2d758c1..000000000000
--- a/ui/app/controllers/jobs/job/loading.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
-
-export default Controller.extend({
- jobController: controller('jobs.job'),
- breadcrumbs: alias('jobController.breadcrumbs'),
-});
diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js
index 1ace41f8fe81..6f8740ce03ac 100644
--- a/ui/app/controllers/jobs/job/task-group.js
+++ b/ui/app/controllers/jobs/job/task-group.js
@@ -1,14 +1,11 @@
import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import { computed } from '@ember/object';
import Sortable from 'nomad-ui/mixins/sortable';
import Searchable from 'nomad-ui/mixins/searchable';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
-import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, {
- jobController: controller('jobs.job'),
-
queryParams: {
currentPage: 'page',
searchTerm: 'search',
@@ -32,19 +29,6 @@ export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, {
listToSearch: alias('listSorted'),
sortedAllocations: alias('listSearched'),
- breadcrumbs: computed('jobController.breadcrumbs.[]', 'model.{name}', function() {
- return this.get('jobController.breadcrumbs').concat([
- {
- label: this.get('model.name'),
- args: [
- 'jobs.job.task-group',
- this.get('model.name'),
- qpBuilder({ jobNamespace: this.get('model.job.namespace.name') || 'default' }),
- ],
- },
- ]);
- }),
-
actions: {
gotoAllocation(allocation) {
this.transitionToRoute('allocations.allocation', allocation);
diff --git a/ui/app/controllers/jobs/job/versions.js b/ui/app/controllers/jobs/job/versions.js
index eb669a22b728..f209d3547ad6 100644
--- a/ui/app/controllers/jobs/job/versions.js
+++ b/ui/app/controllers/jobs/job/versions.js
@@ -1,12 +1,4 @@
-import { alias } from '@ember/object/computed';
-import Controller, { inject as controller } from '@ember/controller';
+import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
-export default Controller.extend(WithNamespaceResetting, {
- jobController: controller('jobs.job'),
-
- job: alias('model'),
- versions: alias('model.versions'),
-
- breadcrumbs: alias('jobController.breadcrumbs'),
-});
+export default Controller.extend(WithNamespaceResetting);
diff --git a/ui/app/routes/allocations/allocation.js b/ui/app/routes/allocations/allocation.js
index b09dc1cb376f..9a73e184ac4f 100644
--- a/ui/app/routes/allocations/allocation.js
+++ b/ui/app/routes/allocations/allocation.js
@@ -3,12 +3,38 @@ import { collect } from '@ember/object/computed';
import { watchRecord } from 'nomad-ui/utils/properties/watch';
import WithWatchers from 'nomad-ui/mixins/with-watchers';
import notifyError from 'nomad-ui/utils/notify-error';
+import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
+import { jobCrumbs } from 'nomad-ui/utils/breadcrumb-utils';
export default Route.extend(WithWatchers, {
startWatchers(controller, model) {
controller.set('watcher', this.get('watch').perform(model));
},
+ // Allocation breadcrumbs extend from job / task group breadcrumbs
+ // even though the route structure does not.
+ breadcrumbs(model) {
+ return [
+ { label: 'Jobs', args: ['jobs.index'] },
+ ...jobCrumbs(model.get('job')),
+ {
+ label: model.get('taskGroupName'),
+ args: [
+ 'jobs.job.task-group',
+ model.get('job'),
+ model.get('taskGroupName'),
+ qpBuilder({
+ jobNamespace: model.get('namespace.name') || 'default',
+ }),
+ ],
+ },
+ {
+ label: model.get('shortId'),
+ args: ['allocations.allocation', model],
+ },
+ ];
+ },
+
model() {
// Preload the job for the allocation since it's required for the breadcrumb trail
return this._super(...arguments)
diff --git a/ui/app/routes/allocations/allocation/task.js b/ui/app/routes/allocations/allocation/task.js
index dcf2bda101f1..c33797290545 100644
--- a/ui/app/routes/allocations/allocation/task.js
+++ b/ui/app/routes/allocations/allocation/task.js
@@ -5,6 +5,16 @@ import EmberError from '@ember/error';
export default Route.extend({
store: service(),
+ breadcrumbs(model) {
+ if (!model) return [];
+ return [
+ {
+ label: model.get('name'),
+ args: ['allocations.allocation.task', model.get('allocation'), model],
+ },
+ ];
+ },
+
model({ name }) {
const allocation = this.modelFor('allocations.allocation');
if (allocation) {
diff --git a/ui/app/routes/clients.js b/ui/app/routes/clients.js
index 49559c8c96bd..b31c96990a32 100644
--- a/ui/app/routes/clients.js
+++ b/ui/app/routes/clients.js
@@ -8,6 +8,13 @@ export default Route.extend(WithForbiddenState, {
store: service(),
system: service(),
+ breadcrumbs: [
+ {
+ label: 'Clients',
+ args: ['clients.index'],
+ },
+ ],
+
beforeModel() {
return this.get('system.leader');
},
diff --git a/ui/app/routes/clients/client.js b/ui/app/routes/clients/client.js
index f049d1efe2f1..9e2493030030 100644
--- a/ui/app/routes/clients/client.js
+++ b/ui/app/routes/clients/client.js
@@ -12,6 +12,16 @@ export default Route.extend(WithWatchers, {
return this._super(...arguments).catch(notifyError(this));
},
+ breadcrumbs(model) {
+ if (!model) return [];
+ return [
+ {
+ label: model.get('shortId'),
+ args: ['clients.client', model.get('id')],
+ },
+ ];
+ },
+
afterModel(model) {
if (model && model.get('isPartial')) {
return model.reload().then(node => node.get('allocations'));
diff --git a/ui/app/routes/jobs.js b/ui/app/routes/jobs.js
index 745e326e2e26..ed9178375511 100644
--- a/ui/app/routes/jobs.js
+++ b/ui/app/routes/jobs.js
@@ -8,6 +8,13 @@ export default Route.extend(WithForbiddenState, {
system: service(),
store: service(),
+ breadcrumbs: [
+ {
+ label: 'Jobs',
+ args: ['jobs.index'],
+ },
+ ],
+
beforeModel() {
return this.get('system.namespaces');
},
diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js
index 86b784508ceb..b424fe997063 100644
--- a/ui/app/routes/jobs/job.js
+++ b/ui/app/routes/jobs/job.js
@@ -2,11 +2,14 @@ import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import notifyError from 'nomad-ui/utils/notify-error';
+import { jobCrumbs } from 'nomad-ui/utils/breadcrumb-utils';
export default Route.extend({
store: service(),
token: service(),
+ breadcrumbs: jobCrumbs,
+
serialize(model) {
return { job_name: model.get('plainId') };
},
diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js
index 40051732492b..a219f252f75f 100644
--- a/ui/app/routes/jobs/job/task-group.js
+++ b/ui/app/routes/jobs/job/task-group.js
@@ -2,8 +2,23 @@ import Route from '@ember/routing/route';
import { collect } from '@ember/object/computed';
import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch';
import WithWatchers from 'nomad-ui/mixins/with-watchers';
+import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
export default Route.extend(WithWatchers, {
+ breadcrumbs(model) {
+ if (!model) return [];
+ return [
+ {
+ label: model.get('name'),
+ args: [
+ 'jobs.job.task-group',
+ model.get('name'),
+ qpBuilder({ jobNamespace: model.get('job.namespace.name') || 'default' }),
+ ],
+ },
+ ];
+ },
+
model({ name }) {
// If the job is a partial (from the list request) it won't have task
// groups. Reload the job to ensure task groups are present.
diff --git a/ui/app/routes/servers.js b/ui/app/routes/servers.js
index 49559c8c96bd..8f69d7b14f23 100644
--- a/ui/app/routes/servers.js
+++ b/ui/app/routes/servers.js
@@ -8,6 +8,13 @@ export default Route.extend(WithForbiddenState, {
store: service(),
system: service(),
+ breadcrumbs: [
+ {
+ label: 'Servers',
+ args: ['servers.index'],
+ },
+ ],
+
beforeModel() {
return this.get('system.leader');
},
diff --git a/ui/app/services/breadcrumbs.js b/ui/app/services/breadcrumbs.js
new file mode 100644
index 000000000000..a86e9c3738f5
--- /dev/null
+++ b/ui/app/services/breadcrumbs.js
@@ -0,0 +1,42 @@
+import { getOwner } from '@ember/application';
+import Service, { inject as service } from '@ember/service';
+import { computed } from '@ember/object';
+
+export default Service.extend({
+ router: service(),
+
+ // currentURL is only used to listen to all transitions.
+ // currentRouteName has all information necessary to compute breadcrumbs,
+ // but it doesn't change when a transition to the same route with a different
+ // model occurs.
+ breadcrumbs: computed('router.currentURL', 'router.currentRouteName', function() {
+ const owner = getOwner(this);
+ const allRoutes = (this.get('router.currentRouteName') || '')
+ .split('.')
+ .without('')
+ .map((segment, index, allSegments) => allSegments.slice(0, index + 1).join('.'));
+
+ let crumbs = [];
+ allRoutes.forEach(routeName => {
+ const route = owner.lookup(`route:${routeName}`);
+
+ // Routes can reset the breadcrumb trail to start anew even
+ // if the route is deeply nested.
+ if (route.get('resetBreadcrumbs')) {
+ crumbs = [];
+ }
+
+ // Breadcrumbs are either an array of static crumbs
+ // or a function that returns breadcrumbs given the current
+ // model for the route's controller.
+ let breadcrumbs = route.get('breadcrumbs') || [];
+ if (typeof breadcrumbs === 'function') {
+ breadcrumbs = breadcrumbs(route.get('controller.model')) || [];
+ }
+
+ crumbs.push(...breadcrumbs);
+ });
+
+ return crumbs;
+ }),
+});
diff --git a/ui/app/templates/allocations.hbs b/ui/app/templates/allocations.hbs
index 8fab3547f4c7..a766b9ff647d 100644
--- a/ui/app/templates/allocations.hbs
+++ b/ui/app/templates/allocations.hbs
@@ -1,3 +1,6 @@
+ {{#global-header class="page-header"}}
+ {{app-breadcrumbs}}
+ {{/global-header}}
{{outlet}}
diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs
index 7abb77bfc7f9..34389a657fce 100644
--- a/ui/app/templates/allocations/allocation/index.hbs
+++ b/ui/app/templates/allocations/allocation/index.hbs
@@ -1,14 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to
- data-test-breadcrumb=breadcrumb.label
- params=breadcrumb.args}}
- {{breadcrumb.label}}
- {{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body"}}
diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs
index ac13a5034869..d24ec3819ec3 100644
--- a/ui/app/templates/allocations/allocation/task/index.hbs
+++ b/ui/app/templates/allocations/allocation/task/index.hbs
@@ -1,14 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to
- data-test-breadcrumb=breadcrumb.label
- params=breadcrumb.args}}
- {{breadcrumb.label}}
- {{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "allocations/allocation/task/subnav"}}
diff --git a/ui/app/templates/allocations/allocation/task/logs.hbs b/ui/app/templates/allocations/allocation/task/logs.hbs
index d4ee3da568b4..6799e39d6efb 100644
--- a/ui/app/templates/allocations/allocation/task/logs.hbs
+++ b/ui/app/templates/allocations/allocation/task/logs.hbs
@@ -1,14 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to
- data-test-breadcrumb=breadcrumb.label
- params=breadcrumb.args}}
- {{breadcrumb.label}}
- {{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "allocations/allocation/task/subnav"}}
diff --git a/ui/app/templates/clients.hbs b/ui/app/templates/clients.hbs
index 8fab3547f4c7..a766b9ff647d 100644
--- a/ui/app/templates/clients.hbs
+++ b/ui/app/templates/clients.hbs
@@ -1,3 +1,6 @@
+ {{#global-header class="page-header"}}
+ {{app-breadcrumbs}}
+ {{/global-header}}
{{outlet}}
diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs
index af3d918fa7a4..f83069ccfc1c 100644
--- a/ui/app/templates/clients/client.hbs
+++ b/ui/app/templates/clients/client.hbs
@@ -1,11 +1,3 @@
-{{#global-header class="page-header"}}
-
- {{#link-to "clients.index" data-test-breadcrumb="clients"}}Clients{{/link-to}}
-
-
- {{#link-to "clients.client" model.id data-test-breadcrumb="client"}}{{model.shortId}}{{/link-to}}
-
-{{/global-header}}
{{#gutter-menu class="page-body"}}
diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs
index 83ccc379619d..2f7d0894f1d6 100644
--- a/ui/app/templates/clients/index.hbs
+++ b/ui/app/templates/clients/index.hbs
@@ -1,8 +1,3 @@
-{{#global-header class="page-header"}}
-
- {{#link-to "clients.index"}}Clients{{/link-to}}
-
-{{/global-header}}
{{#gutter-menu class="page-body"}}
{{#if isForbidden}}
diff --git a/ui/app/templates/clients/loading.hbs b/ui/app/templates/clients/loading.hbs
index 4bfa29cb2ae5..3298bc9c0745 100644
--- a/ui/app/templates/clients/loading.hbs
+++ b/ui/app/templates/clients/loading.hbs
@@ -1,8 +1,3 @@
-{{#global-header class="page-header"}}
-
- {{#link-to "clients.index"}}Clients{{/link-to}}
-
-{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "partials/loading-spinner"}}
{{/gutter-menu}}
diff --git a/ui/app/templates/components/app-breadcrumbs.hbs b/ui/app/templates/components/app-breadcrumbs.hbs
new file mode 100644
index 000000000000..cf5ec56d0425
--- /dev/null
+++ b/ui/app/templates/components/app-breadcrumbs.hbs
@@ -0,0 +1,13 @@
+{{#each breadcrumbs as |breadcrumb index|}}
+
+ {{#if breadcrumb.isPending}}
+ …
+ {{else}}
+ {{#link-to
+ params=breadcrumb.args
+ data-test-breadcrumb=breadcrumb.args.firstObject}}
+ {{breadcrumb.label}}
+ {{/link-to}}
+ {{/if}}
+
+{{/each}}
diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs
index 1ea0cedf9e08..d9f8a1fa6753 100644
--- a/ui/app/templates/components/job-page/batch.hbs
+++ b/ui/app/templates/components/job-page/batch.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs
index d9104b57114e..f0fcad10e250 100644
--- a/ui/app/templates/components/job-page/parameterized-child.hbs
+++ b/ui/app/templates/components/job-page/parameterized-child.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs
index b976e73a8954..8fca16924a41 100644
--- a/ui/app/templates/components/job-page/parameterized.hbs
+++ b/ui/app/templates/components/job-page/parameterized.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs
index 117a866799ef..398cb9327048 100644
--- a/ui/app/templates/components/job-page/periodic-child.hbs
+++ b/ui/app/templates/components/job-page/periodic-child.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs
index 31e0ffe32e3a..bc70771dccb2 100644
--- a/ui/app/templates/components/job-page/periodic.hbs
+++ b/ui/app/templates/components/job-page/periodic.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs
index b724b7ab5afb..f65dd4b6f06b 100644
--- a/ui/app/templates/components/job-page/service.hbs
+++ b/ui/app/templates/components/job-page/service.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs
index 1ea0cedf9e08..d9f8a1fa6753 100644
--- a/ui/app/templates/components/job-page/system.hbs
+++ b/ui/app/templates/components/job-page/system.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
diff --git a/ui/app/templates/jobs.hbs b/ui/app/templates/jobs.hbs
index 8fab3547f4c7..a766b9ff647d 100644
--- a/ui/app/templates/jobs.hbs
+++ b/ui/app/templates/jobs.hbs
@@ -1,3 +1,6 @@
+ {{#global-header class="page-header"}}
+ {{app-breadcrumbs}}
+ {{/global-header}}
{{outlet}}
diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs
index 8767c2355ab3..7b409cadbba1 100644
--- a/ui/app/templates/jobs/index.hbs
+++ b/ui/app/templates/jobs/index.hbs
@@ -1,8 +1,3 @@
-{{#global-header class="page-header"}}
-
- {{#link-to "jobs.index"}}Jobs{{/link-to}}
-
-{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "refresh")}}
{{#if isForbidden}}
diff --git a/ui/app/templates/jobs/job/definition.hbs b/ui/app/templates/jobs/job/definition.hbs
index d2722fa6a4f2..924d180002ea 100644
--- a/ui/app/templates/jobs/job/definition.hbs
+++ b/ui/app/templates/jobs/job/definition.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
diff --git a/ui/app/templates/jobs/job/deployments.hbs b/ui/app/templates/jobs/job/deployments.hbs
index c293bfaef7e9..241a0d32d843 100644
--- a/ui/app/templates/jobs/job/deployments.hbs
+++ b/ui/app/templates/jobs/job/deployments.hbs
@@ -1,13 +1,6 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
- {{job-deployments-stream deployments=deployments}}
+ {{job-deployments-stream deployments=model.deployments}}
{{/gutter-menu}}
diff --git a/ui/app/templates/jobs/job/loading.hbs b/ui/app/templates/jobs/job/loading.hbs
index 666de358d35a..a90321c319a1 100644
--- a/ui/app/templates/jobs/job/loading.hbs
+++ b/ui/app/templates/jobs/job/loading.hbs
@@ -1,10 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "jobs/job/subnav"}}
{{partial "partials/loading-spinner"}}
diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs
index 702168f2275a..b2b08c70162b 100644
--- a/ui/app/templates/jobs/job/task-group.hbs
+++ b/ui/app/templates/jobs/job/task-group.hbs
@@ -1,14 +1,3 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
-
- {{#link-to
- data-test-breadcrumb=breadcrumb.label
- params=breadcrumb.args}}
- {{breadcrumb.label}}
- {{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
diff --git a/ui/app/templates/jobs/job/versions.hbs b/ui/app/templates/jobs/job/versions.hbs
index 6aadbe617232..9b03e23ad364 100644
--- a/ui/app/templates/jobs/job/versions.hbs
+++ b/ui/app/templates/jobs/job/versions.hbs
@@ -1,13 +1,6 @@
-{{#global-header class="page-header"}}
- {{#each breadcrumbs as |breadcrumb index|}}
- -
- {{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
-
- {{/each}}
-{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
- {{job-versions-stream versions=versions verbose=true}}
+ {{job-versions-stream versions=model.versions verbose=true}}
{{/gutter-menu}}
diff --git a/ui/app/templates/jobs/loading.hbs b/ui/app/templates/jobs/loading.hbs
index 58bc6ef5b04c..39002f28aa7f 100644
--- a/ui/app/templates/jobs/loading.hbs
+++ b/ui/app/templates/jobs/loading.hbs
@@ -1,7 +1,5 @@
{{#global-header class="page-header"}}
- -
- {{#link-to "jobs.index"}}Jobs{{/link-to}}
-
+ {{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "partials/loading-spinner"}}
diff --git a/ui/app/templates/loading.hbs b/ui/app/templates/loading.hbs
index 14013c55beb2..206f1ebac97d 100644
--- a/ui/app/templates/loading.hbs
+++ b/ui/app/templates/loading.hbs
@@ -1,5 +1,6 @@
{{#global-header class="page-header"}}
+ {{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "partials/loading-spinner"}}
diff --git a/ui/app/templates/servers.hbs b/ui/app/templates/servers.hbs
index 6a639191d051..a2bdbc84db45 100644
--- a/ui/app/templates/servers.hbs
+++ b/ui/app/templates/servers.hbs
@@ -1,8 +1,6 @@
{{#global-header class="page-header"}}
-
-
- {{#link-to "servers.index"}}Servers{{/link-to}}
-
+ {{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
diff --git a/ui/app/templates/settings.hbs b/ui/app/templates/settings.hbs
index e781df8a2f1c..2e4ddc936321 100644
--- a/ui/app/templates/settings.hbs
+++ b/ui/app/templates/settings.hbs
@@ -1,5 +1,6 @@
{{#global-header class="page-header"}}
+ {{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{outlet}}
diff --git a/ui/app/utils/breadcrumb-utils.js b/ui/app/utils/breadcrumb-utils.js
new file mode 100644
index 000000000000..699d3f87522c
--- /dev/null
+++ b/ui/app/utils/breadcrumb-utils.js
@@ -0,0 +1,28 @@
+import PromiseObject from 'nomad-ui/utils/classes/promise-object';
+import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
+
+export const jobCrumb = job => ({
+ label: job.get('trimmedName'),
+ args: [
+ 'jobs.job.index',
+ job.get('plainId'),
+ qpBuilder({
+ jobNamespace: job.get('namespace.name') || 'default',
+ }),
+ ],
+});
+
+export const jobCrumbs = job => {
+ if (!job) return [];
+
+ if (job.get('parent.content')) {
+ return [
+ PromiseObject.create({
+ promise: job.get('parent').then(parent => jobCrumb(parent)),
+ }),
+ jobCrumb(job),
+ ];
+ } else {
+ return [jobCrumb(job)];
+ }
+};
diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js
index 72204b92b2d1..3d72e868deba 100644
--- a/ui/tests/acceptance/client-detail-test.js
+++ b/ui/tests/acceptance/client-detail-test.js
@@ -26,19 +26,19 @@ test('/clients/:id should have a breadcrumb trail linking back to clients', func
andThen(() => {
assert.equal(
- find('[data-test-breadcrumb="clients"]').textContent.trim(),
+ find('[data-test-breadcrumb="clients.index"]').textContent.trim(),
'Clients',
'First breadcrumb says clients'
);
assert.equal(
- find('[data-test-breadcrumb="client"]').textContent.trim(),
+ find('[data-test-breadcrumb="clients.client"]').textContent.trim(),
node.id.split('-')[0],
'Second breadcrumb says the node short id'
);
});
andThen(() => {
- click(find('[data-test-breadcrumb="clients"]'));
+ click(find('[data-test-breadcrumb="clients.index"]'));
});
andThen(() => {
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
index ee9e83821f5d..712913f1002f 100644
--- a/ui/tests/acceptance/task-detail-test.js
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -37,32 +37,32 @@ test('breadcrumbs match jobs / job / task group / allocation / task', function(a
const shortId = allocation.id.split('-')[0];
assert.equal(
- find('[data-test-breadcrumb="Jobs"]').textContent.trim(),
+ find('[data-test-breadcrumb="jobs.index"]').textContent.trim(),
'Jobs',
'Jobs is the first breadcrumb'
);
assert.equal(
- find(`[data-test-breadcrumb="${job.name}"]`).textContent.trim(),
+ find('[data-test-breadcrumb="jobs.job.index"]').textContent.trim(),
job.name,
'Job is the second breadcrumb'
);
assert.equal(
- find(`[data-test-breadcrumb="${taskGroup}`).textContent.trim(),
+ find('[data-test-breadcrumb="jobs.job.task-group"]').textContent.trim(),
taskGroup,
'Task Group is the third breadcrumb'
);
assert.equal(
- find(`[data-test-breadcrumb="${shortId}"]`).textContent.trim(),
+ find('[data-test-breadcrumb="allocations.allocation"]').textContent.trim(),
shortId,
'Allocation short id is the fourth breadcrumb'
);
assert.equal(
- find(`[data-test-breadcrumb="${task.name}"]`).textContent.trim(),
+ find('[data-test-breadcrumb="allocations.allocation.task"]').textContent.trim(),
task.name,
'Task name is the fifth breadcrumb'
);
- click('[data-test-breadcrumb="Jobs"]');
+ click('[data-test-breadcrumb="jobs.index"]');
andThen(() => {
assert.equal(currentURL(), '/jobs', 'Jobs breadcrumb links correctly');
});
@@ -70,7 +70,7 @@ test('breadcrumbs match jobs / job / task group / allocation / task', function(a
visit(`/allocations/${allocation.id}/${task.name}`);
});
andThen(() => {
- click(`[data-test-breadcrumb="${job.name}"]`);
+ click('[data-test-breadcrumb="jobs.job.index"]');
});
andThen(() => {
assert.equal(currentURL(), `/jobs/${job.id}`, 'Job breadcrumb links correctly');
@@ -79,7 +79,7 @@ test('breadcrumbs match jobs / job / task group / allocation / task', function(a
visit(`/allocations/${allocation.id}/${task.name}`);
});
andThen(() => {
- click(`[data-test-breadcrumb="${taskGroup}"]`);
+ click('[data-test-breadcrumb="jobs.job.task-group"]');
});
andThen(() => {
assert.equal(
@@ -92,7 +92,7 @@ test('breadcrumbs match jobs / job / task group / allocation / task', function(a
visit(`/allocations/${allocation.id}/${task.name}`);
});
andThen(() => {
- click(`[data-test-breadcrumb="${shortId}"]`);
+ click('[data-test-breadcrumb="allocations.allocation"]');
});
andThen(() => {
assert.equal(
diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js
index 7db6f6f27452..7c99f4a18c40 100644
--- a/ui/tests/acceptance/task-group-detail-test.js
+++ b/ui/tests/acceptance/task-group-detail-test.js
@@ -86,31 +86,31 @@ test('/jobs/:id/:task-group should list high-level metrics for the allocation',
test('/jobs/:id/:task-group should have breadcrumbs for job and jobs', function(assert) {
assert.equal(
- find('[data-test-breadcrumb="Jobs"]').textContent.trim(),
+ find('[data-test-breadcrumb="jobs.index"]').textContent.trim(),
'Jobs',
'First breadcrumb says jobs'
);
assert.equal(
- find(`[data-test-breadcrumb="${job.name}"]`).textContent.trim(),
+ find('[data-test-breadcrumb="jobs.job.index"]').textContent.trim(),
job.name,
'Second breadcrumb says the job name'
);
assert.equal(
- find(`[data-test-breadcrumb="${taskGroup.name}"]`).textContent.trim(),
+ find('[data-test-breadcrumb="jobs.job.task-group"]').textContent.trim(),
taskGroup.name,
'Third breadcrumb says the job name'
);
});
test('/jobs/:id/:task-group first breadcrumb should link to jobs', function(assert) {
- click('[data-test-breadcrumb="Jobs"]');
+ click('[data-test-breadcrumb="jobs.index"]');
andThen(() => {
assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs');
});
});
test('/jobs/:id/:task-group second breadcrumb should link to the job for the task group', function(assert) {
- click(`[data-test-breadcrumb="${job.name}"]`);
+ click('[data-test-breadcrumb="jobs.job.index"]');
andThen(() => {
assert.equal(
currentURL(),
diff --git a/ui/tests/integration/app-breadcrumbs-test.js b/ui/tests/integration/app-breadcrumbs-test.js
new file mode 100644
index 000000000000..c046791613e2
--- /dev/null
+++ b/ui/tests/integration/app-breadcrumbs-test.js
@@ -0,0 +1,85 @@
+import Service from '@ember/service';
+import { getOwner } from '@ember/application';
+import RSVP from 'rsvp';
+import { test, moduleForComponent } from 'ember-qunit';
+import { findAll } from 'ember-native-dom-helpers';
+import wait from 'ember-test-helpers/wait';
+import hbs from 'htmlbars-inline-precompile';
+import PromiseObject from 'nomad-ui/utils/classes/promise-object';
+
+moduleForComponent('app-breadcrumbs', 'Integration | Component | app breadcrumbs', {
+ integration: true,
+ beforeEach() {
+ const mockBreadcrumbs = Service.extend({
+ breadcrumbs: [],
+ });
+
+ this.register('service:breadcrumbs', mockBreadcrumbs);
+ this.breadcrumbs = getOwner(this).lookup('service:breadcrumbs');
+ },
+});
+
+const commonCrumbs = [{ label: 'One', args: ['one'] }, { label: 'Two', args: ['two'] }];
+
+const template = hbs`
+ {{app-breadcrumbs}}
+`;
+
+test('breadcrumbs comes from the breadcrumbs service', function(assert) {
+ this.breadcrumbs.set('breadcrumbs', commonCrumbs);
+
+ this.render(template);
+
+ assert.equal(
+ findAll('[data-test-breadcrumb]').length,
+ commonCrumbs.length,
+ 'The number of crumbs matches the crumbs from the service'
+ );
+});
+
+test('every breadcrumb is rendered correctly', function(assert) {
+ this.breadcrumbs.set('breadcrumbs', commonCrumbs);
+
+ this.render(template);
+
+ const renderedCrumbs = findAll('[data-test-breadcrumb]');
+
+ renderedCrumbs.forEach((crumb, index) => {
+ assert.equal(
+ crumb.textContent.trim(),
+ commonCrumbs[index].label,
+ `Crumb ${index} is ${commonCrumbs[index].label}`
+ );
+ });
+});
+
+test('when breadcrumbs are pending promises, an ellipsis is rendered', function(assert) {
+ let resolvePromise;
+ const promise = new RSVP.Promise(resolve => {
+ resolvePromise = resolve;
+ });
+
+ this.breadcrumbs.set('breadcrumbs', [
+ { label: 'One', args: ['one'] },
+ PromiseObject.create({ promise }),
+ { label: 'Three', args: ['three'] },
+ ]);
+
+ this.render(template);
+
+ assert.equal(
+ findAll('[data-test-breadcrumb]')[1].textContent.trim(),
+ '…',
+ 'Promise breadcrumb is in a loading state'
+ );
+
+ resolvePromise({ label: 'Two', args: ['two'] });
+
+ return wait().then(() => {
+ assert.equal(
+ findAll('[data-test-breadcrumb]')[1].textContent.trim(),
+ 'Two',
+ 'Promise breadcrumb has resolved and now renders Two'
+ );
+ });
+});
diff --git a/ui/tests/unit/services/breadcrumbs-test.js b/ui/tests/unit/services/breadcrumbs-test.js
new file mode 100644
index 000000000000..6ec833556c12
--- /dev/null
+++ b/ui/tests/unit/services/breadcrumbs-test.js
@@ -0,0 +1,149 @@
+import Service from '@ember/service';
+import Route from '@ember/routing/route';
+import Controller from '@ember/controller';
+import { get } from '@ember/object';
+import { alias } from '@ember/object/computed';
+import { getOwner } from '@ember/application';
+import RSVP from 'rsvp';
+import { moduleFor, test } from 'ember-qunit';
+import PromiseObject from 'nomad-ui/utils/classes/promise-object';
+
+const makeRoute = (crumbs, controller = {}) =>
+ Route.extend({
+ breadcrumbs: crumbs,
+ controller: Controller.extend(controller).create(),
+ });
+
+moduleFor('service:breadcrumbs', 'Unit | Service | Breadcrumbs', {
+ beforeEach() {
+ const mockRouter = Service.extend({
+ currentRouteName: 'application',
+ currentURL: '/',
+ });
+
+ this.register('service:router', mockRouter);
+ this.router = getOwner(this).lookup('service:router');
+
+ const none = makeRoute();
+ const fixed = makeRoute([{ label: 'Static', args: ['static.index'] }]);
+ const manyFixed = makeRoute([
+ { label: 'Static 1', args: ['static.index', 1] },
+ { label: 'Static 2', args: ['static.index', 2] },
+ ]);
+ const dynamic = makeRoute(model => [{ label: model, args: ['dynamic.index', model] }], {
+ model: 'Label of the Crumb',
+ });
+ const manyDynamic = makeRoute(
+ model => [
+ { label: get(model, 'fishOne'), args: ['dynamic.index', get(model, 'fishOne')] },
+ { label: get(model, 'fishTwo'), args: ['dynamic.index', get(model, 'fishTwo')] },
+ ],
+ {
+ model: {
+ fishOne: 'red',
+ fishTwo: 'blue',
+ },
+ }
+ );
+ const promise = makeRoute([
+ PromiseObject.create({
+ promise: RSVP.Promise.resolve({
+ label: 'delayed',
+ args: ['wait.for.it'],
+ }),
+ }),
+ ]);
+ const fromURL = makeRoute(model => [{ label: model, args: ['url'] }], {
+ router: getOwner(this).lookup('service:router'),
+ model: alias('router.currentURL'),
+ });
+
+ this.register('route:none', none);
+ this.register('route:none.more-none', none);
+ this.register('route:static', fixed);
+ this.register('route:static.many', manyFixed);
+ this.register('route:dynamic', dynamic);
+ this.register('route:dynamic.many', manyDynamic);
+ this.register('route:promise', promise);
+ this.register('route:url', fromURL);
+ },
+
+ subject() {
+ return getOwner(this)
+ .factoryFor('service:breadcrumbs')
+ .create();
+ },
+});
+
+test('when the route hierarchy has no breadcrumbs', function(assert) {
+ this.router.set('currentRouteName', 'none');
+
+ const service = this.subject();
+ assert.deepEqual(service.get('breadcrumbs'), []);
+});
+
+test('when the route hierarchy has one segment with static crumbs', function(assert) {
+ this.router.set('currentRouteName', 'static');
+
+ const service = this.subject();
+ assert.deepEqual(service.get('breadcrumbs'), [{ label: 'Static', args: ['static.index'] }]);
+});
+
+test('when the route hierarchy has multiple segments with static crumbs', function(assert) {
+ this.router.set('currentRouteName', 'static.many');
+
+ const service = this.subject();
+ assert.deepEqual(service.get('breadcrumbs'), [
+ { label: 'Static', args: ['static.index'] },
+ { label: 'Static 1', args: ['static.index', 1] },
+ { label: 'Static 2', args: ['static.index', 2] },
+ ]);
+});
+
+test('when the route hierarchy has a function as its breadcrumbs property', function(assert) {
+ this.router.set('currentRouteName', 'dynamic');
+
+ const service = this.subject();
+ assert.deepEqual(service.get('breadcrumbs'), [
+ { label: 'Label of the Crumb', args: ['dynamic.index', 'Label of the Crumb'] },
+ ]);
+});
+
+test('when the route hierarchy has multiple segments with dynamic crumbs', function(assert) {
+ this.router.set('currentRouteName', 'dynamic.many');
+
+ const service = this.subject();
+ assert.deepEqual(service.get('breadcrumbs'), [
+ { label: 'Label of the Crumb', args: ['dynamic.index', 'Label of the Crumb'] },
+ { label: 'red', args: ['dynamic.index', 'red'] },
+ { label: 'blue', args: ['dynamic.index', 'blue'] },
+ ]);
+});
+
+test('when a route provides a breadcrumb that is a promise, it gets passed through to the template', function(assert) {
+ this.router.set('currentRouteName', 'promise');
+
+ const service = this.subject();
+ assert.ok(service.get('breadcrumbs.firstObject') instanceof PromiseObject);
+});
+
+// This happens when transitioning to the current route but with a different model
+// jobs.job.index --> jobs.job.index
+// /jobs/one --> /jobs/two
+test('when the route stays the same but the url changes, breadcrumbs get recomputed', function(assert) {
+ this.router.set('currentRouteName', 'url');
+
+ const service = this.subject();
+ assert.deepEqual(
+ service.get('breadcrumbs'),
+ [{ label: '/', args: ['url'] }],
+ 'The label is initially / as is the router currentURL'
+ );
+
+ this.router.set('currentURL', '/somewhere/else');
+ assert.deepEqual(
+ service.get('breadcrumbs'),
+ [{ label: '/somewhere/else', args: ['url'] }],
+ 'The label changes with currentURL since it is an alias and a change to currentURL recomputes breadcrumbs'
+ );
+});