Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] Service job status panel #16134

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
faac1de
it begins
philrenaud Feb 10, 2023
32c069f
Hacky demo enabled
philrenaud Feb 15, 2023
2e74e75
Still very hacky but seems deece
philrenaud Feb 15, 2023
a5b5aa6
Floor of at least 3 must be shown
philrenaud Feb 15, 2023
5bbb8cc
Width from on-high
philrenaud Feb 16, 2023
3963a43
Other statuses considered
philrenaud Feb 16, 2023
8b335b6
More sensible allocTypes listing
philrenaud Feb 16, 2023
2c38483
Beginnings of a legend
philrenaud Feb 16, 2023
7c66cf5
Total number of allocs running now maps over job.groups
philrenaud Feb 17, 2023
1b25ec7
Lintfix
philrenaud Feb 17, 2023
d470aad
base the number of slots to hold open on actual tallies, which should…
philrenaud Feb 17, 2023
b4c9991
Versions get yer versions here
philrenaud Feb 17, 2023
d3b0221
Versions lookin like versions
philrenaud Feb 17, 2023
31e9536
Mirage fixup
philrenaud Feb 17, 2023
c14c23a
Adds Remaining as an alloc chart status and adds historical status op…
philrenaud Feb 27, 2023
67ee564
Get tests passing again by making job status static for a sec
philrenaud Feb 27, 2023
d7b6ca1
Historical status panel click actions moved into their own component …
philrenaud Feb 27, 2023
146ef8b
job detail tests plz chill
philrenaud Feb 28, 2023
6d8e7de
Testing if percy is fickle
philrenaud Feb 28, 2023
0934151
Hyper-specfic on summary distribution bar identifier
philrenaud Feb 28, 2023
5656316
Perhaps the 2nd allocSummary item no longer exists with the more accu…
philrenaud Feb 28, 2023
eac79f3
UI Test eschewing the page pattern
philrenaud Mar 1, 2023
1a284eb
Bones of a new acceptance test
philrenaud Mar 1, 2023
f87de80
Track width changes explicitly with window-resize
philrenaud Mar 1, 2023
0c8d10a
testlintfix
philrenaud Mar 1, 2023
7975c96
Alloc counting tests
philrenaud Mar 2, 2023
a837f11
Alloc grouping test
philrenaud Mar 2, 2023
fda3a40
Alloc grouping with complex resizing
philrenaud Mar 2, 2023
ffbb538
Refined the list of showable statuses
philrenaud Mar 2, 2023
33aed24
PR feedback addressed
philrenaud Mar 6, 2023
4d78d97
renamed allocation-row to allocation-status-row
philrenaud Mar 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions ui/app/components/job-page/parts/summary-chart.js
@@ -0,0 +1,24 @@
// @ts-check
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { camelize } from '@ember/string';
import { inject as service } from '@ember/service';

