mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
Merge pull request #11590 from hashicorp/e-ui/breadcrumbs-service
Refactor: Breadcrumbs Service
This commit is contained in:
3
.changelog/11590.txt
Normal file
3
.changelog/11590.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
ui: Add titles to breadcrumb labels in app navigation bar
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reads } from '@ember/object/computed';
|
||||
import { tagName } from '@ember-decorators/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
@tagName('')
|
||||
export default class AppBreadcrumbs extends Component {
|
||||
@service('breadcrumbs') breadcrumbsService;
|
||||
|
||||
@reads('breadcrumbsService.breadcrumbs') breadcrumbs;
|
||||
}
|
||||
27
ui/app/components/breadcrumb.js
Normal file
27
ui/app/components/breadcrumb.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { assert } from '@ember/debug';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class Breadcrumb extends Component {
|
||||
@service breadcrumbs;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
assert('Provide a valid breadcrumb argument', this.args.crumb);
|
||||
this.register();
|
||||
}
|
||||
|
||||
@action register() {
|
||||
this.breadcrumbs.registerBreadcrumb(this);
|
||||
}
|
||||
|
||||
@action deregister() {
|
||||
this.breadcrumbs.deregisterBreadcrumb(this);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy();
|
||||
this.deregister();
|
||||
}
|
||||
}
|
||||
1
ui/app/components/breadcrumbs.hbs
Normal file
1
ui/app/components/breadcrumbs.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{yield this.crumbs}}
|
||||
10
ui/app/components/breadcrumbs.js
Normal file
10
ui/app/components/breadcrumbs.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class Breadcrumbs extends Component {
|
||||
@service breadcrumbs;
|
||||
|
||||
get crumbs() {
|
||||
return this.breadcrumbs.crumbs;
|
||||
}
|
||||
}
|
||||
16
ui/app/components/breadcrumbs/default.hbs
Normal file
16
ui/app/components/breadcrumbs/default.hbs
Normal file
@@ -0,0 +1,16 @@
|
||||
<li data-test-breadcrumb-default>
|
||||
<LinkTo @params={{@crumb.args}} data-test-breadcrumb={{@crumb.args.firstObject}}>
|
||||
{{#if @crumb.title}}
|
||||
<dl>
|
||||
<dt>
|
||||
{{@crumb.title}}
|
||||
</dt>
|
||||
<dd>
|
||||
{{@crumb.label}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{else}}
|
||||
{{@crumb.label}}
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
</li>
|
||||
49
ui/app/components/breadcrumbs/job.hbs
Normal file
49
ui/app/components/breadcrumbs/job.hbs
Normal file
@@ -0,0 +1,49 @@
|
||||
<Trigger @onError={{action this.onError}} @do={{this.fetchParent}} as |trigger|>
|
||||
{{did-insert trigger.fns.do}}
|
||||
{{#if trigger.data.isBusy}}
|
||||
<li>
|
||||
<a href="#" aria-label="loading" data-test-breadcrumb="loading">
|
||||
…
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if trigger.data.isSuccess}}
|
||||
{{#if trigger.data.result}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="jobs.job.index"
|
||||
@model={{trigger.data.result.plainId}}
|
||||
@query={{hash namespace=(or trigger.data.result.namespace.name "default")}}
|
||||
data-test-breadcrumb={{"jobs.job.index"}}
|
||||
>
|
||||
<dl>
|
||||
<dt>
|
||||
Parent Job
|
||||
</dt>
|
||||
<dd>
|
||||
{{trigger.data.result.trimmedName}}
|
||||
</dd>
|
||||
</dl>
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="jobs.job.index"
|
||||
@model={{this.job.plainId}}
|
||||
@query={{hash namespace=(or this.job.namespace.name "default")}}
|
||||
data-test-breadcrumb={{"jobs.job.index"}}
|
||||
data-test-job-breadcrumb
|
||||
>
|
||||
<dl>
|
||||
<dt>
|
||||
{{if this.job.hasChildren "Parent Job" "Job"}}
|
||||
</dt>
|
||||
<dd>
|
||||
{{this.job.trimmedName}}
|
||||
</dd>
|
||||
</dl>
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</Trigger>
|
||||
22
ui/app/components/breadcrumbs/job.js
Normal file
22
ui/app/components/breadcrumbs/job.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { assert } from '@ember/debug';
|
||||
import { action } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class BreadcrumbsJob extends Component {
|
||||
get job() {
|
||||
return this.args.crumb.job;
|
||||
}
|
||||
|
||||
@action
|
||||
onError(err) {
|
||||
assert(`Error: ${err.message}`);
|
||||
}
|
||||
|
||||
@action
|
||||
fetchParent() {
|
||||
const hasParent = !!this.job.belongsTo('parent').id();
|
||||
if (hasParent) {
|
||||
return this.job.get('parent');
|
||||
}
|
||||
}
|
||||
}
|
||||
1
ui/app/components/trigger.hbs
Normal file
1
ui/app/components/trigger.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{yield (hash data=this.data fns=this.fns)}}
|
||||
68
ui/app/components/trigger.js
Normal file
68
ui/app/components/trigger.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { action } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
const noOp = () => undefined;
|
||||
|
||||
export default class Trigger extends Component {
|
||||
@tracked error = null;
|
||||
@tracked result = null;
|
||||
|
||||
get isBusy() {
|
||||
return this.triggerTask.isRunning;
|
||||
}
|
||||
|
||||
get isIdle() {
|
||||
return this.triggerTask.isIdle;
|
||||
}
|
||||
|
||||
get isSuccess() {
|
||||
return this.triggerTask.last?.isSuccessful;
|
||||
}
|
||||
|
||||
get isError() {
|
||||
return !!this.error;
|
||||
}
|
||||
|
||||
get fns() {
|
||||
return {
|
||||
do: this.onTrigger,
|
||||
};
|
||||
}
|
||||
|
||||
get onError() {
|
||||
return this.args.onError ?? noOp;
|
||||
}
|
||||
|
||||
get onSuccess() {
|
||||
return this.args.onSuccess ?? noOp;
|
||||
}
|
||||
|
||||
get data() {
|
||||
const { isBusy, isIdle, isSuccess, isError, result } = this;
|
||||
return { isBusy, isIdle, isSuccess, isError, result };
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this.result = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
@task(function*() {
|
||||
this._reset();
|
||||
try {
|
||||
this.result = yield this.args.do();
|
||||
this.onSuccess(this.result);
|
||||
} catch (e) {
|
||||
this.error = { Error: e };
|
||||
this.onError(this.error);
|
||||
}
|
||||
})
|
||||
triggerTask;
|
||||
|
||||
@action
|
||||
onTrigger() {
|
||||
this.triggerTask.perform();
|
||||
}
|
||||
}
|
||||
47
ui/app/controllers/allocations/allocation.js
Normal file
47
ui/app/controllers/allocations/allocation.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
|
||||
|
||||
export default class AllocationsAllocationController extends Controller {
|
||||
@service store;
|
||||
|
||||
get allocation() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get job() {
|
||||
const allocation = this.model;
|
||||
const jobId = allocation.belongsTo('job').id();
|
||||
const job = this.store.peekRecord('job', jobId);
|
||||
return job;
|
||||
}
|
||||
|
||||
get jobNamespace() {
|
||||
const jobNamespaceId = this.job.belongsTo('namespace').id();
|
||||
|
||||
return jobNamespaceId || 'default';
|
||||
}
|
||||
// Allocation breadcrumbs extend from job / task group breadcrumbs
|
||||
// even though the route structure does not.
|
||||
get breadcrumbs() {
|
||||
const { allocation, job, jobNamespace } = this;
|
||||
const jobQueryParams = qpBuilder({
|
||||
jobNamespace,
|
||||
});
|
||||
|
||||
return [
|
||||
{ label: 'Jobs', args: ['jobs.index', jobQueryParams] },
|
||||
{ type: 'job', job: job },
|
||||
{
|
||||
title: 'Task Group',
|
||||
label: allocation.taskGroupName,
|
||||
args: ['jobs.job.task-group', job.plainId, allocation.taskGroupName, jobQueryParams],
|
||||
},
|
||||
{
|
||||
title: 'Allocation',
|
||||
label: allocation.shortId,
|
||||
args: ['allocations.allocation', allocation],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
15
ui/app/controllers/allocations/allocation/task.js
Normal file
15
ui/app/controllers/allocations/allocation/task.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class AllocationsAllocationTaskController extends Controller {
|
||||
get task() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumb() {
|
||||
return {
|
||||
title: 'Task',
|
||||
label: this.task.get('name'),
|
||||
args: ['allocations.allocation.task', this.task.get('allocation'), this.task],
|
||||
};
|
||||
}
|
||||
}
|
||||
15
ui/app/controllers/clients/client.js
Normal file
15
ui/app/controllers/clients/client.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ClientsClientController extends Controller {
|
||||
get client() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumb() {
|
||||
return {
|
||||
title: 'Client',
|
||||
label: this.client.get('shortId'),
|
||||
args: ['clients.client', this.client.get('id')],
|
||||
};
|
||||
}
|
||||
}
|
||||
21
ui/app/controllers/csi/plugins/plugin.js
Normal file
21
ui/app/controllers/csi/plugins/plugin.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class CsiPluginsPluginController extends Controller {
|
||||
get plugin() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const { plainId } = this.plugin;
|
||||
return [
|
||||
{
|
||||
label: 'Plugins',
|
||||
args: ['csi.plugins'],
|
||||
},
|
||||
{
|
||||
label: plainId,
|
||||
args: ['csi.plugins.plugin', plainId],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
|
||||
|
||||
export default class VolumeController extends Controller {
|
||||
// Used in the template
|
||||
@@ -13,6 +14,31 @@ export default class VolumeController extends Controller {
|
||||
];
|
||||
volumeNamespace = 'default';
|
||||
|
||||
get volume() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const volume = this.volume;
|
||||
return [
|
||||
{
|
||||
label: 'Volumes',
|
||||
args: [
|
||||
'csi.volumes',
|
||||
qpBuilder({ volumeNamespace: volume.get('namespace.name') || 'default' }),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: volume.name,
|
||||
args: [
|
||||
'csi.volumes.volume',
|
||||
volume.plainId,
|
||||
qpBuilder({ volumeNamespace: volume.get('namespace.name') || 'default' }),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@computed('model.readAllocations.@each.modifyIndex')
|
||||
get sortedReadAllocations() {
|
||||
return this.model.readAllocations.sortBy('modifyIndex').reverse();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
// The WithNamespaceResetting Mixin uses Controller Injection and requires us to keep this controller around
|
||||
export default class JobsController extends Controller {}
|
||||
|
||||
@@ -7,4 +7,8 @@ export default class JobController extends Controller {
|
||||
},
|
||||
];
|
||||
jobNamespace = 'default';
|
||||
|
||||
get job() {
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
4
ui/app/controllers/jobs/job/dispatch.js
Normal file
4
ui/app/controllers/jobs/job/dispatch.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
// This may be safe to remove but we can't be sure, some route may try access this directly using this.controllerFor
|
||||
export default class JobsJobDispatchController extends Controller {}
|
||||
@@ -5,6 +5,7 @@ import Controller from '@ember/controller';
|
||||
import { action, computed, get } from '@ember/object';
|
||||
import { scheduleOnce } from '@ember/runloop';
|
||||
import intersection from 'lodash.intersection';
|
||||
import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
import Searchable from 'nomad-ui/mixins/searchable';
|
||||
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
|
||||
@@ -13,10 +14,10 @@ import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class TaskGroupController extends Controller.extend(
|
||||
Sortable,
|
||||
Searchable,
|
||||
WithNamespaceResetting
|
||||
) {
|
||||
Sortable,
|
||||
Searchable,
|
||||
WithNamespaceResetting
|
||||
) {
|
||||
@service userSettings;
|
||||
@service can;
|
||||
|
||||
@@ -141,4 +142,22 @@ export default class TaskGroupController extends Controller.extend(
|
||||
setFacetQueryParam(queryParam, selection) {
|
||||
this.set(queryParam, serialize(selection));
|
||||
}
|
||||
|
||||
get taskGroup() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumb() {
|
||||
const { job, name } = this.taskGroup;
|
||||
return {
|
||||
title: 'Task Group',
|
||||
label: name,
|
||||
args: [
|
||||
'jobs.job.task-group',
|
||||
job,
|
||||
name,
|
||||
qpBuilder({ jobNamespace: job.get('namespace.name') || 'default' }),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,16 @@ export default class OptimizeSummaryController extends Controller {
|
||||
jobNamespace: 'namespace',
|
||||
},
|
||||
];
|
||||
|
||||
get summary() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get breadcrumb() {
|
||||
const { slug } = this.summary;
|
||||
return {
|
||||
label: slug.replace('/', ' / '),
|
||||
args: ['optimize.summary', slug],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
7
ui/app/controllers/servers/server.js
Normal file
7
ui/app/controllers/servers/server.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ServersServerController extends Controller {
|
||||
get server() {
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,28 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
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 class AllocationRoute extends Route.extend(WithWatchers) {
|
||||
@service store;
|
||||
|
||||
startWatchers(controller, model) {
|
||||
if (model) {
|
||||
controller.set('watcher', this.watch.perform(model));
|
||||
}
|
||||
}
|
||||
|
||||
// Allocation breadcrumbs extend from job / task group breadcrumbs
|
||||
// even though the route structure does not.
|
||||
breadcrumbs(model) {
|
||||
const jobQueryParams = qpBuilder({
|
||||
jobNamespace: model.get('job.namespace.name') || 'default',
|
||||
});
|
||||
|
||||
return [
|
||||
{ label: 'Jobs', args: ['jobs.index', jobQueryParams] },
|
||||
...jobCrumbs(model.get('job')),
|
||||
{
|
||||
label: model.get('taskGroupName'),
|
||||
args: [
|
||||
'jobs.job.task-group',
|
||||
model.get('job.plainId'),
|
||||
model.get('taskGroupName'),
|
||||
jobQueryParams,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: model.get('shortId'),
|
||||
args: ['allocations.allocation', model],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
model() {
|
||||
// Preload the job for the allocation since it's required for the breadcrumb trail
|
||||
return super
|
||||
.model(...arguments)
|
||||
.then(allocation => allocation.get('job').then(() => allocation))
|
||||
.then(allocation =>
|
||||
allocation
|
||||
.get('job')
|
||||
.then(() => this.store.findAll('namespace')) // namespaces belong to a job and are an asynchronous relationship so we can peak them later on
|
||||
.then(() => allocation)
|
||||
)
|
||||
.catch(notifyError(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,6 @@ import EmberError from '@ember/error';
|
||||
export default class TaskRoute extends Route {
|
||||
@service store;
|
||||
|
||||
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');
|
||||
|
||||
|
||||
@@ -10,13 +10,6 @@ export default class ClientsRoute extends Route.extend(WithForbiddenState) {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Clients',
|
||||
args: ['clients.index'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeModel() {
|
||||
return this.get('system.leader');
|
||||
}
|
||||
|
||||
@@ -9,16 +9,6 @@ export default class ClientRoute extends Route {
|
||||
return super.model(...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'));
|
||||
|
||||
@@ -6,13 +6,6 @@ import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
|
||||
export default class PluginsRoute extends Route.extend(WithForbiddenState) {
|
||||
@service store;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Storage',
|
||||
args: ['csi.index'],
|
||||
},
|
||||
];
|
||||
|
||||
model() {
|
||||
return this.store.query('plugin', { type: 'csi' }).catch(notifyForbidden(this));
|
||||
}
|
||||
|
||||
@@ -6,17 +6,6 @@ export default class PluginRoute extends Route {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = plugin => [
|
||||
{
|
||||
label: 'Plugins',
|
||||
args: ['csi.plugins'],
|
||||
},
|
||||
{
|
||||
label: plugin.plainId,
|
||||
args: ['csi.plugins.plugin', plugin.plainId],
|
||||
},
|
||||
];
|
||||
|
||||
serialize(model) {
|
||||
return { plugin_name: model.get('plainId') };
|
||||
}
|
||||
|
||||
@@ -6,11 +6,4 @@ import classic from 'ember-classic-decorator';
|
||||
export default class VolumesRoute extends Route.extend() {
|
||||
@service system;
|
||||
@service store;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Storage',
|
||||
args: ['csi.index'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Route from '@ember/routing/route';
|
||||
import { collect } from '@ember/object/computed';
|
||||
import RSVP from 'rsvp';
|
||||
import notifyError from 'nomad-ui/utils/notify-error';
|
||||
import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
|
||||
import { watchRecord } from 'nomad-ui/utils/properties/watch';
|
||||
import WithWatchers from 'nomad-ui/mixins/with-watchers';
|
||||
import classic from 'ember-classic-decorator';
|
||||
@@ -13,24 +12,6 @@ export default class VolumeRoute extends Route.extend(WithWatchers) {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = volume => [
|
||||
{
|
||||
label: 'Volumes',
|
||||
args: [
|
||||
'csi.volumes',
|
||||
qpBuilder({ volumeNamespace: volume.get('namespace.name') || 'default' }),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: volume.name,
|
||||
args: [
|
||||
'csi.volumes.volume',
|
||||
volume.plainId,
|
||||
qpBuilder({ volumeNamespace: volume.get('namespace.name') || 'default' }),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
startWatchers(controller, model) {
|
||||
if (!model) return;
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class JobsRoute extends Route.extend() {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Jobs',
|
||||
args: ['jobs.index'],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
@@ -11,8 +10,6 @@ export default class JobRoute extends Route {
|
||||
@service store;
|
||||
@service token;
|
||||
|
||||
breadcrumbs = jobCrumbs;
|
||||
|
||||
serialize(model) {
|
||||
return { job_name: model.get('plainId') };
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import { inject as service } from '@ember/service';
|
||||
export default class DispatchRoute extends Route {
|
||||
@service can;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Dispatch',
|
||||
args: ['jobs.job.dispatch'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeModel() {
|
||||
const job = this.modelFor('jobs.job');
|
||||
const namespace = job.namespace.get('name');
|
||||
|
||||
@@ -4,25 +4,9 @@ import EmberError from '@ember/error';
|
||||
import { resolve, all } from 'rsvp';
|
||||
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';
|
||||
import notifyError from 'nomad-ui/utils/notify-error';
|
||||
|
||||
export default class TaskGroupRoute extends Route.extend(WithWatchers) {
|
||||
breadcrumbs(model) {
|
||||
if (!model) return [];
|
||||
return [
|
||||
{
|
||||
label: model.get('name'),
|
||||
args: [
|
||||
'jobs.job.task-group',
|
||||
model.get('job'),
|
||||
model.get('name'),
|
||||
qpBuilder({ jobNamespace: model.get('job.namespace.name') || 'default' }),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
model({ name }) {
|
||||
const job = this.modelFor('jobs.job');
|
||||
|
||||
|
||||
@@ -8,13 +8,6 @@ export default class RunRoute extends Route {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Run',
|
||||
args: ['jobs.run'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeModel(transition) {
|
||||
if (this.can.cannot('run job', null, { namespace: transition.to.queryParams.namespace })) {
|
||||
this.transitionTo('jobs');
|
||||
|
||||
@@ -9,13 +9,6 @@ import RSVP from 'rsvp';
|
||||
export default class OptimizeRoute extends Route {
|
||||
@service can;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Recommendations',
|
||||
args: ['optimize'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeModel() {
|
||||
if (this.can.cannot('accept recommendation')) {
|
||||
this.transitionTo('jobs');
|
||||
|
||||
@@ -2,17 +2,6 @@ import Route from '@ember/routing/route';
|
||||
import notifyError from 'nomad-ui/utils/notify-error';
|
||||
|
||||
export default class OptimizeSummaryRoute extends Route {
|
||||
breadcrumbs(model) {
|
||||
if (!model) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
label: model.slug.replace('/', ' / '),
|
||||
args: ['optimize.summary', model.slug],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async model({ jobNamespace, slug }) {
|
||||
const model = this.modelFor('optimize').summaries.find(
|
||||
summary => summary.slug === slug && summary.jobNamespace === jobNamespace
|
||||
|
||||
@@ -10,13 +10,6 @@ export default class ServersRoute extends Route.extend(WithForbiddenState) {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Servers',
|
||||
args: ['servers.index'],
|
||||
},
|
||||
];
|
||||
|
||||
beforeModel() {
|
||||
return this.get('system.leader');
|
||||
}
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||
|
||||
export default class ServerRoute extends Route.extend(WithModelErrorHandling) {
|
||||
breadcrumbs(model) {
|
||||
if (!model) return [];
|
||||
return [
|
||||
{
|
||||
label: model.name,
|
||||
args: ['servers.server', model.id],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
export default class ServerRoute extends Route.extend(WithModelErrorHandling) {}
|
||||
|
||||
@@ -10,13 +10,6 @@ export default class TopologyRoute extends Route.extend(WithForbiddenState) {
|
||||
@service store;
|
||||
@service system;
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Topology',
|
||||
args: ['topology'],
|
||||
},
|
||||
];
|
||||
|
||||
model() {
|
||||
return RSVP.hash({
|
||||
jobs: this.store.findAll('job'),
|
||||
|
||||
@@ -1,45 +1,20 @@
|
||||
import { getOwner } from '@ember/application';
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import Service from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { schedule } from '@ember/runloop';
|
||||
|
||||
@classic
|
||||
export default class BreadcrumbsService extends Service {
|
||||
@service router;
|
||||
export default class BucketService extends Service {
|
||||
@tracked crumbs = [];
|
||||
|
||||
// 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.
|
||||
@computed('router.{currentURL,currentRouteName}')
|
||||
get breadcrumbs() {
|
||||
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.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.breadcrumbs || [];
|
||||
if (typeof breadcrumbs === 'function') {
|
||||
breadcrumbs = breadcrumbs(route.get('controller.model')) || [];
|
||||
}
|
||||
|
||||
crumbs.push(...breadcrumbs);
|
||||
@action registerBreadcrumb(crumb) {
|
||||
schedule('actions', this, () => {
|
||||
this.crumbs = [...this.crumbs, crumb];
|
||||
});
|
||||
}
|
||||
|
||||
return crumbs;
|
||||
@action deregisterBreadcrumb(crumb) {
|
||||
const newCrumbs = this.crumbs.filter(c => c !== crumb);
|
||||
|
||||
this.crumbs = newCrumbs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
li.is-active a {
|
||||
ul {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
li::before {
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
li:last-child a.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin: -4px 0px;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-size: small;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.is-primary {
|
||||
background: linear-gradient(to right, $nomad-green-darker, $nomad-green-dark);
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
{{outlet}}
|
||||
{{#each this.breadcrumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{crumb}} />
|
||||
{{/each}}
|
||||
{{outlet}}
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb @crumb={{this.breadcrumb}} />{{outlet}}
|
||||
@@ -1,3 +1,4 @@
|
||||
<Breadcrumb @crumb={{hash label="Clients" args=(array "clients.index")}} />
|
||||
<PageLayout>
|
||||
{{outlet}}
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb @crumb={{this.breadcrumb}} />{{outlet}}
|
||||
@@ -1,13 +1,7 @@
|
||||
{{#each this.breadcrumbs as |breadcrumb index|}}
|
||||
<li class="{{if (eq (inc index) this.breadcrumbs.length) "is-active"}}">
|
||||
{{#if breadcrumb.isPending}}
|
||||
<a href="#" aria-label="loading" data-test-breadcrumb="loading">…</a>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@params={{breadcrumb.args}}
|
||||
data-test-breadcrumb={{breadcrumb.args.firstObject}}>
|
||||
{{breadcrumb.label}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
<Breadcrumbs as |breadcrumbs|>
|
||||
{{#each breadcrumbs as |crumb|}}
|
||||
{{#let crumb.args.crumb as |c|}}
|
||||
{{component (concat "breadcrumbs/" (or c.type "default")) crumb=c}}
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</Breadcrumbs>
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb @crumb={{hash label="Storage" args=(array "csi.index")}} />{{outlet}}
|
||||
@@ -1 +1,4 @@
|
||||
{{outlet}}
|
||||
{{#each this.breadcrumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{crumb}} />
|
||||
{{/each}}
|
||||
{{outlet}}
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb @crumb={{hash label="Storage" args=(array "csi.index")}} />{{outlet}}
|
||||
@@ -1,107 +1,160 @@
|
||||
{{#each this.breadcrumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{crumb}} />
|
||||
{{/each}}
|
||||
{{page-title "CSI Volume " this.model.name}}
|
||||
<section class="section with-headspace">
|
||||
<h1 class="title" data-test-title>{{this.model.name}}</h1>
|
||||
|
||||
<h1 class="title" data-test-title>
|
||||
{{this.model.name}}
|
||||
</h1>
|
||||
<div class="boxed-section is-small">
|
||||
<div class="boxed-section-body inline-definitions">
|
||||
<span class="label">Volume Details</span>
|
||||
<span class="label">
|
||||
Volume Details
|
||||
</span>
|
||||
<span class="pair" data-test-volume-health>
|
||||
<span class="term">Health</span>
|
||||
<span class="term">
|
||||
Health
|
||||
</span>
|
||||
{{if this.model.schedulable "Schedulable" "Unschedulable"}}
|
||||
</span>
|
||||
<span class="pair" data-test-volume-provider>
|
||||
<span class="term">Provider</span>
|
||||
<span class="term">
|
||||
Provider
|
||||
</span>
|
||||
{{this.model.provider}}
|
||||
</span>
|
||||
<span class="pair" data-test-volume-external-id>
|
||||
<span class="term">External ID</span>
|
||||
<span class="term">
|
||||
External ID
|
||||
</span>
|
||||
{{this.model.externalId}}
|
||||
</span>
|
||||
{{#if this.system.shouldShowNamespaces}}
|
||||
<span class="pair" data-test-volume-namespace>
|
||||
<span class="term">Namespace</span>
|
||||
<span class="term">
|
||||
Namespace
|
||||
</span>
|
||||
{{this.model.namespace.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Write Allocations
|
||||
</div>
|
||||
<div class="boxed-section-body {{if this.model.writeAllocations.length "is-full-bleed"}}">
|
||||
{{#if this.model.writeAllocations.length}}
|
||||
<ListTable
|
||||
@source={{this.sortedWriteAllocations}}
|
||||
@class="with-foot" as |t|>
|
||||
<ListTable @source={{this.sortedWriteAllocations}} @class="with-foot" as |t|>
|
||||
<t.head>
|
||||
<th class="is-narrow"></th>
|
||||
<th>ID</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
<th>Status</th>
|
||||
<th>Client</th>
|
||||
<th>Job</th>
|
||||
<th>Version</th>
|
||||
<th>CPU</th>
|
||||
<th>Memory</th>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Modified
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th>
|
||||
Client
|
||||
</th>
|
||||
<th>
|
||||
Job
|
||||
</th>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<th>
|
||||
CPU
|
||||
</th>
|
||||
<th>
|
||||
Memory
|
||||
</th>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<AllocationRow
|
||||
@data-test-write-allocation={{row.model.id}}
|
||||
@allocation={{row.model}}
|
||||
@context="volume"
|
||||
@onClick={{action "gotoAllocation" row.model}} />
|
||||
@onClick={{action "gotoAllocation" row.model}}
|
||||
/>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<div class="empty-message" data-test-empty-write-allocations>
|
||||
<h3 class="empty-message-headline" data-test-empty-write-allocations-headline>No Write Allocations</h3>
|
||||
<p class="empty-message-body" data-test-empty-write-allocations-message>No allocations are depending on this volume for read/write access.</p>
|
||||
<h3 class="empty-message-headline" data-test-empty-write-allocations-headline>
|
||||
No Write Allocations
|
||||
</h3>
|
||||
<p class="empty-message-body" data-test-empty-write-allocations-message>
|
||||
No allocations are depending on this volume for read/write access.
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Read Allocations
|
||||
</div>
|
||||
<div class="boxed-section-body {{if this.model.readAllocations.length "is-full-bleed"}}">
|
||||
{{#if this.model.readAllocations.length}}
|
||||
<ListTable
|
||||
@source={{this.sortedReadAllocations}}
|
||||
@class="with-foot" as |t|>
|
||||
<ListTable @source={{this.sortedReadAllocations}} @class="with-foot" as |t|>
|
||||
<t.head>
|
||||
<th class="is-narrow"></th>
|
||||
<th>ID</th>
|
||||
<th>Modified</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th>Client</th>
|
||||
<th>Job</th>
|
||||
<th>Version</th>
|
||||
<th>CPU</th>
|
||||
<th>Memory</th>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Modified
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th>
|
||||
Client
|
||||
</th>
|
||||
<th>
|
||||
Job
|
||||
</th>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<th>
|
||||
CPU
|
||||
</th>
|
||||
<th>
|
||||
Memory
|
||||
</th>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<AllocationRow
|
||||
@data-test-read-allocation={{row.model.id}}
|
||||
@allocation={{row.model}}
|
||||
@context="volume"
|
||||
@onClick={{action "gotoAllocation" row.model}} />
|
||||
@onClick={{action "gotoAllocation" row.model}}
|
||||
/>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<div class="empty-message" data-test-empty-read-allocations>
|
||||
<h3 class="empty-message-headline" data-test-empty-read-allocations-headline>No Read Allocations</h3>
|
||||
<p class="empty-message-body" data-test-empty-read-allocations-message>No allocations are depending on this volume for read-only access.</p>
|
||||
<h3 class="empty-message-headline" data-test-empty-read-allocations-headline>
|
||||
No Read Allocations
|
||||
</h3>
|
||||
<p class="empty-message-body" data-test-empty-read-allocations-message>
|
||||
No allocations are depending on this volume for read-only access.
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Constraints
|
||||
@@ -109,20 +162,32 @@
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Setting</th>
|
||||
<th>Value</th>
|
||||
<th>
|
||||
Setting
|
||||
</th>
|
||||
<th>
|
||||
Value
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Access Mode</td>
|
||||
<td data-test-access-mode>{{this.model.accessMode}}</td>
|
||||
<td>
|
||||
Access Mode
|
||||
</td>
|
||||
<td data-test-access-mode>
|
||||
{{this.model.accessMode}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attachment Mode</td>
|
||||
<td data-test-attachment-mode>{{this.model.attachmentMode}}</td>
|
||||
<td>
|
||||
Attachment Mode
|
||||
</td>
|
||||
<td data-test-attachment-mode>
|
||||
{{this.model.attachmentMode}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,3 +1,4 @@
|
||||
<Breadcrumb @crumb={{hash label="Jobs" args=(array "jobs.index")}} />
|
||||
<PageLayout>
|
||||
{{outlet}}
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb @crumb={{hash type="job" job=this.job}} />{{outlet}}
|
||||
@@ -1,5 +1,6 @@
|
||||
<Breadcrumb @crumb={{hash label="Dispatch" args=(array "jobs.job.dispatch")}} />
|
||||
{{page-title "Dispatch new " this.model.name}}
|
||||
<JobSubnav @job={{this.model}} />
|
||||
<section class="section">
|
||||
<JobDispatch @job={{this.model}}/>
|
||||
</section>
|
||||
<JobDispatch @job={{this.model}} />
|
||||
</section>
|
||||
@@ -1,64 +1,117 @@
|
||||
<Breadcrumb @crumb={{this.breadcrumb}} />
|
||||
{{page-title "Task group " this.model.name " - Job " this.model.job.name}}
|
||||
<div class="tabs is-subnav">
|
||||
<ul>
|
||||
<li><LinkTo @route="jobs.job.task-group" @models={{array this.model.job this.model}} @activeClass="is-active">Overview</LinkTo></li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="jobs.job.task-group"
|
||||
@models={{array this.model.job this.model}}
|
||||
@activeClass="is-active"
|
||||
>
|
||||
Overview
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<section class="section">
|
||||
<h1 class="title with-flex">
|
||||
<span>{{this.model.name}}</span>
|
||||
<span>
|
||||
{{this.model.name}}
|
||||
</span>
|
||||
<div>
|
||||
<Exec::OpenButton @job={{this.model.job}} @taskGroup={{this.model}} />
|
||||
{{#if this.model.scaling}}
|
||||
<StepperInput
|
||||
data-test-task-group-count-stepper
|
||||
aria-label={{this.tooltipText}}
|
||||
@min={{this.model.scaling.min}}
|
||||
@max={{this.model.scaling.max}}
|
||||
@value={{this.model.count}}
|
||||
@class="is-primary is-small"
|
||||
@disabled={{or this.model.job.runningDeployment (cannot "scale job" namespace=this.model.job.namespace.name)}}
|
||||
@onChange={{action "scaleTaskGroup"}}>
|
||||
Count
|
||||
</StepperInput>
|
||||
<StepperInput
|
||||
data-test-task-group-count-stepper
|
||||
aria-label={{this.tooltipText}}
|
||||
@min={{this.model.scaling.min}}
|
||||
@max={{this.model.scaling.max}}
|
||||
@value={{this.model.count}}
|
||||
@class="is-primary is-small"
|
||||
@disabled={{or
|
||||
this.model.job.runningDeployment
|
||||
(cannot "scale job" namespace=this.model.job.namespace.name)
|
||||
}}
|
||||
@onChange={{action "scaleTaskGroup"}}
|
||||
>
|
||||
Count
|
||||
</StepperInput>
|
||||
{{/if}}
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
<div class="boxed-section-body inline-definitions">
|
||||
<span class="label">Task Group Details</span>
|
||||
|
||||
<span class="pair" data-test-task-group-tasks><span class="term"># Tasks</span> {{this.model.tasks.length}}</span>
|
||||
<span class="pair" data-test-task-group-cpu><span class="term">Reserved CPU</span> {{format-scheduled-hertz this.model.reservedCPU}}</span>
|
||||
<span class="label">
|
||||
Task Group Details
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-tasks>
|
||||
<span class="term">
|
||||
# Tasks
|
||||
</span>
|
||||
{{this.model.tasks.length}}
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-cpu>
|
||||
<span class="term">
|
||||
Reserved CPU
|
||||
</span>
|
||||
{{format-scheduled-hertz this.model.reservedCPU}}
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-mem>
|
||||
<span class="term">Reserved Memory</span>
|
||||
<span class="term">
|
||||
Reserved Memory
|
||||
</span>
|
||||
{{format-scheduled-bytes this.model.reservedMemory start="MiB"}}
|
||||
{{#if (gt this.model.reservedMemoryMax this.model.reservedMemory)}}
|
||||
({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}} Max)
|
||||
({{format-scheduled-bytes this.model.reservedMemoryMax start="MiB"}}Max)
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-disk><span class="term">Reserved Disk</span> {{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}}</span>
|
||||
{{#if this.model.scaling}}
|
||||
<span class="pair" data-test-task-group-min><span class="term">Count Range</span>
|
||||
{{this.model.scaling.min}} to {{this.model.scaling.max}}
|
||||
<span class="pair" data-test-task-group-disk>
|
||||
<span class="term">
|
||||
Reserved Disk
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-max><span class="term">Scaling Policy?</span>
|
||||
{{format-scheduled-bytes this.model.reservedEphemeralDisk start="MiB"}}
|
||||
</span>
|
||||
{{#if this.model.scaling}}
|
||||
<span class="pair" data-test-task-group-min>
|
||||
<span class="term">
|
||||
Count Range
|
||||
</span>
|
||||
{{this.model.scaling.min}}
|
||||
to
|
||||
{{this.model.scaling.max}}
|
||||
</span>
|
||||
<span class="pair" data-test-task-group-max>
|
||||
<span class="term">
|
||||
Scaling Policy?
|
||||
</span>
|
||||
{{if this.model.scaling.policy "Yes" "No"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
<div>Allocation Status <span class="badge is-white">{{this.allocations.length}}</span></div>
|
||||
<div>
|
||||
Allocation Status
|
||||
<span class="badge is-white">
|
||||
{{this.allocations.length}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<AllocationStatusBar @allocationContainer={{this.model.summary}} @class="split-view" as |chart|>
|
||||
<AllocationStatusBar
|
||||
@allocationContainer={{this.model.summary}}
|
||||
@class="split-view" as |chart|
|
||||
>
|
||||
<ol class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li class="{{datum.className}} {{if (eq datum.label chart.activeDatum.label) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<li
|
||||
class="{{datum.className}}
|
||||
|
||||
{{if (eq datum.label chart.activeDatum.label) "is-active"}}
|
||||
|
||||
{{if (eq datum.value 0) "is-empty"}}"
|
||||
>
|
||||
<JobPage::Parts::SummaryLegendItem @datum={{datum}} @index={{index}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
@@ -99,62 +152,102 @@
|
||||
@source={{this.sortedAllocations}}
|
||||
@size={{this.pageSize}}
|
||||
@page={{this.currentPage}}
|
||||
@class="allocations" as |p|>
|
||||
@class="allocations" as |p|
|
||||
>
|
||||
<ListTable
|
||||
@source={{p.list}}
|
||||
@sortProperty={{this.sortProperty}}
|
||||
@sortDescending={{this.sortDescending}}
|
||||
@class="with-foot" as |t|>
|
||||
@class="with-foot" as |t|
|
||||
>
|
||||
<t.head>
|
||||
<th class="is-narrow"></th>
|
||||
<t.sort-by @prop="shortId">ID</t.sort-by>
|
||||
<t.sort-by @prop="createIndex" @title="Create Index">Created</t.sort-by>
|
||||
<t.sort-by @prop="modifyIndex" @title="Modify Index">Modified</t.sort-by>
|
||||
<t.sort-by @prop="statusIndex">Status</t.sort-by>
|
||||
<t.sort-by @prop="jobVersion">Version</t.sort-by>
|
||||
<t.sort-by @prop="node.shortId">Client</t.sort-by>
|
||||
<th>Volume</th>
|
||||
<th>CPU</th>
|
||||
<th>Memory</th>
|
||||
<t.sort-by @prop="shortId">
|
||||
ID
|
||||
</t.sort-by>
|
||||
<t.sort-by @prop="createIndex" @title="Create Index">
|
||||
Created
|
||||
</t.sort-by>
|
||||
<t.sort-by @prop="modifyIndex" @title="Modify Index">
|
||||
Modified
|
||||
</t.sort-by>
|
||||
<t.sort-by @prop="statusIndex">
|
||||
Status
|
||||
</t.sort-by>
|
||||
<t.sort-by @prop="jobVersion">
|
||||
Version
|
||||
</t.sort-by>
|
||||
<t.sort-by @prop="node.shortId">
|
||||
Client
|
||||
</t.sort-by>
|
||||
<th>
|
||||
Volume
|
||||
</th>
|
||||
<th>
|
||||
CPU
|
||||
</th>
|
||||
<th>
|
||||
Memory
|
||||
</th>
|
||||
</t.head>
|
||||
<t.body @key="model.id" as |row|>
|
||||
<AllocationRow @data-test-allocation={{row.model.id}} @allocation={{row.model}} @context="taskGroup" @onClick={{action "gotoAllocation" row.model}} />
|
||||
<AllocationRow
|
||||
@data-test-allocation={{row.model.id}}
|
||||
@allocation={{row.model}}
|
||||
@context="taskGroup"
|
||||
@onClick={{action "gotoAllocation" row.model}}
|
||||
/>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
<div class="table-foot">
|
||||
<PageSizeSelect @onChange={{action this.resetPagination}} />
|
||||
<nav class="pagination">
|
||||
<div class="pagination-numbers">
|
||||
{{p.startsAt}}–{{p.endsAt}} of {{this.sortedAllocations.length}}
|
||||
{{p.startsAt}}
|
||||
–
|
||||
{{p.endsAt}}
|
||||
of
|
||||
{{this.sortedAllocations.length}}
|
||||
</div>
|
||||
<p.prev @class="pagination-previous">{{x-icon "chevron-left"}}</p.prev>
|
||||
<p.next @class="pagination-next">{{x-icon "chevron-right"}}</p.next>
|
||||
<p.prev @class="pagination-previous">
|
||||
{{x-icon "chevron-left"}}
|
||||
</p.prev>
|
||||
<p.next @class="pagination-next">
|
||||
{{x-icon "chevron-right"}}
|
||||
</p.next>
|
||||
<ul class="pagination-list"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
</ListPagination>
|
||||
{{else if this.allocations.length}}
|
||||
<div class="boxed-section-body">
|
||||
<div class="empty-message" data-test-empty-allocations-list>
|
||||
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No allocations match the term
|
||||
<strong>
|
||||
{{this.searchTerm}}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.allocations.length}}
|
||||
<div class="boxed-section-body">
|
||||
<div class="empty-message" data-test-empty-allocations-list>
|
||||
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Matches</h3>
|
||||
<p class="empty-message-body">No allocations match the term <strong>{{this.searchTerm}}</strong></p>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<div class="empty-message" data-test-empty-allocations-list>
|
||||
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>
|
||||
No Allocations
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No allocations have been placed.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="boxed-section-body">
|
||||
<div class="empty-message" data-test-empty-allocations-list>
|
||||
<h3 class="empty-message-headline" data-test-empty-allocations-list-headline>No Allocations</h3>
|
||||
<p class="empty-message-body">No allocations have been placed.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LifecycleChart @tasks={{this.model.tasks}} />
|
||||
|
||||
{{#if this.model.scaleState.isVisible}}
|
||||
{{#if this.shouldShowScaleEventTimeline}}
|
||||
<div data-test-scaling-timeline class="boxed-section">
|
||||
@@ -166,7 +259,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div data-test-scaling-events class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Recent Scaling Events
|
||||
@@ -176,7 +268,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.model.volumes.length}}
|
||||
<div data-test-volumes class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
@@ -185,29 +276,47 @@
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
<ListTable @source={{this.model.volumes}} as |t|>
|
||||
<t.head>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Source</th>
|
||||
<th>Permissions</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Type
|
||||
</th>
|
||||
<th>
|
||||
Source
|
||||
</th>
|
||||
<th>
|
||||
Permissions
|
||||
</th>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-test-volume>
|
||||
<td data-test-volume-name>
|
||||
{{#if row.model.isCSI}}
|
||||
<LinkTo @route="csi.volumes.volume" @model={{row.model.source}} @query={{hash volumeNamespace=row.model.namespace.id}}>
|
||||
<LinkTo
|
||||
@route="csi.volumes.volume"
|
||||
@model={{row.model.source}}
|
||||
@query={{hash volumeNamespace=row.model.namespace.id}}
|
||||
>
|
||||
{{row.model.name}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{row.model.name}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-test-volume-type>{{row.model.type}}</td>
|
||||
<td data-test-volume-source>{{row.model.source}}</td>
|
||||
<td data-test-volume-permissions>{{if row.model.readOnly "Read" "Read/Write"}}</td>
|
||||
<td data-test-volume-type>
|
||||
{{row.model.type}}
|
||||
</td>
|
||||
<td data-test-volume-source>
|
||||
{{row.model.source}}
|
||||
</td>
|
||||
<td data-test-volume-permissions>
|
||||
{{if row.model.readOnly "Read" "Read/Write"}}
|
||||
</td>
|
||||
</tr>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,7 +1,5 @@
|
||||
<Breadcrumb @crumb={{hash label="Run" args=(array "jobs.run")}} />
|
||||
{{page-title "Run a job"}}
|
||||
<section class="section">
|
||||
<JobEditor
|
||||
@job={{this.model}}
|
||||
@context="new"
|
||||
@onSubmit={{action this.onSubmit}} />
|
||||
</section>
|
||||
<JobEditor @job={{this.model}} @context="new" @onSubmit={{action this.onSubmit}} />
|
||||
</section>
|
||||
@@ -1,3 +1,4 @@
|
||||
<Breadcrumb @crumb={{hash label="Recommendations" args=(array "optimize")}} />
|
||||
<PageLayout>
|
||||
<section class="section">
|
||||
{{#if this.summaries}}
|
||||
@@ -8,7 +9,12 @@
|
||||
data-test-recommendation-summaries-search
|
||||
@onChange={{this.syncActiveSummary}}
|
||||
@searchTerm={{mut this.searchTerm}}
|
||||
@placeholder="Search {{this.summaries.length}} {{pluralize "recommendation" this.summaries.length}}..." />
|
||||
@placeholder="Search
|
||||
{{this.summaries.length}}
|
||||
|
||||
{{pluralize "recommendation" this.summaries.length}}
|
||||
..."
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="toolbar-item is-right-aligned is-mobile-full-width">
|
||||
@@ -19,72 +25,88 @@
|
||||
@label="Namespace"
|
||||
@options={{this.optionsNamespaces}}
|
||||
@selection={{this.qpNamespace}}
|
||||
@onSelect={{action (queue
|
||||
(action this.cacheNamespace)
|
||||
(action this.setFacetQueryParam "qpNamespace")
|
||||
)}} />
|
||||
@onSelect={{action
|
||||
(queue
|
||||
(action this.cacheNamespace) (action this.setFacetQueryParam "qpNamespace")
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
<MultiSelectDropdown
|
||||
data-test-type-facet
|
||||
@label="Type"
|
||||
@options={{this.optionsType}}
|
||||
@selection={{this.selectionType}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpType"}} />
|
||||
@onSelect={{action this.setFacetQueryParam "qpType"}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-status-facet
|
||||
@label="Status"
|
||||
@options={{this.optionsStatus}}
|
||||
@selection={{this.selectionStatus}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpStatus"}} />
|
||||
@onSelect={{action this.setFacetQueryParam "qpStatus"}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-datacenter-facet
|
||||
@label="Datacenter"
|
||||
@options={{this.optionsDatacenter}}
|
||||
@selection={{this.selectionDatacenter}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}} />
|
||||
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
data-test-prefix-facet
|
||||
@label="Prefix"
|
||||
@options={{this.optionsPrefix}}
|
||||
@selection={{this.selectionPrefix}}
|
||||
@onSelect={{action this.setFacetQueryParam "qpPrefix"}} />
|
||||
@onSelect={{action this.setFacetQueryParam "qpPrefix"}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.filteredSummaries}}
|
||||
{{outlet}}
|
||||
|
||||
<ListTable
|
||||
@source={{this.filteredSummaries}} as |t|>
|
||||
<ListTable @source={{this.filteredSummaries}} as |t|>
|
||||
<t.head>
|
||||
<th>Job</th>
|
||||
<th>Recommended At</th>
|
||||
<th># Allocs</th>
|
||||
<th>CPU</th>
|
||||
<th>Mem</th>
|
||||
<th>Agg. CPU</th>
|
||||
<th>Agg. Mem</th>
|
||||
<th>
|
||||
Job
|
||||
</th>
|
||||
<th>
|
||||
Recommended At
|
||||
</th>
|
||||
<th>
|
||||
# Allocs
|
||||
</th>
|
||||
<th>
|
||||
CPU
|
||||
</th>
|
||||
<th>
|
||||
Mem
|
||||
</th>
|
||||
<th>
|
||||
Agg. CPU
|
||||
</th>
|
||||
<th>
|
||||
Agg. Mem
|
||||
</th>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
{{#if row.model.isProcessed}}
|
||||
<Das::RecommendationRow
|
||||
class="is-disabled"
|
||||
@summary={{row.model}}
|
||||
/>
|
||||
<Das::RecommendationRow class="is-disabled" @summary={{row.model}} />
|
||||
{{else}}
|
||||
<Das::RecommendationRow
|
||||
class="is-interactive {{if (eq row.model this.activeRecommendationSummary) 'is-active'}}"
|
||||
class="is-interactive
|
||||
{{if (eq row.model this.activeRecommendationSummary) "is-active"}}"
|
||||
@summary={{row.model}}
|
||||
{{on "click" (fn this.transitionToSummary row.model)}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<div class="empty-message" data-test-empty-recommendations>
|
||||
<h3 class="empty-message-headline" data-test-empty-recommendations-headline>No Matches</h3>
|
||||
<h3 class="empty-message-headline" data-test-empty-recommendations-headline>
|
||||
No Matches
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No recommendations match your current filter selection.
|
||||
</p>
|
||||
@@ -92,11 +114,13 @@
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="empty-message" data-test-empty-recommendations>
|
||||
<h3 class="empty-message-headline" data-test-empty-recommendations-headline>No Recommendations</h3>
|
||||
<h3 class="empty-message-headline" data-test-empty-recommendations-headline>
|
||||
No Recommendations
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
All recommendations have been accepted or dismissed. Nomad will continuously monitor applications so expect more recommendations in the future.
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
@@ -1,6 +1,2 @@
|
||||
{{#if @model}}
|
||||
<Das::RecommendationCard
|
||||
@summary={{@model}}
|
||||
@proceed={{this.optimizeController.proceed}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Breadcrumb @crumb={{this.breadcrumb}} />
|
||||
<Das::RecommendationCard @summary={{@model}} @proceed={{this.optimizeController.proceed}} />
|
||||
@@ -1,3 +1,4 @@
|
||||
<Breadcrumb @crumb={{hash label="Servers" args=(array "servers.index")}} />
|
||||
<PageLayout>
|
||||
{{outlet}}
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
@@ -1 +1,4 @@
|
||||
{{outlet}}
|
||||
<Breadcrumb
|
||||
@crumb={{hash title="Server" label=this.server.name args=(array "servers.server" this.server.id)}}
|
||||
/>
|
||||
{{outlet}}
|
||||
@@ -1,3 +1,4 @@
|
||||
<Breadcrumb @crumb={{hash label="Topology" args=(array "topology")}} />
|
||||
{{page-title "Cluster Topology"}}
|
||||
<PageLayout>
|
||||
<section class="section is-full-width">
|
||||
@@ -8,11 +9,26 @@
|
||||
<div class="notification is-warning">
|
||||
<div data-test-filtered-nodes-warning class="columns">
|
||||
<div class="column">
|
||||
<h3 data-test-title class="title is-4">Some Clients Were Filtered</h3>
|
||||
<p data-test-message>{{this.filteredNodes.length}} {{if (eq this.filteredNodes.length 1) "client was" "clients were"}} filtered from the topology visualization. This is most likely due to the {{pluralize "client" this.filteredNodes.length}} running a version of Nomad <0.9.0.</p>
|
||||
<h3 data-test-title class="title is-4">
|
||||
Some Clients Were Filtered
|
||||
</h3>
|
||||
<p data-test-message>
|
||||
{{this.filteredNodes.length}}
|
||||
{{if (eq this.filteredNodes.length 1) "client was" "clients were"}}
|
||||
filtered from the topology visualization. This is most likely due to the
|
||||
{{pluralize "client" this.filteredNodes.length}}
|
||||
running a version of Nomad
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-dismiss class="button is-warning" onclick={{action (mut this.filteredNodes) null}} type="button">Okay</button>
|
||||
<button
|
||||
data-test-dismiss
|
||||
class="button is-warning"
|
||||
onclick={{action (mut this.filteredNodes) null}}
|
||||
type="button"
|
||||
>
|
||||
Okay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,11 +39,22 @@
|
||||
<div class="notification is-warning">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title is-4">No Live Updating</h3>
|
||||
<p>The topology visualization depends on too much data to continuously poll.</p>
|
||||
<h3 class="title is-4">
|
||||
No Live Updating
|
||||
</h3>
|
||||
<p>
|
||||
The topology visualization depends on too much data to continuously poll.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-polling-notice-dismiss class="button is-warning" type="button" onclick={{toggle-action "showPollingNotice" this}}>Okay</button>
|
||||
<button
|
||||
data-test-polling-notice-dismiss
|
||||
class="button is-warning"
|
||||
type="button"
|
||||
onclick={{toggle-action "showPollingNotice" this}}
|
||||
>
|
||||
Okay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,154 +63,335 @@
|
||||
<div class="boxed-section-head">
|
||||
Legend
|
||||
{{#if (cannot "list all jobs")}}
|
||||
<span aria-label="Your ACL token may limit your ability to list all allocations" class="tag is-warning pull-right tooltip multiline">Partial View</span>
|
||||
<span
|
||||
aria-label="Your ACL token may limit your ability to list all allocations"
|
||||
class="tag is-warning pull-right tooltip multiline"
|
||||
>
|
||||
Partial View
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<div class="legend">
|
||||
<h3 class="legend-label">Metrics</h3>
|
||||
<h3 class="legend-label">
|
||||
Metrics
|
||||
</h3>
|
||||
<dl class="legend-terms">
|
||||
<dt>M:</dt><dd>Memory</dd>
|
||||
<dt>C:</dt><dd>CPU</dd>
|
||||
<dt>
|
||||
M:
|
||||
</dt>
|
||||
<dd>
|
||||
Memory
|
||||
</dd>
|
||||
<dt>
|
||||
C:
|
||||
</dt>
|
||||
<dd>
|
||||
CPU
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<h3 class="legend-label">Allocation Status</h3>
|
||||
<h3 class="legend-label">
|
||||
Allocation Status
|
||||
</h3>
|
||||
<dl class="legend-terms">
|
||||
<div class="legend-term"><dt><span class="color-swatch is-wide running" title="Running" /></dt><dd>Running</dd></div>
|
||||
<div class="legend-term"><dt><span class="color-swatch is-wide pending" title="Starting" /></dt><dd>Starting</dd></div>
|
||||
<div class="legend-term">
|
||||
<dt>
|
||||
<span class="color-swatch is-wide running" title="Running"></span>
|
||||
</dt>
|
||||
<dd>
|
||||
Running
|
||||
</dd>
|
||||
</div>
|
||||
<div class="legend-term">
|
||||
<dt>
|
||||
<span class="color-swatch is-wide pending" title="Starting"></span>
|
||||
</dt>
|
||||
<dd>
|
||||
Starting
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section">
|
||||
<div data-test-info-panel-title class="boxed-section-head">
|
||||
{{#if this.activeNode}}Client{{else if this.activeAllocation}}Allocation{{else}}Cluster{{/if}} Details
|
||||
{{#if this.activeNode}}
|
||||
Client
|
||||
{{else if this.activeAllocation}}
|
||||
Allocation
|
||||
{{else}}
|
||||
Cluster
|
||||
{{/if}}
|
||||
Details
|
||||
</div>
|
||||
<div data-test-info-panel class="boxed-section-body">
|
||||
{{#if this.activeNode}}
|
||||
{{#let this.activeNode.node as |node|}}
|
||||
<div class="dashboard-metric">
|
||||
<p data-test-allocations class="metric">{{this.activeNode.allocations.length}} <span class="metric-label">Allocations</span></p>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<h3 class="pair">
|
||||
<strong>Client:</strong>
|
||||
<LinkTo data-test-client-link @route="clients.client" @model={{node}}>
|
||||
{{node.shortId}}
|
||||
</LinkTo>
|
||||
</h3>
|
||||
<p data-test-name class="minor-pair"><strong>Name:</strong> {{node.name}}</p>
|
||||
<p data-test-address class="minor-pair"><strong>Address:</strong> {{node.httpAddr}}</p>
|
||||
<p data-test-status class="minor-pair"><strong>Status:</strong> {{node.status}}</p>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<h3 class="pair">
|
||||
<strong>Draining?</strong> <span data-test-draining class="{{if node.isDraining "status-text is-info"}}">{{if node.isDraining "Yes" "No"}}</span>
|
||||
</h3>
|
||||
<h3 class="pair">
|
||||
<strong>Eligible?</strong> <span data-test-eligible class="{{unless node.isEligible "status-text is-warning"}}">{{if node.isEligible "Yes" "No"}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<p class="metric">
|
||||
{{this.nodeUtilization.totalMemoryFormatted}}
|
||||
<span class="metric-units">{{this.nodeUtilization.totalMemoryUnits}}</span>
|
||||
<span class="metric-label">of memory</span>
|
||||
</p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart">
|
||||
<progress
|
||||
data-test-memory-progress-bar
|
||||
class="progress is-danger is-small"
|
||||
value="{{this.nodeUtilization.reservedMemoryPercent}}"
|
||||
max="1">
|
||||
{{this.nodeUtilization.reservedMemoryPercent}}
|
||||
</progress>
|
||||
<div class="dashboard-metric">
|
||||
<p data-test-allocations class="metric">
|
||||
{{this.activeNode.allocations.length}}
|
||||
<span class="metric-label">
|
||||
Allocations
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<h3 class="pair">
|
||||
<strong>
|
||||
Client:
|
||||
</strong>
|
||||
<LinkTo data-test-client-link @route="clients.client" @model={{node}}>
|
||||
{{node.shortId}}
|
||||
</LinkTo>
|
||||
</h3>
|
||||
<p data-test-name class="minor-pair">
|
||||
<strong>
|
||||
Name:
|
||||
</strong>
|
||||
{{node.name}}
|
||||
</p>
|
||||
<p data-test-address class="minor-pair">
|
||||
<strong>
|
||||
Address:
|
||||
</strong>
|
||||
{{node.httpAddr}}
|
||||
</p>
|
||||
<p data-test-status class="minor-pair">
|
||||
<strong>
|
||||
Status:
|
||||
</strong>
|
||||
{{node.status}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<h3 class="pair">
|
||||
<strong>
|
||||
Draining?
|
||||
</strong>
|
||||
<span data-test-draining class="{{if node.isDraining "status-text is-info"}}">
|
||||
{{if node.isDraining "Yes" "No"}}
|
||||
</span>
|
||||
</h3>
|
||||
<h3 class="pair">
|
||||
<strong>
|
||||
Eligible?
|
||||
</strong>
|
||||
<span
|
||||
data-test-eligible
|
||||
class="{{unless node.isEligible "status-text is-warning"}}"
|
||||
>
|
||||
{{if node.isEligible "Yes" "No"}}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<p class="metric">
|
||||
{{this.nodeUtilization.totalMemoryFormatted}}
|
||||
<span class="metric-units">
|
||||
{{this.nodeUtilization.totalMemoryUnits}}
|
||||
</span>
|
||||
<span class="metric-label">
|
||||
of memory
|
||||
</span>
|
||||
</p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart">
|
||||
<progress
|
||||
data-test-memory-progress-bar
|
||||
class="progress is-danger is-small"
|
||||
value="{{this.nodeUtilization.reservedMemoryPercent}}"
|
||||
max="1"
|
||||
>
|
||||
{{this.nodeUtilization.reservedMemoryPercent}}
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-percentage>
|
||||
{{format-percentage this.nodeUtilization.reservedMemoryPercent total=1}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-percentage>{{format-percentage this.nodeUtilization.reservedMemoryPercent total=1}}</span>
|
||||
<div class="annotation" data-test-memory-absolute-value>
|
||||
<strong>
|
||||
{{format-scheduled-bytes this.nodeUtilization.totalReservedMemory}}
|
||||
</strong>
|
||||
/
|
||||
{{format-scheduled-bytes this.nodeUtilization.totalMemory}}
|
||||
reserved
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotation" data-test-memory-absolute-value>
|
||||
<strong>{{format-scheduled-bytes this.nodeUtilization.totalReservedMemory}}</strong> / {{format-scheduled-bytes this.nodeUtilization.totalMemory}} reserved
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<p class="metric">{{this.nodeUtilization.totalCPU}} <span class="metric-units">MHz</span> <span class="metric-label">of CPU</span></p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart" data-test-percentage-bar>
|
||||
<progress
|
||||
data-test-cpu-progress-bar
|
||||
class="progress is-info is-small"
|
||||
value="{{this.nodeUtilization.reservedCPUPercent}}"
|
||||
max="1">
|
||||
{{this.nodeUtilization.reservedCPUPercent}}
|
||||
</progress>
|
||||
<div class="dashboard-metric">
|
||||
<p class="metric">
|
||||
{{this.nodeUtilization.totalCPU}}
|
||||
<span class="metric-units">
|
||||
MHz
|
||||
</span>
|
||||
<span class="metric-label">
|
||||
of CPU
|
||||
</span>
|
||||
</p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart" data-test-percentage-bar>
|
||||
<progress
|
||||
data-test-cpu-progress-bar
|
||||
class="progress is-info is-small"
|
||||
value="{{this.nodeUtilization.reservedCPUPercent}}"
|
||||
max="1"
|
||||
>
|
||||
{{this.nodeUtilization.reservedCPUPercent}}
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-percentage>
|
||||
{{format-percentage this.nodeUtilization.reservedCPUPercent total=1}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-percentage>{{format-percentage this.nodeUtilization.reservedCPUPercent total=1}}</span>
|
||||
<div class="annotation" data-test-cpu-absolute-value>
|
||||
<strong>
|
||||
{{format-scheduled-hertz this.nodeUtilization.totalReservedCPU}}
|
||||
</strong>
|
||||
/
|
||||
{{format-scheduled-hertz this.nodeUtilization.totalCPU}}
|
||||
reserved
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotation" data-test-cpu-absolute-value>
|
||||
<strong>{{format-scheduled-hertz this.nodeUtilization.totalReservedCPU}}</strong> / {{format-scheduled-hertz this.nodeUtilization.totalCPU}} reserved
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{else if this.activeAllocation}}
|
||||
<div class="dashboard-metric">
|
||||
<h3 class="pair">
|
||||
<strong>Allocation:</strong>
|
||||
<LinkTo data-test-id @route="allocations.allocation" @model={{this.activeAllocation}} class="is-primary">{{this.activeAllocation.shortId}}</LinkTo>
|
||||
<strong>
|
||||
Allocation:
|
||||
</strong>
|
||||
<LinkTo
|
||||
data-test-id
|
||||
@route="allocations.allocation"
|
||||
@model={{this.activeAllocation}}
|
||||
class="is-primary"
|
||||
>
|
||||
{{this.activeAllocation.shortId}}
|
||||
</LinkTo>
|
||||
</h3>
|
||||
<p data-test-sibling-allocs class="minor-pair"><strong>Sibling Allocations:</strong> {{this.siblingAllocations.length}}</p>
|
||||
<p data-test-unique-placements class="minor-pair"><strong>Unique Client Placements:</strong> {{this.uniqueActiveAllocationNodes.length}}</p>
|
||||
<p data-test-sibling-allocs class="minor-pair">
|
||||
<strong>
|
||||
Sibling Allocations:
|
||||
</strong>
|
||||
{{this.siblingAllocations.length}}
|
||||
</p>
|
||||
<p data-test-unique-placements class="minor-pair">
|
||||
<strong>
|
||||
Unique Client Placements:
|
||||
</strong>
|
||||
{{this.uniqueActiveAllocationNodes.length}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<h3 class="pair">
|
||||
<strong>Job:</strong>
|
||||
<strong>
|
||||
Job:
|
||||
</strong>
|
||||
<LinkTo
|
||||
data-test-job
|
||||
@route="jobs.job"
|
||||
@model={{this.activeAllocation.job}}
|
||||
@query={{hash jobNamespace=this.activeAllocation.job.namespace.id}}>
|
||||
{{this.activeAllocation.job.name}}</LinkTo>
|
||||
<span class="is-faded" data-test-task-group> / {{this.activeAllocation.taskGroupName}}</span>
|
||||
@query={{hash jobNamespace=this.activeAllocation.job.namespace.id}}
|
||||
>
|
||||
{{this.activeAllocation.job.name}}
|
||||
</LinkTo>
|
||||
<span class="is-faded" data-test-task-group>
|
||||
/
|
||||
{{this.activeAllocation.taskGroupName}}
|
||||
</span>
|
||||
</h3>
|
||||
<p class="minor-pair"><strong>Type:</strong> {{this.activeAllocation.job.type}}</p>
|
||||
<p class="minor-pair"><strong>Priority:</strong> {{this.activeAllocation.job.priority}}</p>
|
||||
<p class="minor-pair">
|
||||
<strong>
|
||||
Type:
|
||||
</strong>
|
||||
{{this.activeAllocation.job.type}}
|
||||
</p>
|
||||
<p class="minor-pair">
|
||||
<strong>
|
||||
Priority:
|
||||
</strong>
|
||||
{{this.activeAllocation.job.priority}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<h3 class="pair">
|
||||
<strong>Client:</strong>
|
||||
<LinkTo data-test-client @route="clients.client" @model={{this.activeAllocation.node}}>
|
||||
<strong>
|
||||
Client:
|
||||
</strong>
|
||||
<LinkTo
|
||||
data-test-client
|
||||
@route="clients.client"
|
||||
@model={{this.activeAllocation.node}}
|
||||
>
|
||||
{{this.activeAllocation.node.shortId}}
|
||||
</LinkTo>
|
||||
</h3>
|
||||
<p class="minor-pair"><strong>Name:</strong> {{this.activeAllocation.node.name}}</p>
|
||||
<p class="minor-pair"><strong>Address:</strong> {{this.activeAllocation.node.httpAddr}}</p>
|
||||
<p class="minor-pair">
|
||||
<strong>
|
||||
Name:
|
||||
</strong>
|
||||
{{this.activeAllocation.node.name}}
|
||||
</p>
|
||||
<p class="minor-pair">
|
||||
<strong>
|
||||
Address:
|
||||
</strong>
|
||||
{{this.activeAllocation.node.httpAddr}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<PrimaryMetric::Allocation @allocation={{this.activeAllocation}} @metric="memory" class="is-short" />
|
||||
<PrimaryMetric::Allocation
|
||||
@allocation={{this.activeAllocation}}
|
||||
@metric="memory"
|
||||
class="is-short"
|
||||
/>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<PrimaryMetric::Allocation @allocation={{this.activeAllocation}} @metric="cpu" class="is-short" />
|
||||
<PrimaryMetric::Allocation
|
||||
@allocation={{this.activeAllocation}}
|
||||
@metric="cpu"
|
||||
class="is-short"
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="columns is-flush">
|
||||
<div class="dashboard-metric column">
|
||||
<p data-test-node-count class="metric">{{this.model.nodes.length}} <span class="metric-label">Clients</span></p>
|
||||
<p data-test-node-count class="metric">
|
||||
{{this.model.nodes.length}}
|
||||
<span class="metric-label">
|
||||
Clients
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="dashboard-metric column">
|
||||
<p data-test-alloc-count class="metric">{{this.scheduledAllocations.length}} <span class="metric-label">Allocations</span></p>
|
||||
<p data-test-alloc-count class="metric">
|
||||
{{this.scheduledAllocations.length}}
|
||||
<span class="metric-label">
|
||||
Allocations
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-metric with-divider">
|
||||
<p class="metric">{{this.totalMemoryFormatted}} <span class="metric-units">{{this.totalMemoryUnits}}</span> <span class="metric-label">of memory</span></p>
|
||||
<p class="metric">
|
||||
{{this.totalMemoryFormatted}}
|
||||
<span class="metric-units">
|
||||
{{this.totalMemoryUnits}}
|
||||
</span>
|
||||
<span class="metric-label">
|
||||
of memory
|
||||
</span>
|
||||
</p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart" data-test-percentage-bar>
|
||||
@@ -191,21 +399,37 @@
|
||||
data-test-memory-progress-bar
|
||||
class="progress is-danger is-small"
|
||||
value="{{this.reservedMemoryPercent}}"
|
||||
max="1">
|
||||
max="1"
|
||||
>
|
||||
{{this.reservedMemoryPercent}}
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-memory-percentage>{{format-percentage this.reservedMemoryPercent total=1}}</span>
|
||||
<span class="nowrap" data-test-memory-percentage>
|
||||
{{format-percentage this.reservedMemoryPercent total=1}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotation" data-test-memory-absolute-value>
|
||||
<strong>{{format-bytes this.totalReservedMemory}}</strong> / {{format-bytes this.totalMemory}} reserved
|
||||
<strong>
|
||||
{{format-bytes this.totalReservedMemory}}
|
||||
</strong>
|
||||
/
|
||||
{{format-bytes this.totalMemory}}
|
||||
reserved
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-metric">
|
||||
<p class="metric">{{this.totalCPUFormatted}} <span class="metric-units">{{this.totalCPUUnits}}</span> <span class="metric-label">of CPU</span></p>
|
||||
<p class="metric">
|
||||
{{this.totalCPUFormatted}}
|
||||
<span class="metric-units">
|
||||
{{this.totalCPUUnits}}
|
||||
</span>
|
||||
<span class="metric-label">
|
||||
of CPU
|
||||
</span>
|
||||
</p>
|
||||
<div class="columns graphic">
|
||||
<div class="column">
|
||||
<div class="inline-chart" data-test-percentage-bar>
|
||||
@@ -213,17 +437,25 @@
|
||||
data-test-cpu-progress-bar
|
||||
class="progress is-info is-small"
|
||||
value="{{this.reservedCPUPercent}}"
|
||||
max="1">
|
||||
max="1"
|
||||
>
|
||||
{{this.reservedCPUPercent}}
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-minimum">
|
||||
<span class="nowrap" data-test-cpu-percentage>{{format-percentage this.reservedCPUPercent total=1}}</span>
|
||||
<span class="nowrap" data-test-cpu-percentage>
|
||||
{{format-percentage this.reservedCPUPercent total=1}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotation" data-test-cpu-absolute-value>
|
||||
<strong>{{format-hertz this.totalReservedCPU}}</strong> / {{format-hertz this.totalCPU}} reserved
|
||||
<strong>
|
||||
{{format-hertz this.totalReservedCPU}}
|
||||
</strong>
|
||||
/
|
||||
{{format-hertz this.totalCPU}}
|
||||
reserved
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -236,9 +468,10 @@
|
||||
@allocations={{this.model.allocations}}
|
||||
@onAllocationSelect={{action this.setAllocation}}
|
||||
@onNodeSelect={{action this.setNode}}
|
||||
@onDataError={{action this.handleTopoVizDataError}}/>
|
||||
@onDataError={{action this.handleTopoVizDataError}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
@@ -1,28 +0,0 @@
|
||||
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)];
|
||||
}
|
||||
};
|
||||
@@ -94,6 +94,7 @@
|
||||
"ember-power-select": "^4.1.3",
|
||||
"ember-qunit": "^4.6.0",
|
||||
"ember-qunit-nice-errors": "^1.2.0",
|
||||
"ember-render-helpers": "^0.2.0",
|
||||
"ember-resolver": "^8.0.0",
|
||||
"ember-responsive": "^3.0.4",
|
||||
"ember-sinon": "^4.0.0",
|
||||
|
||||
@@ -68,8 +68,8 @@ module('Acceptance | client detail', function(hooks) {
|
||||
);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('clients.client').text,
|
||||
node.id.split('-')[0],
|
||||
'Second breadcrumb says the node short id'
|
||||
`Client ${node.id.split('-')[0]}`,
|
||||
'Second breadcrumb is a titled breadcrumb saying the node short id'
|
||||
);
|
||||
await Layout.breadcrumbFor('clients.index').visit();
|
||||
assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
|
||||
|
||||
@@ -36,7 +36,7 @@ module('Acceptance | client monitor', function(hooks) {
|
||||
await ClientMonitor.visit({ id: node.id });
|
||||
|
||||
assert.equal(Layout.breadcrumbFor('clients.index').text, 'Clients');
|
||||
assert.equal(Layout.breadcrumbFor('clients.client').text, node.id.split('-')[0]);
|
||||
assert.equal(Layout.breadcrumbFor('clients.client').text, `Client ${node.id.split('-')[0]}`);
|
||||
|
||||
await Layout.breadcrumbFor('clients.index').visit();
|
||||
assert.equal(currentURL(), '/clients');
|
||||
|
||||
@@ -33,9 +33,12 @@ module('Acceptance | server monitor', function(hooks) {
|
||||
|
||||
test('/servers/:id/monitor should have a breadcrumb trail linking back to servers', async function(assert) {
|
||||
await ServerMonitor.visit({ name: agent.name });
|
||||
|
||||
assert.equal(Layout.breadcrumbFor('servers.index').text, 'Servers');
|
||||
assert.equal(Layout.breadcrumbFor('servers.server').text, agent.name);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('servers.index').text,
|
||||
'Servers',
|
||||
'The page should read the breadcrumb Servers'
|
||||
);
|
||||
assert.equal(Layout.breadcrumbFor('servers.server').text, `Server ${agent.name}`);
|
||||
|
||||
await Layout.breadcrumbFor('servers.index').visit();
|
||||
assert.equal(currentURL(), '/servers');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { currentURL } from '@ember/test-helpers';
|
||||
import { currentURL, waitFor } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
@@ -57,26 +57,27 @@ module('Acceptance | task detail', function(hooks) {
|
||||
const job = server.db.jobs.find(jobId);
|
||||
|
||||
const shortId = allocation.id.split('-')[0];
|
||||
|
||||
assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'Jobs is the first breadcrumb');
|
||||
|
||||
await waitFor('[data-test-job-breadcrumb]');
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('jobs.job.index').text,
|
||||
job.name,
|
||||
`Job ${job.name}`,
|
||||
'Job is the second breadcrumb'
|
||||
);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('jobs.job.task-group').text,
|
||||
taskGroup,
|
||||
`Task Group ${taskGroup}`,
|
||||
'Task Group is the third breadcrumb'
|
||||
);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('allocations.allocation').text,
|
||||
shortId,
|
||||
`Allocation ${shortId}`,
|
||||
'Allocation short id is the fourth breadcrumb'
|
||||
);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('allocations.allocation.task').text,
|
||||
task.name,
|
||||
`Task ${task.name}`,
|
||||
'Task name is the fifth breadcrumb'
|
||||
);
|
||||
|
||||
|
||||
@@ -122,12 +122,12 @@ module('Acceptance | task group detail', function(hooks) {
|
||||
assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'First breadcrumb says jobs');
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('jobs.job.index').text,
|
||||
job.name,
|
||||
`Job ${job.name}`,
|
||||
'Second breadcrumb says the job name'
|
||||
);
|
||||
assert.equal(
|
||||
Layout.breadcrumbFor('jobs.job.task-group').text,
|
||||
taskGroup.name,
|
||||
`Task Group ${taskGroup.name}`,
|
||||
'Third breadcrumb says the job name'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,49 +1,31 @@
|
||||
import Service from '@ember/service';
|
||||
import RSVP from 'rsvp';
|
||||
/* eslint-disable ember-a11y-testing/a11y-audit-called */
|
||||
import { setComponentTemplate } from '@ember/component';
|
||||
import Component from '@glimmer/component';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { findAll, render, settled } from '@ember/test-helpers';
|
||||
import { findAll, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import PromiseObject from 'nomad-ui/utils/classes/promise-object';
|
||||
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
|
||||
module('Integration | Component | app breadcrumbs', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
const mockBreadcrumbs = Service.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.breadcrumbs = [];
|
||||
},
|
||||
});
|
||||
|
||||
this.owner.register('service:breadcrumbs', mockBreadcrumbs);
|
||||
this.breadcrumbs = this.owner.lookup('service:breadcrumbs');
|
||||
});
|
||||
|
||||
const commonCrumbs = [{ label: 'One', args: ['one'] }, { label: 'Two', args: ['two'] }];
|
||||
|
||||
const template = hbs`
|
||||
<ul><AppBreadcrumbs /></ul>
|
||||
`;
|
||||
|
||||
test('breadcrumbs comes from the breadcrumbs service', async function(assert) {
|
||||
this.breadcrumbs.set('breadcrumbs', commonCrumbs);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.equal(
|
||||
findAll('[data-test-breadcrumb]').length,
|
||||
commonCrumbs.length,
|
||||
'The number of crumbs matches the crumbs from the service'
|
||||
);
|
||||
});
|
||||
const commonCrumbs = [
|
||||
{ label: 'Jobs', args: ['jobs.index'] },
|
||||
{ label: 'Job', args: ['jobs.job.index'] },
|
||||
];
|
||||
|
||||
test('every breadcrumb is rendered correctly', async function(assert) {
|
||||
this.breadcrumbs.set('breadcrumbs', commonCrumbs);
|
||||
this.set('commonCrumbs', commonCrumbs);
|
||||
await render(hbs`
|
||||
<AppBreadcrumbs />
|
||||
{{#each this.commonCrumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{hash label=crumb.label args=crumb.args }} />
|
||||
{{/each}}
|
||||
`);
|
||||
|
||||
await render(template);
|
||||
assert
|
||||
.dom('[data-test-breadcrumb-default]')
|
||||
.exists('We register the default breadcrumb component if no type is specified on the crumb');
|
||||
|
||||
const renderedCrumbs = findAll('[data-test-breadcrumb]');
|
||||
|
||||
@@ -56,36 +38,39 @@ module('Integration | Component | app breadcrumbs', function(hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('when breadcrumbs are pending promises, an ellipsis is rendered', async function(assert) {
|
||||
let resolvePromise;
|
||||
const promise = new RSVP.Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
test('when we register a crumb with a type property, a dedicated breadcrumb/<type> component renders', async function(assert) {
|
||||
const crumbs = [
|
||||
{ label: 'Jobs', args: ['jobs.index'] },
|
||||
{ type: 'special', label: 'Job', args: ['jobs.job.index'] },
|
||||
];
|
||||
this.set('crumbs', crumbs);
|
||||
|
||||
this.breadcrumbs.set('breadcrumbs', [
|
||||
{ label: 'One', args: ['one'] },
|
||||
PromiseObject.create({ promise }),
|
||||
{ label: 'Three', args: ['three'] },
|
||||
]);
|
||||
|
||||
await render(template);
|
||||
|
||||
assert.equal(
|
||||
findAll('[data-test-breadcrumb]')[1].textContent.trim(),
|
||||
'…',
|
||||
'Promise breadcrumb is in a loading state'
|
||||
class MockComponent extends Component {}
|
||||
this.owner.register(
|
||||
'component:breadcrumbs/special',
|
||||
setComponentTemplate(
|
||||
hbs`
|
||||
<div data-test-breadcrumb-special>Test</div>
|
||||
`,
|
||||
MockComponent
|
||||
)
|
||||
);
|
||||
|
||||
await componentA11yAudit(this.element, assert);
|
||||
await render(hbs`
|
||||
<AppBreadcrumbs />
|
||||
{{#each this.crumbs as |crumb|}}
|
||||
<Breadcrumb @crumb={{hash type=crumb.type label=crumb.label args=crumb.args }} />
|
||||
{{/each}}
|
||||
`);
|
||||
|
||||
resolvePromise({ label: 'Two', args: ['two'] });
|
||||
|
||||
return settled().then(() => {
|
||||
assert.equal(
|
||||
findAll('[data-test-breadcrumb]')[1].textContent.trim(),
|
||||
'Two',
|
||||
'Promise breadcrumb has resolved and now renders Two'
|
||||
assert
|
||||
.dom('[data-test-breadcrumb-special]')
|
||||
.exists(
|
||||
'We can create a new type of breadcrumb component and AppBreadcrumbs will handle rendering by type'
|
||||
);
|
||||
});
|
||||
|
||||
assert
|
||||
.dom('[data-test-breadcrumb-default]')
|
||||
.exists('Default breadcrumb registers if no type is specified');
|
||||
});
|
||||
});
|
||||
|
||||
67
ui/tests/integration/components/breadcrumbs-test.js
Normal file
67
ui/tests/integration/components/breadcrumbs-test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable ember-a11y-testing/a11y-audit-called */
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, findAll, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | breadcrumbs', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it declaratively renders a list of registered crumbs', async function(assert) {
|
||||
this.set('isRegistered', false);
|
||||
this.set('toggleCrumb', () => this.set('isRegistered', !this.isRegistered));
|
||||
await render(hbs`
|
||||
<Breadcrumbs as |bb|>
|
||||
<ul>
|
||||
{{#each bb as |crumb|}}
|
||||
<li data-test-crumb={{crumb.args.crumb}}>{{crumb.args.crumb}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</Breadcrumbs>
|
||||
<button data-test-button type="button" {{on "click" toggleCrumb}}>Toggle</button>
|
||||
<Breadcrumb @crumb={{'Zoey'}} />
|
||||
{{#if this.isRegistered}}
|
||||
<Breadcrumb @crumb={{'Tomster'}} />
|
||||
{{/if}}
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-crumb]').exists({ count: 1 }, 'We register one crumb');
|
||||
assert.dom('[data-test-crumb]').hasText('Zoey', 'The first registered crumb is Zoey');
|
||||
|
||||
await click('[data-test-button]');
|
||||
const crumbs = await findAll('[data-test-crumb]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-crumb]')
|
||||
.exists({ count: 2 }, 'The second crumb registered successfully');
|
||||
assert
|
||||
.dom(crumbs[0])
|
||||
.hasText('Zoey', 'Breadcrumbs maintain the order in which they are declared');
|
||||
assert
|
||||
.dom(crumbs[1])
|
||||
.hasText('Tomster', 'Breadcrumbs maintain the order in which they are declared');
|
||||
|
||||
await click('[data-test-button]');
|
||||
assert.dom('[data-test-crumb]').exists({ count: 1 }, 'We deregister one crumb');
|
||||
assert
|
||||
.dom('[data-test-crumb]')
|
||||
.hasText('Zoey', 'Zoey remains in the template after Tomster deregisters');
|
||||
});
|
||||
|
||||
test('it can register complex crumb objects', async function(assert) {
|
||||
await render(hbs`
|
||||
<Breadcrumbs as |bb|>
|
||||
<ul>
|
||||
{{#each bb as |crumb|}}
|
||||
<li data-test-crumb>{{crumb.args.crumb.name}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</Breadcrumbs>
|
||||
<Breadcrumb @crumb={{hash name='Tomster'}} />
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-crumb]')
|
||||
.hasText('Tomster', 'We can access the registered breadcrumbs in the template');
|
||||
});
|
||||
});
|
||||
191
ui/tests/integration/components/trigger-test.js
Normal file
191
ui/tests/integration/components/trigger-test.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, waitFor } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | trigger', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
module('Synchronous Interactions', function() {
|
||||
test('it can trigger a synchronous action', async function(assert) {
|
||||
this.set('name', 'Tomster');
|
||||
this.set('changeName', () => this.set('name', 'Zoey'));
|
||||
await render(hbs`
|
||||
<Trigger @do={{this.changeName}} as |trigger|>
|
||||
<h2 data-test-name>{{this.name}}</h2>
|
||||
<button data-test-button type="button" {{on "click" trigger.fns.do}}>Change my name.</button>
|
||||
</Trigger>
|
||||
`);
|
||||
assert.dom('[data-test-name]').hasText('Tomster', 'Initial state renders correctly.');
|
||||
|
||||
await click('[data-test-button]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-name]')
|
||||
.hasText('Zoey', 'The name property changes when the button is clicked');
|
||||
});
|
||||
|
||||
test('it sets the result of the action', async function(assert) {
|
||||
this.set('tomster', () => 'Tomster');
|
||||
await render(hbs`
|
||||
<Trigger @do={{this.tomster}} as |trigger|>
|
||||
{{#if trigger.data.result}}
|
||||
<h2 data-test-name>{{trigger.data.result}}</h2>
|
||||
{{/if}}
|
||||
<button data-test-button {{on "click" trigger.fns.do}}>Generate</button>
|
||||
</Trigger>
|
||||
`);
|
||||
assert
|
||||
.dom('[data-test-name]')
|
||||
.doesNotExist('Initial state does not render because there is no result yet.');
|
||||
|
||||
await click('[data-test-button]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-name]')
|
||||
.hasText('Tomster', 'The result state updates after the triggered action');
|
||||
});
|
||||
});
|
||||
|
||||
module('Asynchronous Interactions', function() {
|
||||
test('it can trigger an asynchronous action', async function(assert) {
|
||||
this.set(
|
||||
'onTrigger',
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.set('resolve', resolve);
|
||||
})
|
||||
);
|
||||
|
||||
await render(hbs`
|
||||
<Trigger @do={{this.onTrigger}} as |trigger|>
|
||||
{{#if trigger.data.isBusy}}
|
||||
<div data-test-div-loading>...Loading</div>
|
||||
{{/if}}
|
||||
{{#if trigger.data.isSuccess}}
|
||||
<div data-test-div>Success!</div>
|
||||
{{/if}}
|
||||
<button data-test-button {{on "click" trigger.fns.do}}>Click Me</button>
|
||||
</Trigger>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.doesNotExist('The div does not render until after the action dispatches successfully');
|
||||
|
||||
await click('[data-test-button]');
|
||||
assert
|
||||
.dom('[data-test-div-loading]')
|
||||
.exists('Loading state is displayed when the action hasnt resolved yet');
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.doesNotExist('Success message does not display until after promise resolves');
|
||||
|
||||
this.resolve();
|
||||
await waitFor('[data-test-div]');
|
||||
assert
|
||||
.dom('[data-test-div-loading]')
|
||||
.doesNotExist(
|
||||
'Loading state is no longer rendered after state changes from busy to success'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.exists('Action has dispatched successfully after the promise resolves');
|
||||
|
||||
await click('[data-test-button]');
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.doesNotExist('Aftering clicking the button, again, the state is reset');
|
||||
assert
|
||||
.dom('[data-test-div-loading]')
|
||||
.exists('After clicking the button, again, we are back in the loading state');
|
||||
|
||||
this.resolve();
|
||||
await waitFor('[data-test-div]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.exists(
|
||||
'An new action and new promise resolve after clicking the button for the second time'
|
||||
);
|
||||
});
|
||||
|
||||
test('it handles the success state', async function(assert) {
|
||||
this.set(
|
||||
'onTrigger',
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.set('resolve', resolve);
|
||||
})
|
||||
);
|
||||
this.set('onSuccess', () => assert.step('On success happened'));
|
||||
|
||||
await render(hbs`
|
||||
<Trigger @do={{this.onTrigger}} @onSuccess={{this.onSuccess}} as |trigger|>
|
||||
{{#if trigger.data.isSuccess}}
|
||||
<span data-test-div>Success!</span>
|
||||
{{/if}}
|
||||
<button data-test-button {{on "click" trigger.fns.do}}>Click Me</button>
|
||||
</Trigger>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.doesNotExist('No text should appear until after the onSuccess callback is fired');
|
||||
await click('[data-test-button]');
|
||||
this.resolve();
|
||||
await waitFor('[data-test-div]');
|
||||
assert.verifySteps(['On success happened']);
|
||||
});
|
||||
|
||||
test('it handles the error state', async function(assert) {
|
||||
this.set(
|
||||
'onTrigger',
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
this.set('reject', reject);
|
||||
})
|
||||
);
|
||||
this.set('onError', () => {
|
||||
assert.step('On error happened');
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<Trigger @do={{this.onTrigger}} @onError={{this.onError}} as |trigger|>
|
||||
{{#if trigger.data.isBusy}}
|
||||
<div data-test-div-loading>...Loading</div>
|
||||
{{/if}}
|
||||
{{#if trigger.data.isError}}
|
||||
<span data-test-span>Error!</span>
|
||||
{{/if}}
|
||||
<button data-test-button {{on "click" trigger.fns.do}}>Click Me</button>
|
||||
</Trigger>
|
||||
`);
|
||||
|
||||
await click('[data-test-button]');
|
||||
assert
|
||||
.dom('[data-test-div-loading]')
|
||||
.exists('Loading state is displayed when the action hasnt resolved yet');
|
||||
|
||||
assert
|
||||
.dom('[data-test-div]')
|
||||
.doesNotExist('No text should appear until after the onError callback is fired');
|
||||
|
||||
this.reject();
|
||||
await waitFor('[data-test-span]');
|
||||
assert.verifySteps(['On error happened']);
|
||||
|
||||
await click('[data-test-button]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-div-loading]')
|
||||
.exists('The previous error state was cleared and we show loading, again.');
|
||||
|
||||
assert.dom('[data-test-div]').doesNotExist('The error state is cleared');
|
||||
|
||||
this.reject();
|
||||
await waitFor('[data-test-span]');
|
||||
assert.verifySteps(['On error happened'], 'The error dispatches');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
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 RSVP from 'rsvp';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } 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(),
|
||||
});
|
||||
|
||||
module('Unit | Service | Breadcrumbs', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
this.subject = function() {
|
||||
return this.owner.factoryFor('service:breadcrumbs').create();
|
||||
};
|
||||
});
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
const mockRouter = Service.extend({
|
||||
currentRouteName: 'application',
|
||||
currentURL: '/',
|
||||
});
|
||||
|
||||
this.owner.register('service:router', mockRouter);
|
||||
this.router = this.owner.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: this.owner.lookup('service:router'),
|
||||
model: alias('router.currentURL'),
|
||||
});
|
||||
|
||||
this.owner.register('route:none', none);
|
||||
this.owner.register('route:none.more-none', none);
|
||||
this.owner.register('route:static', fixed);
|
||||
this.owner.register('route:static.many', manyFixed);
|
||||
this.owner.register('route:dynamic', dynamic);
|
||||
this.owner.register('route:dynamic.many', manyDynamic);
|
||||
this.owner.register('route:promise', promise);
|
||||
this.owner.register('route:url', fromURL);
|
||||
});
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
31
ui/yarn.lock
31
ui/yarn.lock
@@ -4345,6 +4345,13 @@ ansi-to-html@^0.6.11, ansi-to-html@^0.6.6:
|
||||
dependencies:
|
||||
entities "^1.1.2"
|
||||
|
||||
ansi-to-html@^0.6.15:
|
||||
version "0.6.15"
|
||||
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7"
|
||||
integrity sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==
|
||||
dependencies:
|
||||
entities "^2.0.0"
|
||||
|
||||
ansicolors@~0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
|
||||
@@ -8925,6 +8932,22 @@ ember-cli-typescript@^3.0.0, ember-cli-typescript@^3.1.3, ember-cli-typescript@^
|
||||
stagehand "^1.0.0"
|
||||
walk-sync "^2.0.0"
|
||||
|
||||
ember-cli-typescript@^4.0.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-4.2.1.tgz#54d08fc90318cc986f3ea562f93ce58a6cc4c24d"
|
||||
integrity sha512-0iKTZ+/wH6UB/VTWKvGuXlmwiE8HSIGcxHamwNhEC5x1mN3z8RfvsFZdQWYUzIWFN2Tek0gmepGRPTwWdBYl/A==
|
||||
dependencies:
|
||||
ansi-to-html "^0.6.15"
|
||||
broccoli-stew "^3.0.0"
|
||||
debug "^4.0.0"
|
||||
execa "^4.0.0"
|
||||
fs-extra "^9.0.1"
|
||||
resolve "^1.5.0"
|
||||
rsvp "^4.8.1"
|
||||
semver "^7.3.2"
|
||||
stagehand "^1.0.0"
|
||||
walk-sync "^2.2.0"
|
||||
|
||||
ember-cli-typescript@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-4.1.0.tgz#2ff17be2e6d26b58c88b1764cb73887e7176618b"
|
||||
@@ -9426,6 +9449,14 @@ ember-qunit@^4.6.0:
|
||||
ember-cli-test-loader "^2.2.0"
|
||||
qunit "^2.9.3"
|
||||
|
||||
ember-render-helpers@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-render-helpers/-/ember-render-helpers-0.2.0.tgz#5f7af8ee74ae29f85e0d156b2775edff23f6de21"
|
||||
integrity sha512-MnqGS8BnY3GJ+n5RZVVRqCwKjfXXMr5quKyqNu1vxft8oslOJuZ1f1dOesQouD+6LwD4Y9tWRVKNw+LOqM9ocw==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.23.0"
|
||||
ember-cli-typescript "^4.0.0"
|
||||
|
||||
ember-resolver@^8.0.0:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ember-resolver/-/ember-resolver-8.0.2.tgz#8a45a744aaf5391eb52b4cb393b3b06d2db1975c"
|
||||
|
||||
Reference in New Issue
Block a user