Merge pull request #4458 from hashicorp/f-ui-refactor-breadcrumbs

UI: Refactor breadcrumbs
This commit is contained in:
Michael Lange 2018-07-10 11:01:28 -07:00 committed by GitHub
commit 38b4d30272
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 455 additions and 337 deletions

View File

@ -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'),
});

View File

@ -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);

View File

@ -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')],
},
];
}),
});

View File

@ -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'),

View File

@ -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')],
},
]);
}),
});

View File

@ -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') || [])

View File

@ -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'),
});

View File

@ -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',
}),
],
},
];
}),
});

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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'),
});

View File

@ -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);

View File

@ -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);

View File

@ -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)

View File

@ -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) {

View File

@ -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');
},

View File

@ -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'));

View File

@ -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');
},

View File

@ -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') };
},

View File

@ -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.

View File

@ -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');
},

View File

@ -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;
}),
});

View File

@ -1,3 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
{{app-breadcrumbs}}
{{/global-header}}
{{outlet}}
</div>

View File

@ -1,14 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to
data-test-breadcrumb=breadcrumb.label
params=breadcrumb.args}}
{{breadcrumb.label}}
{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section">
<h1 data-test-title class="title">

View File

@ -1,14 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to
data-test-breadcrumb=breadcrumb.label
params=breadcrumb.args}}
{{breadcrumb.label}}
{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "allocations/allocation/task/subnav"}}
<section class="section">

View File

@ -1,14 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to
data-test-breadcrumb=breadcrumb.label
params=breadcrumb.args}}
{{breadcrumb.label}}
{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "allocations/allocation/task/subnav"}}
<section class="section">

View File

@ -1,3 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
{{app-breadcrumbs}}
{{/global-header}}
{{outlet}}
</div>

View File

@ -1,11 +1,3 @@
{{#global-header class="page-header"}}
<li>
{{#link-to "clients.index" data-test-breadcrumb="clients"}}Clients{{/link-to}}
</li>
<li class="is-active">
{{#link-to "clients.client" model.id data-test-breadcrumb="client"}}{{model.shortId}}{{/link-to}}
</li>
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section">
<h1 data-test-title class="title">

View File

@ -1,8 +1,3 @@
{{#global-header class="page-header"}}
<li class="is-active">
{{#link-to "clients.index"}}Clients{{/link-to}}
</li>
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section">
{{#if isForbidden}}

View File

@ -1,8 +1,3 @@
{{#global-header class="page-header"}}
<li class="is-active">
{{#link-to "clients.index"}}Clients{{/link-to}}
</li>
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section has-text-centered">{{partial "partials/loading-spinner"}}</section>
{{/gutter-menu}}

View File

@ -0,0 +1,13 @@
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#if breadcrumb.isPending}}
<a href="#" aria-label="loading" data-test-breadcrumb="loading">&hellip;</a>
{{else}}
{{#link-to
params=breadcrumb.args
data-test-breadcrumb=breadcrumb.args.firstObject}}
{{breadcrumb.label}}
{{/link-to}}
{{/if}}
</li>
{{/each}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to data-test-breadcrumb=breadcrumb.label params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}

View File

@ -1,3 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
{{app-breadcrumbs}}
{{/global-header}}
{{outlet}}
</div>

View File

@ -1,8 +1,3 @@
{{#global-header class="page-header"}}
<li class="is-active">
{{#link-to "jobs.index"}}Jobs{{/link-to}}
</li>
{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "refresh")}}
<section class="section">
{{#if isForbidden}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
<section class="section">

View File

@ -1,13 +1,6 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
<section class="section">
{{job-deployments-stream deployments=deployments}}
{{job-deployments-stream deployments=model.deployments}}
</section>
{{/gutter-menu}}

View File

@ -1,10 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "jobs/job/subnav"}}
<section class="section has-text-centered">{{partial "partials/loading-spinner"}}</section>

View File

@ -1,14 +1,3 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to
data-test-breadcrumb=breadcrumb.label
params=breadcrumb.args}}
{{breadcrumb.label}}
{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
<div class="tabs is-subnav">
<ul>

View File

@ -1,13 +1,6 @@
{{#global-header class="page-header"}}
{{#each breadcrumbs as |breadcrumb index|}}
<li class="{{if (eq (inc index) breadcrumbs.length) "is-active"}}">
{{#link-to params=breadcrumb.args}}{{breadcrumb.label}}{{/link-to}}
</li>
{{/each}}
{{/global-header}}
{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}}
{{partial "jobs/job/subnav"}}
<section class="section">
{{job-versions-stream versions=versions verbose=true}}
{{job-versions-stream versions=model.versions verbose=true}}
</section>
{{/gutter-menu}}

View File

@ -1,7 +1,5 @@
{{#global-header class="page-header"}}
<li class="is-active">
{{#link-to "jobs.index"}}Jobs{{/link-to}}
</li>
{{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section has-text-centered">{{partial "partials/loading-spinner"}}</section>

View File

@ -1,5 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
{{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section has-text-centered">{{partial "partials/loading-spinner"}}</section>

View File

@ -1,8 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
<li class="is-active">
{{#link-to "servers.index"}}Servers{{/link-to}}
</li>
{{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
<section class="section">

View File

@ -1,5 +1,6 @@
<div class="page-layout">
{{#global-header class="page-header"}}
{{app-breadcrumbs}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{outlet}}

View File

@ -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)];
}
};

View File

@ -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(() => {

View File

@ -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(

View File

@ -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(),

View File

@ -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'
);
});
});

View File

@ -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'
);
});