export default class JobPagePartsSummaryChartComponent extends Component {
@service router;

@action
gotoAllocations(status) {
this.router.transitionTo('jobs.job.allocations', this.args.job, {
queryParams: {
status: JSON.stringify(status),
namespace: this.args.job.get('namespace.name'),
},
});
}

@action
onSliceClick(ev, slice) {
this.gotoAllocations([camelize(slice.label)]);
}
philrenaud marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 1 addition & 17 deletions ui/app/components/job-page/parts/summary.js
@@ -1,9 +1,8 @@
import Component from '@ember/component';
import { action, computed } from '@ember/object';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
import { camelize } from '@ember/string';
@classic
@classNames('boxed-section')
export default class Summary extends Component {
Expand All @@ -12,21 +11,6 @@ export default class Summary extends Component {
job = null;
forceCollapsed = false;

@action
gotoAllocations(status) {
this.router.transitionTo('jobs.job.allocations', this.job, {
queryParams: {
status: JSON.stringify(status),
namespace: this.job.get('namespace.name'),
},
});
}

@action
onSliceClick(ev, slice) {
this.gotoAllocations([camelize(slice.label)]);
}

@computed('forceCollapsed')
get isExpanded() {
if (this.forceCollapsed) return false;
Expand Down
17 changes: 17 additions & 0 deletions ui/app/components/job-status/allocation-status-block.hbs
@@ -0,0 +1,17 @@
<div
class="allocation-status-block {{unless this.countToShow "rest-only"}}"
style={{html-safe (concat "width: " @width "%")}}
>
{{#if this.countToShow}}
<div class="ungrouped-allocs">
{{#each (range 0 this.countToShow)}}
<span class="represented-allocation {{@status}}" />
{{/each}}
</div>
{{/if}}
{{#if this.remaining}}
<span class="represented-allocation rest {{@status}}">
{{#if this.countToShow}}+{{/if}}{{this.remaining}}
</span>
{{/if}}
</div>
15 changes: 15 additions & 0 deletions ui/app/components/job-status/allocation-status-block.js
@@ -0,0 +1,15 @@
import Component from '@glimmer/component';

export default class JobStatusAllocationStatusBlockComponent extends Component {
get countToShow() {
const restWidth = 50;
const restGap = 10;
let cts = Math.floor((this.args.width - (restWidth + restGap)) / 42);
// Either show 3+ or show only a single/remaining box
return cts > 3 ? cts : 0;
}

get remaining() {
return this.args.count - this.countToShow;
}
}
22 changes: 22 additions & 0 deletions ui/app/components/job-status/allocation-status-row.hbs
@@ -0,0 +1,22 @@
{{#if this.showSummaries}}
<div class="alloc-status-summaries"
{{did-insert this.captureElement}}
{{window-resize this.reflow}}
>
{{#each-in @allocBlocks as |status allocs|}}
{{#if (gt allocs.length 0)}}
<JobStatus::AllocationStatusBlock @status={{status}} @count={{allocs.length}} @width={{compute (action this.calcPerc) allocs.length}} />
{{/if}}
{{/each-in}}
</div>
{{else}}
<div class="ungrouped-allocs">
{{#each-in @allocBlocks as |status allocs|}}
{{#if (gt allocs.length 0)}}
{{#each (range 0 allocs.length)}}
<span class="represented-allocation {{status}}"></span>
{{/each}}
{{/if}}
{{/each-in}}
</div>
{{/if}}
33 changes: 33 additions & 0 deletions ui/app/components/job-status/allocation-status-row.js
@@ -0,0 +1,33 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

const UNGROUPED_ALLOCS_THRESHOLD = 50;

export default class JobStatusAllocationStatusRowComponent extends Component {
@tracked width = 0;

get allocBlockSlots() {
return Object.values(this.args.allocBlocks).reduce(
(m, n) => m + n.length,
0
);
}

get showSummaries() {
return this.allocBlockSlots > UNGROUPED_ALLOCS_THRESHOLD;
}

calcPerc(count) {
return (count / this.allocBlockSlots) * this.width;
}

@action reflow(element) {
this.width = element.clientWidth;
}

@action
captureElement(element) {
this.width = element.clientWidth;
}
}
53 changes: 53 additions & 0 deletions ui/app/components/job-status/panel.hbs
@@ -0,0 +1,53 @@
<div class="job-status-panel boxed-section" data-test-job-status-panel data-test-status-mode={{this.mode}}>
<div class="boxed-section-head">
<h2>Status</h2>
<div class="select-mode">
<button type="button"
data-test-status-mode-current
class="button is-small is-borderless {{if (eq this.mode "current") "is-active"}}"
{{on "click" (action (mut this.mode) "current")}}
>
Current
</button>
<button type="button"
data-test-status-mode-historical
class="button is-small is-borderless {{if (eq this.mode "historical") "is-active"}}"
{{on "click" (action (mut this.mode) "historical")}}>
Historical
</button>
</div>
</div>
<div class="boxed-section-body">
{{#if (eq this.mode "current")}}
<h3 class="title is-4 running-allocs-title"><strong>{{@job.runningAllocs}}/{{this.totalAllocs}}</strong> Allocations Running</h3>

<JobStatus::AllocationStatusRow @allocBlocks={{this.allocBlocks}} />

<footer>
<legend>
{{#each this.allocTypes as |type|}}
<span class="legend-item {{if (eq (get (get this.allocBlocks type.label) "length") 0) "faded"}}">
<span class="represented-allocation {{type.label}}"></span>
{{get (get this.allocBlocks type.label) "length"}} {{capitalize type.label}}
</span>
{{/each}}
</legend>

<section class="versions">
<h4>Versions</h4>
<ul>
{{#each-in this.versions as |version allocs|}}
<li>
<Hds::Badge @text={{version}} />
<Hds::BadgeCount @text={{allocs.length}} @type="inverted" />
</li>
{{/each-in}}
</ul>
</section>
</footer>
{{/if}}
{{#if (eq this.mode "historical")}}
<JobPage::Parts::SummaryChart @job={{@job}} />
{{/if}}
</div>
</div>
81 changes: 81 additions & 0 deletions ui/app/components/job-status/panel.js
@@ -0,0 +1,81 @@
// @ts-check
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class JobStatusPanelComponent extends Component {
// Build note: allocTypes order matters! We will fill up to 100% of totalAllocs in this order.
allocTypes = [
'running',
'pending',
'failed',
// 'unknown',
// 'lost',
// 'queued',
// 'complete',
'unplaced',
].map((type) => {
return {
label: type,
property: `${type}Allocs`,
};
});

get allocBlocks() {
let availableSlotsToFill = this.totalAllocs;
// Only fill up to 100% of totalAllocs. Once we've filled up, we can stop counting.
let allocationsOfShowableType = this.allocTypes.reduce((blocks, type) => {
const jobAllocsOfType = this.args.job.allocations.filterBy(
'clientStatus',
type.label
);
if (availableSlotsToFill > 0) {
blocks[type.label] = Array(
Math.min(availableSlotsToFill, jobAllocsOfType.length)
)
.fill()
.map((_, i) => {
return jobAllocsOfType[i];
});
availableSlotsToFill -= blocks[type.label].length;
} else {
blocks[type.label] = [];
}
return blocks;
}, {});
if (availableSlotsToFill > 0) {
allocationsOfShowableType['unplaced'] = Array(availableSlotsToFill)
.fill()
.map(() => {
return { clientStatus: 'unplaced' };
});
}
return allocationsOfShowableType;
philrenaud marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @type {('current'|'historical')}
*/
@tracked mode = 'current'; // can be either "current" or "historical"

// TODO: eventually we will want this from a new property on a job.
get totalAllocs() {
// v----- Experimental method: Count all allocs. Good for testing but not a realistic representation of "Desired"
// return this.allocTypes.reduce((sum, type) => sum + this.args.job[type.property], 0);

// v----- Realistic method: Tally a job's task groups' "count" property
return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0);
}
Comment on lines +60 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately leaving scratchnotes here for API-layer discussion later


get versions() {
return Object.values(this.allocBlocks)
.flat()
.map((a) => (!isNaN(a?.jobVersion) ? `v${a.jobVersion}` : 'pending')) // "starting" allocs, and possibly others, do not yet have a jobVersion
.reduce(
(result, item) => ({
...result,
[item]: [...(result[item] || []), item],
}),
[]
);
}
}
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Expand Up @@ -53,3 +53,4 @@
@import './components/authorization';
@import './components/policies';
@import './components/metadata-editor';
@import './components/job-status-panel';