diff --git a/ui/app/components/job-page/parts/stats-box.js b/ui/app/components/job-page/parts/stats-box.js
new file mode 100644
index 000000000..62080fbff
--- /dev/null
+++ b/ui/app/components/job-page/parts/stats-box.js
@@ -0,0 +1,6 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+
+export default class StatsBox extends Component {
+ @service system;
+}
diff --git a/ui/app/models/job.js b/ui/app/models/job.js
index 8182546df..43c82e5d1 100644
--- a/ui/app/models/job.js
+++ b/ui/app/models/job.js
@@ -2,7 +2,7 @@ import { alias, equal, or, and, mapBy } from '@ember/object/computed';
import { computed } from '@ember/object';
import Model from '@ember-data/model';
import { attr, belongsTo, hasMany } from '@ember-data/model';
-import { fragmentArray } from 'ember-data-model-fragments/attributes';
+import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
import RSVP from 'rsvp';
import { assert } from '@ember/debug';
import classic from 'ember-classic-decorator';
@@ -23,7 +23,8 @@ export default class Job extends Model {
@attr('number') createIndex;
@attr('number') modifyIndex;
@attr('date') submitTime;
- @attr() meta;
+
+ @fragment('structured-attributes') meta;
// True when the job is the parent periodic or parameterized jobs
// Instances of periodic or parameterized jobs are false for both properties
diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js
index ebb2618f5..5aebcf9f2 100644
--- a/ui/app/models/node-driver.js
+++ b/ui/app/models/node-driver.js
@@ -9,11 +9,11 @@ import { fragment } from 'ember-data-model-fragments/attributes';
export default class NodeDriver extends Fragment {
@fragmentOwner() node;
- @fragment('node-attributes') attributes;
+ @fragment('structured-attributes') attributes;
- @computed('name', 'attributes.attributesStructured')
+ @computed('name', 'attributes.structured')
get attributesShort() {
- const attributes = this.get('attributes.attributesStructured');
+ const attributes = this.get('attributes.structured');
return get(attributes, `driver.${this.name}`);
}
diff --git a/ui/app/models/node.js b/ui/app/models/node.js
index f3d8a6769..8d63616c3 100644
--- a/ui/app/models/node.js
+++ b/ui/app/models/node.js
@@ -25,8 +25,8 @@ export default class Node extends Model {
// Available from single response
@attr('string') httpAddr;
@attr('boolean') tlsEnabled;
- @fragment('node-attributes') attributes;
- @fragment('node-attributes') meta;
+ @fragment('structured-attributes') attributes;
+ @fragment('structured-attributes') meta;
@fragment('resources') resources;
@fragment('resources') reserved;
@fragment('drain-strategy') drainStrategy;
diff --git a/ui/app/models/node-attributes.js b/ui/app/models/structured-attributes.js
similarity index 74%
rename from ui/app/models/node-attributes.js
rename to ui/app/models/structured-attributes.js
index dbae04df8..2aba23241 100644
--- a/ui/app/models/node-attributes.js
+++ b/ui/app/models/structured-attributes.js
@@ -5,12 +5,12 @@ import flat from 'flat';
const { unflatten } = flat;
-export default class NodeAttributes extends Fragment {
- @attr() nodeAttributes;
+export default class StructuredAttributes extends Fragment {
+ @attr() raw;
- @computed('nodeAttributes')
- get attributesStructured() {
- const original = this.nodeAttributes;
+ @computed('raw')
+ get structured() {
+ const original = this.raw;
if (!original) {
return undefined;
@@ -31,8 +31,8 @@ export default class NodeAttributes extends Fragment {
//
// ex: nodeAttrs.get('driver.docker')
// [ "1", { version: "17.05.0-ce", volumes: { enabled: "1" } } ]
- if (this.attributesStructured) {
- return get(this.attributesStructured, key);
+ if (this.structured) {
+ return get(this.structured, key);
}
}
}
diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js
index fafed701a..5ea8f2667 100644
--- a/ui/app/models/task-group.js
+++ b/ui/app/models/task-group.js
@@ -24,10 +24,10 @@ export default class TaskGroup extends Fragment {
@attr() meta;
- @computed('job.meta', 'meta')
+ @computed('job.meta.raw', 'meta')
get mergedMeta() {
return {
- ...this.job.meta,
+ ...this.job.get('meta.raw'),
...this.meta,
};
}
diff --git a/ui/app/serializers/node-attributes.js b/ui/app/serializers/node-attributes.js
deleted file mode 100644
index cbd2c5c2e..000000000
--- a/ui/app/serializers/node-attributes.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import ApplicationSerializer from './application';
-
-export default class NodeAttributes extends ApplicationSerializer {
- normalize(typeHash, hash) {
- return super.normalize(typeHash, { NodeAttributes: hash });
- }
-}
diff --git a/ui/app/serializers/structured-attributes.js b/ui/app/serializers/structured-attributes.js
new file mode 100644
index 000000000..87fd6dc8b
--- /dev/null
+++ b/ui/app/serializers/structured-attributes.js
@@ -0,0 +1,7 @@
+import ApplicationSerializer from './application';
+
+export default class StructuredAttributes extends ApplicationSerializer {
+ normalize(typeHash, hash) {
+ return super.normalize(typeHash, { Raw: hash });
+ }
+}
diff --git a/ui/app/styles/components/inline-definitions.scss b/ui/app/styles/components/inline-definitions.scss
index 50f72b905..97b398d9c 100644
--- a/ui/app/styles/components/inline-definitions.scss
+++ b/ui/app/styles/components/inline-definitions.scss
@@ -1,9 +1,10 @@
.inline-definitions {
.label {
text-transform: uppercase;
- display: inline;
+ display: inline-block;
color: darken($grey-blue, 20%);
margin-right: 2rem;
+ margin-bottom: 0;
font-size: inherit;
font-weight: $weight-semibold;
}
diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss
index 30a1837f3..14950c368 100644
--- a/ui/app/styles/core/tag.scss
+++ b/ui/app/styles/core/tag.scss
@@ -62,4 +62,9 @@
font-size: 0.7rem;
vertical-align: 2px;
}
+
+ .icon {
+ height: 1rem;
+ width: 1rem;
+ }
}
diff --git a/ui/app/templates/clients/client/index.hbs b/ui/app/templates/clients/client/index.hbs
index d3135b7ff..471a799e6 100644
--- a/ui/app/templates/clients/client/index.hbs
+++ b/ui/app/templates/clients/client/index.hbs
@@ -441,7 +441,7 @@
{{capitalize a.item.name}} Attributes
- {{#if a.item.attributes.attributesStructured}}
+ {{#if a.item.attributes.structured}}
Meta
- {{#if this.model.meta.attributesStructured}}
+ {{#if this.model.meta.structured}}
{{else}}
diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs
index 93a792d3e..6030a6020 100644
--- a/ui/app/templates/components/job-page/batch.hbs
+++ b/ui/app/templates/components/job-page/batch.hbs
@@ -3,15 +3,7 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
@@ -24,4 +16,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
+
+
diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs
index 5a4777057..83c6f1b0f 100644
--- a/ui/app/templates/components/job-page/parameterized-child.hbs
+++ b/ui/app/templates/components/job-page/parameterized-child.hbs
@@ -3,21 +3,16 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}} |
-
- Parent:
+
+ <:before-namespace>
+
+ Parent
{{this.job.parent.name}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
+
{{#if this.job.hasClientStatus}}
{{#if this.job.definition.Meta}}
-
+
{{else}}
diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs
index db3c9a942..ea77a4d72 100644
--- a/ui/app/templates/components/job-page/parameterized.hbs
+++ b/ui/app/templates/components/job-page/parameterized.hbs
@@ -5,15 +5,7 @@
Parameterized
-
-
- Version: {{this.job.version}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
+
+
diff --git a/ui/app/templates/components/job-page/parts/meta.hbs b/ui/app/templates/components/job-page/parts/meta.hbs
new file mode 100644
index 000000000..595f64776
--- /dev/null
+++ b/ui/app/templates/components/job-page/parts/meta.hbs
@@ -0,0 +1,13 @@
+{{#if @job.meta.structured}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/job-page/parts/stats-box.hbs b/ui/app/templates/components/job-page/parts/stats-box.hbs
new file mode 100644
index 000000000..1767a3dce
--- /dev/null
+++ b/ui/app/templates/components/job-page/parts/stats-box.hbs
@@ -0,0 +1,54 @@
+
+
+ Job Details
+
+ Type
+ {{@job.type}}
+
+
+ Priority
+ {{@job.priority}}
+
+
+ Version
+ {{@job.version}}
+
+ {{yield to="before-namespace"}}
+ {{#if (and @job.namespace this.system.shouldShowNamespaces)}}
+
+ Namespace
+ {{@job.namespace.name}}
+
+ {{/if}}
+ {{yield to="after-namespace"}}
+
+
+ {{#if @job.meta.structured.pack.name}}
+
+ Pack Details
+
+ Name
+ {{@job.meta.structured.pack.name}}
+
+ {{#if @job.meta.structured.pack.registry}}
+
+ Registry
+ {{@job.meta.structured.pack.registry}}
+
+ {{/if}}
+ {{#if @job.meta.structured.pack.version}}
+
+ Version
+ {{@job.meta.structured.pack.version}}
+
+ {{/if}}
+ {{#if @job.meta.structured.pack.revision}}
+
+ Revision
+ {{@job.meta.structured.pack.revision}}
+
+ {{/if}}
+ {{yield to="pack"}}
+
+ {{/if}}
+
diff --git a/ui/app/templates/components/job-page/parts/title.hbs b/ui/app/templates/components/job-page/parts/title.hbs
index c6e1b06ff..068c0731f 100644
--- a/ui/app/templates/components/job-page/parts/title.hbs
+++ b/ui/app/templates/components/job-page/parts/title.hbs
@@ -1,6 +1,12 @@
{{or this.title this.job.name}}
+ {{#if @job.meta.structured.pack}}
+
+ {{x-icon "box" class= "test"}}
+ Pack
+
+ {{/if}}
{{this.job.status}}
{{yield}}
diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs
index 965c5a41a..65bcad3cc 100644
--- a/ui/app/templates/components/job-page/periodic-child.hbs
+++ b/ui/app/templates/components/job-page/periodic-child.hbs
@@ -3,21 +3,16 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}} |
-
- Parent:
+
+ <:before-namespace>
+
+ Parent
{{this.job.parent.name}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
+
{{#if this.job.hasClientStatus}}
-
\ No newline at end of file
+
+
+
diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs
index b87e034e9..103cdd1d9 100644
--- a/ui/app/templates/components/job-page/periodic.hbs
+++ b/ui/app/templates/components/job-page/periodic.hbs
@@ -6,16 +6,14 @@
Force Launch
-
-
- Version: {{this.job.version}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
- | Cron: {{this.job.periodicDetails.Spec}}
-
-
+
+ <:after-namespace>
+
+ Cron
+ {{this.job.periodicDetails.Spec}}
+
+
+
+
+
diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs
index d8180f244..3c5c4e4a5 100644
--- a/ui/app/templates/components/job-page/service.hbs
+++ b/ui/app/templates/components/job-page/service.hbs
@@ -3,15 +3,7 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
{{#if (can "accept recommendations")}}
{{#each this.job.recommendationSummaries as |summary|}}
@@ -32,4 +24,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
+
+
diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs
index 35f6d6faa..c5fd40faa 100644
--- a/ui/app/templates/components/job-page/sysbatch.hbs
+++ b/ui/app/templates/components/job-page/sysbatch.hbs
@@ -3,15 +3,7 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
+
+
\ No newline at end of file
diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs
index 1f1db67d1..76b1ad7c2 100644
--- a/ui/app/templates/components/job-page/system.hbs
+++ b/ui/app/templates/components/job-page/system.hbs
@@ -3,15 +3,7 @@
-
-
- Type: {{this.job.type}} |
- Priority: {{this.job.priority}}
- {{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
- | Namespace: {{this.job.namespace.name}}
- {{/if}}
-
-
+
{{#if (can "accept recommendations")}}
{{#each this.job.recommendationSummaries as |summary|}}
@@ -35,4 +27,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
+
+
diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js
index bf135888f..24d6cf1a7 100644
--- a/ui/mirage/factories/job.js
+++ b/ui/mirage/factories/job.js
@@ -53,6 +53,8 @@ export default Factory.extend({
childrenCount: () => faker.random.number({ min: 1, max: 2 }),
+ meta: null,
+
periodic: trait({
type: 'batch',
periodic: true,
@@ -139,6 +141,13 @@ export default Factory.extend({
payload: window.btoa(faker.lorem.sentence()),
}),
+ pack: trait({
+ meta: () => ({
+ 'pack.name': faker.hacker.noun(),
+ 'pack.version': faker.system.semver(),
+ }),
+ }),
+
createIndex: i => i,
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
diff --git a/ui/mirage/scenarios/sysbatch.js b/ui/mirage/scenarios/sysbatch.js
index 5bb4b57c7..8b3161b8b 100644
--- a/ui/mirage/scenarios/sysbatch.js
+++ b/ui/mirage/scenarios/sysbatch.js
@@ -27,6 +27,8 @@ function sysbatchScenario(server, clientCount) {
createAllocations: true,
});
+ server.create('job', 'pack');
+
['system', 'sysbatch'].forEach(type => {
// Job with 1 task group.
const job1 = server.create('job', {
diff --git a/ui/public/images/icons/box.svg b/ui/public/images/icons/box.svg
new file mode 100644
index 000000000..bf341f81c
--- /dev/null
+++ b/ui/public/images/icons/box.svg
@@ -0,0 +1 @@
+
diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js
index 6ddc145eb..a0cf8409c 100644
--- a/ui/tests/acceptance/job-detail-test.js
+++ b/ui/tests/acceptance/job-detail-test.js
@@ -296,6 +296,47 @@ module('Acceptance | job detail (with namespaces)', function(hooks) {
assert.notOk(JobDetail.execButton.isDisabled);
});
+ test('meta table is displayed if job has meta attributes', async function(assert) {
+ const jobWithMeta = server.create('job', {
+ status: 'running',
+ namespaceId: server.db.namespaces[1].id,
+ meta: {
+ 'a.b': 'c',
+ },
+ });
+
+ await JobDetail.visit({ id: job.id, namespace: server.db.namespaces[1].name });
+ assert.notOk(JobDetail.metaTable, 'Meta table not present');
+
+ await JobDetail.visit({ id: jobWithMeta.id, namespace: server.db.namespaces[1].name });
+ assert.ok(JobDetail.metaTable, 'Meta table is present');
+ });
+
+ test('pack details are displayed', async function(assert) {
+ const namespace = server.db.namespaces[1].id;
+ const jobFromPack = server.create('job', {
+ status: 'running',
+ namespaceId: namespace,
+ meta: {
+ 'pack.name': 'my-pack',
+ 'pack.version': '1.0.0',
+ },
+ });
+
+ await JobDetail.visit({ id: jobFromPack.id, namespace });
+ assert.ok(JobDetail.packTag, 'Pack tag is present');
+ assert.equal(
+ JobDetail.packStatFor('name').text,
+ `Name ${jobFromPack.meta['pack.name']}`,
+ `Pack name is ${jobFromPack.meta['pack.name']}`
+ );
+ assert.equal(
+ JobDetail.packStatFor('version').text,
+ `Version ${jobFromPack.meta['pack.version']}`,
+ `Pack version is ${jobFromPack.meta['pack.version']}`
+ );
+ });
+
test('resource recommendations show when they exist and can be expanded, collapsed, and processed', async function(assert) {
server.create('feature', { name: 'Dynamic Application Sizing' });
diff --git a/ui/tests/pages/jobs/detail.js b/ui/tests/pages/jobs/detail.js
index 92e083e2a..38b45294a 100644
--- a/ui/tests/pages/jobs/detail.js
+++ b/ui/tests/pages/jobs/detail.js
@@ -34,6 +34,9 @@ export default create({
stop: twoStepButton('[data-test-stop]'),
start: twoStepButton('[data-test-start]'),
+ packTag: isPresent('[data-test-pack-tag]'),
+ metaTable: isPresent('[data-test-meta]'),
+
execButton: {
scope: '[data-test-exec-button]',
isDisabled: property('disabled'),
@@ -60,6 +63,15 @@ export default create({
return this.stats.toArray().findBy('id', id);
},
+ packStats: collection('[data-test-pack-stat]', {
+ id: attribute('data-test-pack-stat'),
+ text: text(),
+ }),
+
+ packStatFor(id) {
+ return this.packStats.toArray().findBy('id', id);
+ },
+
jobClientStatusSummary: jobClientStatusBar('[data-test-job-client-status-bar]'),
childrenSummary: isPresent('[data-test-job-summary] [data-test-children-status-bar]'),
allocationsSummary: isPresent('[data-test-job-summary] [data-test-allocation-status-bar]'),
diff --git a/ui/tests/unit/models/task-group-test.js b/ui/tests/unit/models/task-group-test.js
index bd1839130..7ef246f2f 100644
--- a/ui/tests/unit/models/task-group-test.js
+++ b/ui/tests/unit/models/task-group-test.js
@@ -64,10 +64,12 @@ module('Unit | Model | task-group', function(hooks) {
});
test("should expose mergedMeta as merged with the job's meta", function(assert) {
+ const store = this.owner.lookup('service:store');
+
const jobWithMeta = run(() =>
- this.owner.lookup('service:store').createRecord('job', {
+ store.createRecord('job', {
name: 'example-with-meta',
- meta: { a: 'b' },
+ meta: store.createFragment('structured-attributes', { raw: { a: 'b' } }),
taskGroups: [
{
name: 'one',