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}} +
+
+ Meta +
+
+ +
+
+{{/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 @@
-
-
- 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',