diff --git a/.changelog/14592.txt b/.changelog/14592.txt new file mode 100644 index 000000000000..5a0f2d7f5e64 --- /dev/null +++ b/.changelog/14592.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: allow deep-dive clicks to tasks from client, job, and task group routes. +``` diff --git a/ui/app/components/job-page/parts/recent-allocations.js b/ui/app/components/job-page/parts/recent-allocations.js index 3afd9f37d7bb..5f40c434a932 100644 --- a/ui/app/components/job-page/parts/recent-allocations.js +++ b/ui/app/components/job-page/parts/recent-allocations.js @@ -4,6 +4,7 @@ import { inject as service } from '@ember/service'; import PromiseArray from 'nomad-ui/utils/classes/promise-array'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; +import localStorageProperty from 'nomad-ui/utils/properties/local-storage'; @classic @classNames('boxed-section') @@ -13,6 +14,14 @@ export default class RecentAllocations extends Component { sortProperty = 'modifyIndex'; sortDescending = true; + @localStorageProperty('nomadShowSubTasks', true) showSubTasks; + + @action + toggleShowSubTasks(e) { + e.preventDefault(); + this.set('showSubTasks', !this.get('showSubTasks')); + } + @computed('job.allocations.@each.modifyIndex') get sortedAllocations() { return PromiseArray.create({ diff --git a/ui/app/components/task-sub-row.hbs b/ui/app/components/task-sub-row.hbs new file mode 100644 index 000000000000..203fd6555e57 --- /dev/null +++ b/ui/app/components/task-sub-row.hbs @@ -0,0 +1,79 @@ + + + / + + {{this.task.name}} + + {{!-- TODO: in-page logs --}} + {{!-- --}} + + + {{#if this.task.isRunning}} + {{#if (and (not this.cpu) this.fetchStats.isRunning)}} + ... + {{else if this.statsError}} + + {{x-icon "alert-triangle" class="is-warning"}} + + {{else}} + + {{/if}} + {{/if}} + + + {{#if this.task.isRunning}} + {{#if (and (not this.memory) this.fetchStats.isRunning)}} + ... + {{else if this.statsError}} + + {{x-icon "alert-triangle" class="is-warning"}} + + {{else}} + + {{/if}} + {{/if}} + + + +{{yield}} \ No newline at end of file diff --git a/ui/app/components/task-sub-row.js b/ui/app/components/task-sub-row.js new file mode 100644 index 000000000000..1499c2edf126 --- /dev/null +++ b/ui/app/components/task-sub-row.js @@ -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; +} diff --git a/ui/app/controllers/clients/client/index.js b/ui/app/controllers/clients/client/index.js index 05d9ce5c9662..1775a07270db 100644 --- a/ui/app/controllers/clients/client/index.js +++ b/ui/app/controllers/clients/client/index.js @@ -15,6 +15,7 @@ import { deserializedQueryParam as selection, } from 'nomad-ui/utils/qp-serialize'; import classic from 'ember-classic-decorator'; +import localStorageProperty from 'nomad-ui/utils/properties/local-storage'; @classic export default class ClientController extends Controller.extend( @@ -60,6 +61,14 @@ export default class ClientController extends Controller.extend( sortProperty = 'modifyIndex'; sortDescending = true; + @localStorageProperty('nomadShowSubTasks', false) showSubTasks; + + @action + toggleShowSubTasks(e) { + e.preventDefault(); + this.set('showSubTasks', !this.get('showSubTasks')); + } + @computed() get searchProps() { return ['shortId', 'name']; diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index fccb0f40cf88..763fa5b3d8d9 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -13,6 +13,7 @@ import { deserializedQueryParam as selection, } from 'nomad-ui/utils/qp-serialize'; import classic from 'ember-classic-decorator'; +import localStorageProperty from 'nomad-ui/utils/properties/local-storage'; @classic export default class TaskGroupController extends Controller.extend( @@ -57,6 +58,14 @@ export default class TaskGroupController extends Controller.extend( return ['shortId', 'name']; } + @localStorageProperty('nomadShowSubTasks', true) showSubTasks; + + @action + toggleShowSubTasks(e) { + e.preventDefault(); + this.set('showSubTasks', !this.get('showSubTasks')); + } + @computed('model.allocations.[]') get allocations() { return this.get('model.allocations') || []; diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index ef5f3fbc1257..960c9c6d1e96 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -49,3 +49,4 @@ @import './components/variables'; @import './components/keyboard-shortcuts-modal'; @import './components/services'; +@import './components/task-sub-row'; diff --git a/ui/app/styles/components/task-sub-row.scss b/ui/app/styles/components/task-sub-row.scss new file mode 100644 index 000000000000..e8334b03d579 --- /dev/null +++ b/ui/app/styles/components/task-sub-row.scss @@ -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; + } + } +} diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 17ea8b56afaa..2e2f1d673a57 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -41,6 +41,10 @@ } } + &.with-collapsed-borders { + border-collapse: collapse; + } + &.is-darkened { tbody tr:not(.is-selected) { background-color: $white-bis; diff --git a/ui/app/templates/clients/client/index.hbs b/ui/app/templates/clients/client/index.hbs index 20a8f3506b53..22056e0b2818 100644 --- a/ui/app/templates/clients/client/index.hbs +++ b/ui/app/templates/clients/client/index.hbs @@ -496,6 +496,17 @@ @inputClass="is-compact" @class="is-padded" /> + + + + Show Tasks + +
@@ -555,6 +566,11 @@ @onClick={{action "gotoAllocation" row.model}} @data-test-allocation={{row.model.id}} /> + {{#if this.showSubTasks}} + {{#each row.model.states as |task|}} + + {{/each}} + {{/if}}
diff --git a/ui/app/templates/components/job-page/parts/recent-allocations.hbs b/ui/app/templates/components/job-page/parts/recent-allocations.hbs index c08679f816c5..e91aca79f263 100644 --- a/ui/app/templates/components/job-page/parts/recent-allocations.hbs +++ b/ui/app/templates/components/job-page/parts/recent-allocations.hbs @@ -1,6 +1,14 @@
Recent Allocations + + Show Tasks +
@@ -52,11 +60,18 @@ @allocation={{row.model}} @context="job" @onClick={{action "gotoAllocation" row.model}} + @showSubTasks={{this.showSubTasks}} {{keyboard-shortcut enumerated=true action=(action "gotoAllocation" row.model) }} /> + + {{#if this.showSubTasks}} + {{#each row.model.states as |task|}} + + {{/each}} + {{/if}} {{else}} diff --git a/ui/app/templates/jobs/job/allocations.hbs b/ui/app/templates/jobs/job/allocations.hbs index d7d63e9b7d50..d655a9e8f932 100644 --- a/ui/app/templates/jobs/job/allocations.hbs +++ b/ui/app/templates/jobs/job/allocations.hbs @@ -46,7 +46,7 @@ @source={{p.list}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} - @class="with-foot" as |t|> + @class="with-foot with-collapsed-borders" as |t|> ID @@ -70,6 +70,10 @@ @allocation={{row.model}} @context="job" @onClick={{action "gotoAllocation" row.model}} /> + {{#each row.model.states as |task|}} + + {{/each}} +
diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index 9495cb3849e1..ef5fedd12d81 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -149,6 +149,16 @@ @class="is-padded" @inputClass="is-compact" /> + + + Show Tasks + +
@@ -163,7 +173,7 @@ @source={{p.list}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} - @class="with-foot" as |t| + @class="with-foot {{if this.showSubTasks "with-collapsed-borders"}}" as |t| > @@ -206,6 +216,11 @@ @context="taskGroup" @onClick={{action "gotoAllocation" row.model}} /> + {{#if this.showSubTasks}} + {{#each row.model.states as |task|}} + + {{/each}} + {{/if}}
diff --git a/ui/package.json b/ui/package.json index 0bd7c43fa3d3..6a67d94cc421 100644 --- a/ui/package.json +++ b/ui/package.json @@ -133,8 +133,8 @@ "jsonlint": "^1.6.3", "lint-staged": "^11.2.6", "loader.js": "^4.7.0", - "lodash.isequal": "^4.5.0", "lodash.intersection": "^4.4.0", + "lodash.isequal": "^4.5.0", "morgan": "^1.3.2", "npm-run-all": "^4.1.5", "pretender": "^3.0.1", @@ -174,7 +174,7 @@ ] }, "dependencies": { - "@hashicorp/ember-flight-icons": "^2.0.5", + "@hashicorp/ember-flight-icons": "^2.0.12", "@percy/cli": "^1.6.1", "@percy/ember": "^3.0.0", "curved-arrows": "^0.1.0", diff --git a/ui/tests/integration/components/task-sub-row-test.js b/ui/tests/integration/components/task-sub-row-test.js new file mode 100644 index 000000000000..05b38eec3974 --- /dev/null +++ b/ui/tests/integration/components/task-sub-row-test.js @@ -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``); + assert.dom(this.element).hasText(`/ ${mockTask.name}`); + await componentA11yAudit(this.element, assert); + }); +}); diff --git a/ui/yarn.lock b/ui/yarn.lock index ddb6aaf61185..b26fe7def226 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3095,19 +3095,19 @@ resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf" integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A== -"@hashicorp/ember-flight-icons@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-2.0.5.tgz#a1edfdd24475ecd0cf07cd1f944e2e5bdb5e97cc" - integrity sha512-PXNk1aRBjYSGeoB4e2ovOBm6RhGKE554XjW8leYYK+y9yorHhJNNwWRkwjhDRLYWikLhNmfwp6nAYOJWl/IOgw== +"@hashicorp/ember-flight-icons@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-2.0.12.tgz#788adf7a4fedc468d612d35b604255df948f4012" + integrity sha512-8fHPGaSpMkr5dLWaruwbq9INwZCi2EyTof/TR/dL8PN4UbCuY+KXNqG0lLIKNGFFTj09B1cO303m5GUfKKDGKQ== dependencies: - "@hashicorp/flight-icons" "^2.3.1" + "@hashicorp/flight-icons" "^2.10.0" ember-cli-babel "^7.26.11" ember-cli-htmlbars "^6.0.1" -"@hashicorp/flight-icons@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.3.1.tgz#0b0dc259c0a4255c5613174db7192ab48523ca6f" - integrity sha512-WGCMMixkmYCP5Dyz4QW7XjW4zDhIc7njkVVucoj7Iv7abtfgQDWwm05Ja2aBJTxFHiP4jat9w9cbGNgC6QHmZQ== +"@hashicorp/flight-icons@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.10.0.tgz#24b03043bacda16e505200e6591dfef896ddacf1" + integrity sha512-jYUA0M6Tz+4RAudil+GW/fHbhZPcKCiIZZAguBDviqbLneMkMgPOBgbXWCGWsEQ1fJzP2cXbUaio8L0aQZPWQw== "@hashicorp/structure-icons@^1.3.0": version "1.9.2"