Added task links to various alloc tables (#14592)

* Added task links to various alloc tables

* Lintfix

* Border collapse and added to task group page

* Logs icon temporarily removed and localStorage added

* Mock task added to test

* Delog

* Two asserts in new test

* Remove commented-out code

* Changelog

* Removing args.allocation deps
This commit is contained in:
Phil Renaud 2022-09-16 15:58:22 -04:00 committed by GitHub
parent bf0e83e3a0
commit d6c9676252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 329 additions and 15 deletions

3
.changelog/14592.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: allow deep-dive clicks to tasks from client, job, and task group routes.
```

View File

@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
import PromiseArray from 'nomad-ui/utils/classes/promise-array'; import PromiseArray from 'nomad-ui/utils/classes/promise-array';
import { classNames } from '@ember-decorators/component'; import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator'; import classic from 'ember-classic-decorator';
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
@classic @classic
@classNames('boxed-section') @classNames('boxed-section')
@ -13,6 +14,14 @@ export default class RecentAllocations extends Component {
sortProperty = 'modifyIndex'; sortProperty = 'modifyIndex';
sortDescending = true; sortDescending = true;
@localStorageProperty('nomadShowSubTasks', true) showSubTasks;
@action
toggleShowSubTasks(e) {
e.preventDefault();
this.set('showSubTasks', !this.get('showSubTasks'));
}
@computed('job.allocations.@each.modifyIndex') @computed('job.allocations.@each.modifyIndex')
get sortedAllocations() { get sortedAllocations() {
return PromiseArray.create({ return PromiseArray.create({

View File

@ -0,0 +1,79 @@
<tr class="task-sub-row"
{{keyboard-shortcut
enumerated=true
action=(action "gotoTask" this.task.allocation this.task)
}}
>
<td colspan={{@namespan}}>
/
<LinkTo @route="allocations.allocation.task" @models={{array this.task.allocation this.task}}>
{{this.task.name}}
</LinkTo>
{{!-- TODO: in-page logs --}}
{{!-- <FlightIcon @name="logs" /> --}}
</td>
<td data-test-cpu class="is-1 has-text-centered">
{{#if this.task.isRunning}}
{{#if (and (not this.cpu) this.fetchStats.isRunning)}}
...
{{else if this.statsError}}
<span
class="tooltip text-center"
role="tooltip"
aria-label="Couldn't collect stats"
>
{{x-icon "alert-triangle" class="is-warning"}}
</span>
{{else}}
<div
class="inline-chart is-small tooltip"
role="tooltip"
aria-label="{{format-hertz this.cpu.used}}
/
{{format-hertz this.taskStats.reservedCPU}}"
>
<progress
class="progress is-info is-small"
value="{{this.cpu.percent}}"
max="1"
>
{{this.cpu.percent}}
</progress>
</div>
{{/if}}
{{/if}}
</td>
<td data-test-mem class="is-1 has-text-centered">
{{#if this.task.isRunning}}
{{#if (and (not this.memory) this.fetchStats.isRunning)}}
...
{{else if this.statsError}}
<span
class="tooltip is-small text-center"
role="tooltip"
aria-label="Couldn't collect stats"
>
{{x-icon "alert-triangle" class="is-warning"}}
</span>
{{else}}
<div
class="inline-chart tooltip"
role="tooltip"
aria-label="{{format-bytes this.memory.used}}
/
{{format-bytes this.taskStats.reservedMemory start="MiB"}}"
>
<progress
class="progress is-danger is-small"
value="{{this.memory.percent}}"
max="1"
>
{{this.memory.percent}}
</progress>
</div>
{{/if}}
{{/if}}
</td>
</tr>
{{yield}}

View File

@ -0,0 +1,74 @@
import Ember from 'ember';
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task, timeout } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
export default class TaskSubRowComponent extends Component {
@service store;
@service router;
@service('stats-trackers-registry') statsTrackersRegistry;
constructor() {
super(...arguments);
// Kick off stats polling
const allocation = this.task.allocation;
if (allocation) {
this.fetchStats.perform();
} else {
this.fetchStats.cancelAll();
}
}
@alias('args.taskState') task;
@action
gotoTask(allocation, task) {
this.router.transitionTo('allocations.allocation.task', allocation, task);
}
// Since all tasks for an allocation share the same tracker, use the registry
@computed('task.{allocation,isRunning}')
get stats() {
if (!this.task.isRunning) return undefined;
return this.statsTrackersRegistry.getTracker(this.task.allocation);
}
// Internal state
@tracked statsError = false;
@computed
get enablePolling() {
return !Ember.testing;
}
@computed('task.name', 'stats.tasks.[]')
get taskStats() {
if (!this.stats) return undefined;
return this.stats.tasks.findBy('task', this.task.name);
}
@alias('taskStats.cpu.lastObject') cpu;
@alias('taskStats.memory.lastObject') memory;
@(task(function* () {
do {
if (this.stats) {
try {
yield this.stats.poll.linked().perform();
this.statsError = false;
} catch (error) {
this.statsError = true;
}
}
yield timeout(500);
} while (this.enablePolling);
}).drop())
fetchStats;
}

View File

@ -15,6 +15,7 @@ import {
deserializedQueryParam as selection, deserializedQueryParam as selection,
} from 'nomad-ui/utils/qp-serialize'; } from 'nomad-ui/utils/qp-serialize';
import classic from 'ember-classic-decorator'; import classic from 'ember-classic-decorator';
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
@classic @classic
export default class ClientController extends Controller.extend( export default class ClientController extends Controller.extend(
@ -60,6 +61,14 @@ export default class ClientController extends Controller.extend(
sortProperty = 'modifyIndex'; sortProperty = 'modifyIndex';
sortDescending = true; sortDescending = true;
@localStorageProperty('nomadShowSubTasks', false) showSubTasks;
@action
toggleShowSubTasks(e) {
e.preventDefault();
this.set('showSubTasks', !this.get('showSubTasks'));
}
@computed() @computed()
get searchProps() { get searchProps() {
return ['shortId', 'name']; return ['shortId', 'name'];

View File

@ -13,6 +13,7 @@ import {
deserializedQueryParam as selection, deserializedQueryParam as selection,
} from 'nomad-ui/utils/qp-serialize'; } from 'nomad-ui/utils/qp-serialize';
import classic from 'ember-classic-decorator'; import classic from 'ember-classic-decorator';
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
@classic @classic
export default class TaskGroupController extends Controller.extend( export default class TaskGroupController extends Controller.extend(
@ -57,6 +58,14 @@ export default class TaskGroupController extends Controller.extend(
return ['shortId', 'name']; return ['shortId', 'name'];
} }
@localStorageProperty('nomadShowSubTasks', true) showSubTasks;
@action
toggleShowSubTasks(e) {
e.preventDefault();
this.set('showSubTasks', !this.get('showSubTasks'));
}
@computed('model.allocations.[]') @computed('model.allocations.[]')
get allocations() { get allocations() {
return this.get('model.allocations') || []; return this.get('model.allocations') || [];

View File

@ -49,3 +49,4 @@
@import './components/variables'; @import './components/variables';
@import './components/keyboard-shortcuts-modal'; @import './components/keyboard-shortcuts-modal';
@import './components/services'; @import './components/services';
@import './components/task-sub-row';

View File

@ -0,0 +1,16 @@
table tbody .task-sub-row {
td {
border-top: 2px solid white;
}
td:nth-child(1) {
padding-left: 4rem;
a {
margin-right: 1rem;
}
svg.flight-icon {
position: relative;
top: 3px;
}
}
}

View File

@ -41,6 +41,10 @@
} }
} }
&.with-collapsed-borders {
border-collapse: collapse;
}
&.is-darkened { &.is-darkened {
tbody tr:not(.is-selected) { tbody tr:not(.is-selected) {
background-color: $white-bis; background-color: $white-bis;

View File

@ -496,6 +496,17 @@
@inputClass="is-compact" @inputClass="is-compact"
@class="is-padded" @class="is-padded"
/> />
<span class="is-padded">
<Toggle
class="button is-borderless is-inline"
@isActive={{this.showSubTasks}}
@onToggle={{this.toggleShowSubTasks}}
title="Show tasks of allocations"
>
Show Tasks
</Toggle>
</span>
</div> </div>
</div> </div>
<div <div
@ -512,7 +523,7 @@
@source={{p.list}} @source={{p.list}}
@sortProperty={{this.sortProperty}} @sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}} @sortDescending={{this.sortDescending}}
@class="with-foot" as |t| @class="with-foot {{if this.showSubTasks "with-collapsed-borders"}}" as |t|
> >
<t.head> <t.head>
<th class="is-narrow"></th> <th class="is-narrow"></th>
@ -555,6 +566,11 @@
@onClick={{action "gotoAllocation" row.model}} @onClick={{action "gotoAllocation" row.model}}
@data-test-allocation={{row.model.id}} @data-test-allocation={{row.model.id}}
/> />
{{#if this.showSubTasks}}
{{#each row.model.states as |task|}}
<TaskSubRow @namespan="8" @taskState={{task}} />
{{/each}}
{{/if}}
</t.body> </t.body>
</ListTable> </ListTable>
<div class="table-foot"> <div class="table-foot">

View File

@ -1,6 +1,14 @@
<div class="boxed-section"> <div class="boxed-section">
<div class="boxed-section-head"> <div class="boxed-section-head">
Recent Allocations Recent Allocations
<span class="pull-right is-padded">
<Toggle
class="button is-borderless is-inline"
@isActive={{this.showSubTasks}}
@onToggle={{this.toggleShowSubTasks}}
title="Show tasks of allocations"
>Show Tasks</Toggle>
</span>
</div> </div>
<div <div
class="boxed-section-body class="boxed-section-body
@ -11,7 +19,7 @@
@source={{this.sortedAllocations}} @source={{this.sortedAllocations}}
@sortProperty={{this.sortProperty}} @sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}} @sortDescending={{this.sortDescending}}
@class="with-foot" as |t| @class="with-foot {{if this.showSubTasks "with-collapsed-borders"}}" as |t|
> >
<t.head> <t.head>
<th class="is-narrow"></th> <th class="is-narrow"></th>
@ -52,11 +60,18 @@
@allocation={{row.model}} @allocation={{row.model}}
@context="job" @context="job"
@onClick={{action "gotoAllocation" row.model}} @onClick={{action "gotoAllocation" row.model}}
@showSubTasks={{this.showSubTasks}}
{{keyboard-shortcut {{keyboard-shortcut
enumerated=true enumerated=true
action=(action "gotoAllocation" row.model) action=(action "gotoAllocation" row.model)
}} }}
/> />
{{#if this.showSubTasks}}
{{#each row.model.states as |task|}}
<TaskSubRow @namespan="9" @taskState={{task}} />
{{/each}}
{{/if}}
</t.body> </t.body>
</ListTable> </ListTable>
{{else}} {{else}}

View File

@ -46,7 +46,7 @@
@source={{p.list}} @source={{p.list}}
@sortProperty={{this.sortProperty}} @sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}} @sortDescending={{this.sortDescending}}
@class="with-foot" as |t|> @class="with-foot with-collapsed-borders" as |t|>
<t.head> <t.head>
<th class="is-narrow"></th> <th class="is-narrow"></th>
<t.sort-by @prop="shortId">ID</t.sort-by> <t.sort-by @prop="shortId">ID</t.sort-by>
@ -70,6 +70,10 @@
@allocation={{row.model}} @allocation={{row.model}}
@context="job" @context="job"
@onClick={{action "gotoAllocation" row.model}} /> @onClick={{action "gotoAllocation" row.model}} />
{{#each row.model.states as |task|}}
<TaskSubRow @namespan="9" @taskState={{task}} />
{{/each}}
</t.body> </t.body>
</ListTable> </ListTable>
<div class="table-foot"> <div class="table-foot">

View File

@ -149,6 +149,16 @@
@class="is-padded" @class="is-padded"
@inputClass="is-compact" @inputClass="is-compact"
/> />
<span class="is-padded">
<Toggle
class="button is-borderless is-inline"
@isActive={{this.showSubTasks}}
@onToggle={{this.toggleShowSubTasks}}
title="Show tasks of allocations"
>
Show Tasks
</Toggle>
</span>
</div> </div>
</div> </div>
<div class="boxed-section-body is-full-bleed"> <div class="boxed-section-body is-full-bleed">
@ -163,7 +173,7 @@
@source={{p.list}} @source={{p.list}}
@sortProperty={{this.sortProperty}} @sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}} @sortDescending={{this.sortDescending}}
@class="with-foot" as |t| @class="with-foot {{if this.showSubTasks "with-collapsed-borders"}}" as |t|
> >
<t.head> <t.head>
<th class="is-narrow"></th> <th class="is-narrow"></th>
@ -206,6 +216,11 @@
@context="taskGroup" @context="taskGroup"
@onClick={{action "gotoAllocation" row.model}} @onClick={{action "gotoAllocation" row.model}}
/> />
{{#if this.showSubTasks}}
{{#each row.model.states as |task|}}
<TaskSubRow @namespan="8" @taskState={{task}} />
{{/each}}
{{/if}}
</t.body> </t.body>
</ListTable> </ListTable>
<div class="table-foot"> <div class="table-foot">

View File

@ -133,8 +133,8 @@
"jsonlint": "^1.6.3", "jsonlint": "^1.6.3",
"lint-staged": "^11.2.6", "lint-staged": "^11.2.6",
"loader.js": "^4.7.0", "loader.js": "^4.7.0",
"lodash.isequal": "^4.5.0",
"lodash.intersection": "^4.4.0", "lodash.intersection": "^4.4.0",
"lodash.isequal": "^4.5.0",
"morgan": "^1.3.2", "morgan": "^1.3.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pretender": "^3.0.1", "pretender": "^3.0.1",
@ -174,7 +174,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hashicorp/ember-flight-icons": "^2.0.5", "@hashicorp/ember-flight-icons": "^2.0.12",
"@percy/cli": "^1.6.1", "@percy/cli": "^1.6.1",
"@percy/ember": "^3.0.0", "@percy/ember": "^3.0.0",
"curved-arrows": "^0.1.0", "curved-arrows": "^0.1.0",

View File

@ -0,0 +1,60 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
const mockTask = {
name: 'another-server',
state: 'running',
startedAt: '2022-09-14T17:19:12.351Z',
finishedAt: null,
failed: false,
resources: null,
events: [
{
Type: 'Received',
Signal: 0,
ExitCode: 0,
Time: '2022-09-14T17:19:11.919Z',
TimeNanos: 156992,
DisplayMessage: 'Task received by client',
},
{
Type: 'Task Setup',
Signal: 0,
ExitCode: 0,
Time: '2022-09-14T17:19:11.920Z',
TimeNanos: 793088,
DisplayMessage: 'Building Task Directory',
},
{
Type: 'Started',
Signal: 0,
ExitCode: 0,
Time: '2022-09-14T17:19:12.351Z',
TimeNanos: 258112,
DisplayMessage: 'Task started by client',
},
{
Type: 'Alloc Unhealthy',
Signal: 0,
ExitCode: 0,
Time: '2022-09-14T17:24:11.919Z',
TimeNanos: 589120,
DisplayMessage:
'Task not running for min_healthy_time of 10s by healthy_deadline of 5m0s',
},
],
};
module('Integration | Component | task-sub-row', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
assert.expect(2);
this.set('task', mockTask);
await render(hbs`<TaskSubRow @taskState={{this.task}} />`);
assert.dom(this.element).hasText(`/ ${mockTask.name}`);
await componentA11yAudit(this.element, assert);
});
});

View File

@ -3095,19 +3095,19 @@
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf" resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf"
integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A== integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A==
"@hashicorp/ember-flight-icons@^2.0.5": "@hashicorp/ember-flight-icons@^2.0.12":
version "2.0.5" version "2.0.12"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-2.0.5.tgz#a1edfdd24475ecd0cf07cd1f944e2e5bdb5e97cc" resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-2.0.12.tgz#788adf7a4fedc468d612d35b604255df948f4012"
integrity sha512-PXNk1aRBjYSGeoB4e2ovOBm6RhGKE554XjW8leYYK+y9yorHhJNNwWRkwjhDRLYWikLhNmfwp6nAYOJWl/IOgw== integrity sha512-8fHPGaSpMkr5dLWaruwbq9INwZCi2EyTof/TR/dL8PN4UbCuY+KXNqG0lLIKNGFFTj09B1cO303m5GUfKKDGKQ==
dependencies: dependencies:
"@hashicorp/flight-icons" "^2.3.1" "@hashicorp/flight-icons" "^2.10.0"
ember-cli-babel "^7.26.11" ember-cli-babel "^7.26.11"
ember-cli-htmlbars "^6.0.1" ember-cli-htmlbars "^6.0.1"
"@hashicorp/flight-icons@^2.3.1": "@hashicorp/flight-icons@^2.10.0":
version "2.3.1" version "2.10.0"
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.3.1.tgz#0b0dc259c0a4255c5613174db7192ab48523ca6f" resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.10.0.tgz#24b03043bacda16e505200e6591dfef896ddacf1"
integrity sha512-WGCMMixkmYCP5Dyz4QW7XjW4zDhIc7njkVVucoj7Iv7abtfgQDWwm05Ja2aBJTxFHiP4jat9w9cbGNgC6QHmZQ== integrity sha512-jYUA0M6Tz+4RAudil+GW/fHbhZPcKCiIZZAguBDviqbLneMkMgPOBgbXWCGWsEQ1fJzP2cXbUaio8L0aQZPWQw==
"@hashicorp/structure-icons@^1.3.0": "@hashicorp/structure-icons@^1.3.0":
version "1.9.2" version "1.9.2"