Update job details box (#11288)

This commit is contained in:
Luiz Aoqui 2021-10-12 16:36:10 -04:00 committed by GitHub
parent 76b05f3cd2
commit c0023c6c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 224 additions and 114 deletions

View File

@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class StatsBox extends Component {
@service system;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import ApplicationSerializer from './application';
export default class NodeAttributes extends ApplicationSerializer {
normalize(typeHash, hash) {
return super.normalize(typeHash, { NodeAttributes: hash });
}
}

View File

@ -0,0 +1,7 @@
import ApplicationSerializer from './application';
export default class StructuredAttributes extends ApplicationSerializer {
normalize(typeHash, hash) {
return super.normalize(typeHash, { Raw: hash });
}
}

View File

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

View File

@ -62,4 +62,9 @@
font-size: 0.7rem;
vertical-align: 2px;
}
.icon {
height: 1rem;
width: 1rem;
}
}

View File

@ -441,7 +441,7 @@
<div class="boxed-section-head">
{{capitalize a.item.name}} Attributes
</div>
{{#if a.item.attributes.attributesStructured}}
{{#if a.item.attributes.structured}}
<div class="boxed-section-body is-full-bleed">
<AttributesTable
@attributePairs={{a.item.attributesShort}}
@ -467,17 +467,17 @@
<div class="boxed-section-body is-full-bleed">
<AttributesTable
data-test-attributes
@attributePairs={{this.model.attributes.attributesStructured}}
@attributePairs={{this.model.attributes.structured}}
@class="attributes-table" />
</div>
<div class="boxed-section-head">
Meta
</div>
{{#if this.model.meta.attributesStructured}}
{{#if this.model.meta.structured}}
<div class="boxed-section-body is-full-bleed">
<AttributesTable
data-test-meta
@attributePairs={{this.model.meta.attributesStructured}}
@attributePairs={{this.model.meta.structured}}
@class="attributes-table" />
</div>
{{else}}

View File

@ -3,15 +3,7 @@
<JobPage::Parts::Title @job={{this.job}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong> {{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} </span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}} />
<JobPage::Parts::Summary @job={{this.job}} />
@ -24,4 +16,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
<JobPage::Parts::RecentAllocations @job={{this.job}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -3,21 +3,16 @@
<JobPage::Parts::Title @job={{this.job}} @title={{this.job.trimmedName}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong> {{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} |</span>
<span data-test-job-stat="parent">
<strong>Parent:</strong>
<JobPage::Parts::StatsBox @job={{this.job}}>
<:before-namespace>
<span class="pair" data-test-job-stat="parent">
<span class="term">Parent</span>
<LinkTo @route="jobs.job" @model={{this.job.parent}} @query={{hash jobNamespace=this.job.parent.namespace.name}}>
{{this.job.parent.name}}
</LinkTo>
</span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
</:before-namespace>
</JobPage::Parts::StatsBox>
{{#if this.job.hasClientStatus}}
<JobPage::Parts::JobClientStatusSummary
@ -44,12 +39,7 @@
Meta
</div>
{{#if this.job.definition.Meta}}
<div class="boxed-section-body is-full-bleed">
<AttributesTable
data-test-meta
@attributePairs={{this.job.definition.Meta}}
@class="attributes-table" />
</div>
<JobPage::Parts::Meta @job={{this.job}} />
{{else}}
<div class="boxed-section-body">
<div data-test-empty-meta-message class="empty-message">

View File

@ -5,15 +5,7 @@
<span class="tag is-hollow">Parameterized</span>
</JobPage::Parts::Title>
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="version"><strong>Version:</strong> {{this.job.version}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} </span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}} />
<JobPage::Parts::Summary @job={{this.job}} />
<JobPage::Parts::Children
@ -22,4 +14,6 @@
@sortDescending={{this.sortDescending}}
@currentPage={{this.currentPage}}
@gotoJob={{this.gotoJob}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -0,0 +1,13 @@
{{#if @job.meta.structured}}
<div class="boxed-section">
<div class="boxed-section-head">
Meta
</div>
<div class="boxed-section-body is-full-bleed">
<AttributesTable
data-test-meta
@attributePairs={{@job.meta.structured}}
@class="attributes-table" />
</div>
</div>
{{/if}}

View File

@ -0,0 +1,54 @@
<div class="boxed-section is-small">
<div class="boxed-section-body inline-definitions">
<span class="label" style="width: 6.125rem;">Job Details</span>
<span class="pair" data-test-job-stat="type">
<span class="term">Type</span>
{{@job.type}}
</span>
<span class="pair" data-test-job-stat="priority">
<span class="term">Priority</span>
{{@job.priority}}
</span>
<span class="pair" data-test-job-stat="version">
<span class="term">Version</span>
{{@job.version}}
</span>
{{yield to="before-namespace"}}
{{#if (and @job.namespace this.system.shouldShowNamespaces)}}
<span class="pair" data-test-job-stat="namespace">
<span class="term">Namespace</span>
{{@job.namespace.name}}
</span>
{{/if}}
{{yield to="after-namespace"}}
</div>
{{#if @job.meta.structured.pack.name}}
<div class="boxed-section-body inline-definitions">
<span class="label" style="width: 6.125rem;">Pack Details</span>
<span class="pair" data-test-pack-stat="name">
<span class="term">Name</span>
{{@job.meta.structured.pack.name}}
</span>
{{#if @job.meta.structured.pack.registry}}
<span class="pair" data-test-pack-stat="registry">
<span class="term">Registry</span>
{{@job.meta.structured.pack.registry}}
</span>
{{/if}}
{{#if @job.meta.structured.pack.version}}
<span class="pair" data-test-pack-stat="version">
<span class="term">Version</span>
{{@job.meta.structured.pack.version}}
</span>
{{/if}}
{{#if @job.meta.structured.pack.revision}}
<span class="pair" data-test-pack-stat="revision">
<span class="term">Revision</span>
{{@job.meta.structured.pack.revision}}
</span>
{{/if}}
{{yield to="pack"}}
</div>
{{/if}}
</div>

View File

@ -1,6 +1,12 @@
<h1 class="title with-flex">
<div data-test-job-name>
{{or this.title this.job.name}}
{{#if @job.meta.structured.pack}}
<span data-test-pack-tag class="tag is-hollow">
{{x-icon "box" class= "test"}}
<span>Pack</span>
</span>
{{/if}}
<span class="bumper-left tag {{this.job.statusClass}}" data-test-job-status>{{this.job.status}}</span>
{{yield}}
</div>

View File

@ -3,21 +3,16 @@
<JobPage::Parts::Title @job={{this.job}} @title={{this.job.trimmedName}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong> {{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} |</span>
<span data-test-job-stat="parent">
<strong>Parent:</strong>
<JobPage::Parts::StatsBox @job={{this.job}}>
<:before-namespace>
<span class="pair" data-test-job-stat="parent">
<span class="term">Parent</span>
<LinkTo @route="jobs.job" @model={{this.job.parent}} @query={{hash namespace=this.job.parent.namespace.name}}>
{{this.job.parent.name}}
</LinkTo>
</span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
</:before-namespace>
</JobPage::Parts::StatsBox>
{{#if this.job.hasClientStatus}}
<JobPage::Parts::JobClientStatusSummary
@ -38,4 +33,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
<JobPage::Parts::RecentAllocations @job={{this.job}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -6,16 +6,14 @@
<button data-test-force-launch class="button is-warning is-small is-inline" onclick={{action "forceLaunch"}} type="button">Force Launch</button>
</JobPage::Parts::Title>
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="version"><strong>Version:</strong> {{this.job.version}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} </span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
<span data-test-job-stat="cron"> | <strong>Cron:</strong> <code>{{this.job.periodicDetails.Spec}}</code></span>
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}}>
<:after-namespace>
<span class="pair" data-test-job-stat="cron">
<span class="term">Cron</span>
{{this.job.periodicDetails.Spec}}
</span>
</:after-namespace>
</JobPage::Parts::StatsBox>
<JobPage::Parts::Summary @job={{this.job}} />
<JobPage::Parts::Children
@ -24,4 +22,6 @@
@sortDescending={{this.sortDescending}}
@currentPage={{this.currentPage}}
@gotoJob={{this.gotoJob}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -3,15 +3,7 @@
<JobPage::Parts::Title @job={{this.job}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong> {{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} </span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}} />
{{#if (can "accept recommendations")}}
{{#each this.job.recommendationSummaries as |summary|}}
@ -32,4 +24,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
<JobPage::Parts::RecentAllocations @job={{this.job}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -3,15 +3,7 @@
<JobPage::Parts::Title @job={{this.job}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong>{{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong>{{this.job.priority}}</span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong>{{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}} />
<JobPage::Parts::JobClientStatusSummary
@job={{this.job}}
@ -29,4 +21,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
<JobPage::Parts::RecentAllocations @job={{this.job}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

@ -3,15 +3,7 @@
<JobPage::Parts::Title @job={{this.job}} @handleError={{action "handleError"}} />
<div class="boxed-section job-stats">
<div class="boxed-section-body">
<span data-test-job-stat="type"><strong>Type:</strong> {{this.job.type}} | </span>
<span data-test-job-stat="priority"><strong>Priority:</strong> {{this.job.priority}} </span>
{{#if (and this.job.namespace this.system.shouldShowNamespaces)}}
<span data-test-job-stat="namespace"> | <strong>Namespace:</strong> {{this.job.namespace.name}}</span>
{{/if}}
</div>
</div>
<JobPage::Parts::StatsBox @job={{this.job}} />
{{#if (can "accept recommendations")}}
{{#each this.job.recommendationSummaries as |summary|}}
@ -35,4 +27,6 @@
@gotoTaskGroup={{this.gotoTaskGroup}} />
<JobPage::Parts::RecentAllocations @job={{this.job}} />
<JobPage::Parts::Meta @job={{this.job}} />
</JobPage::Parts::Body>

View File

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

View File

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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M13.206.754a2.75 2.75 0 00-2.412 0L1.983 5.052A1.75 1.75 0 001 6.625v9.902a2.75 2.75 0 001.496 2.448l8.25 4.226a2.75 2.75 0 002.508 0l8.25-4.226A2.75 2.75 0 0023 16.527V6.625a1.75 1.75 0 00-.983-1.573L13.206.754zm-1.754 1.348a1.25 1.25 0 011.096 0l8.11 3.956L12 10.17 3.343 6.058l8.109-3.956zM2.5 7.318v9.21c0 .468.263.898.68 1.112l8.07 4.133V11.474L2.5 7.318zm10.25 14.455l8.07-4.133a1.25 1.25 0 00.68-1.113v-9.21l-8.75 4.157v10.3z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 606 B

View File

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

View File

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

View File

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