From cb044353f1041a1be7305dae30cd8f08d4491d4b Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 15 Mar 2026 03:48:32 -0700 Subject: [PATCH 01/86] feat: ember 6.4 --- ui/.stylelintrc.js | 5 +++++ ui/app/services/watch-list.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 ui/.stylelintrc.js diff --git a/ui/.stylelintrc.js b/ui/.stylelintrc.js new file mode 100644 index 00000000000..af4e190f773 --- /dev/null +++ b/ui/.stylelintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: ['stylelint-config-standard-scss'], +}; diff --git a/ui/app/services/watch-list.js b/ui/app/services/watch-list.js index 1fe65b53e2f..c95874494d7 100644 --- a/ui/app/services/watch-list.js +++ b/ui/app/services/watch-list.js @@ -7,6 +7,7 @@ import { computed } from '@ember/object'; import { readOnly } from '@ember/object/computed'; import { copy } from 'ember-copy'; import Service from '@ember/service'; +import { AbortController } from 'fetch'; let list = {}; From b582ecad20e13cee5477e539fbd08b76b8712189 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 15 Mar 2026 05:48:42 -0700 Subject: [PATCH 02/86] ui: remove ember-fetch and stabilize async tests --- ui/app/services/watch-list.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/services/watch-list.js b/ui/app/services/watch-list.js index c95874494d7..1fe65b53e2f 100644 --- a/ui/app/services/watch-list.js +++ b/ui/app/services/watch-list.js @@ -7,7 +7,6 @@ import { computed } from '@ember/object'; import { readOnly } from '@ember/object/computed'; import { copy } from 'ember-copy'; import Service from '@ember/service'; -import { AbortController } from 'fetch'; let list = {}; From ec07db8991f5046f3a75fba5a243ec1558fc0cb2 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 15 Mar 2026 06:05:28 -0700 Subject: [PATCH 03/86] ui: add missing copyright headers --- ui/.stylelintrc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/.stylelintrc.js b/ui/.stylelintrc.js index af4e190f773..7cf9caada28 100644 --- a/ui/.stylelintrc.js +++ b/ui/.stylelintrc.js @@ -1,3 +1,8 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + 'use strict'; module.exports = { From 205ad09db3c671d539801bfc25c3b00111bd91b7 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 15 Mar 2026 20:15:10 -0700 Subject: [PATCH 04/86] ui: stabilize Ember Data 4.12 token/job flows --- ui/app/components/job-version.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/components/job-version.hbs b/ui/app/components/job-version.hbs index 1ac989f9aa5..112b53047df 100644 --- a/ui/app/components/job-version.hbs +++ b/ui/app/components/job-version.hbs @@ -3,7 +3,6 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -{{did-update-helper this.versionsDidUpdate this.diff}}
Date: Sun, 15 Mar 2026 20:20:23 -0700 Subject: [PATCH 05/86] ui: restore deep-link redirect and diff sync --- ui/app/components/job-version.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/job-version.hbs b/ui/app/components/job-version.hbs index 112b53047df..1ac989f9aa5 100644 --- a/ui/app/components/job-version.hbs +++ b/ui/app/components/job-version.hbs @@ -3,6 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} +{{did-update-helper this.versionsDidUpdate this.diff}}
Date: Sun, 15 Mar 2026 23:50:52 -0700 Subject: [PATCH 06/86] feat: upgrade ember-cli (no linting yet) --- ui/.stylelintrc.js | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 ui/.stylelintrc.js diff --git a/ui/.stylelintrc.js b/ui/.stylelintrc.js deleted file mode 100644 index 7cf9caada28..00000000000 --- a/ui/.stylelintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2026 - * SPDX-License-Identifier: BUSL-1.1 - */ - -'use strict'; - -module.exports = { - extends: ['stylelint-config-standard-scss'], -}; From 1dd02bb4a950fbb49babaa57056b45997294e7ec Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 15 Mar 2026 23:53:37 -0700 Subject: [PATCH 07/86] =?UTF-8?q?format=20the=20world=20=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/app/styles/components/node-status-light.scss | 2 +- ui/app/styles/components/storage.scss | 2 +- ui/app/styles/components/toolbar.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/styles/components/node-status-light.scss b/ui/app/styles/components/node-status-light.scss index 14937df3ed3..e326f9e43a0 100644 --- a/ui/app/styles/components/node-status-light.scss +++ b/ui/app/styles/components/node-status-light.scss @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -@use "sass:math"; +@use 'sass:math'; $size: 1.6rem; diff --git a/ui/app/styles/components/storage.scss b/ui/app/styles/components/storage.scss index 710fc561916..e8be96b6363 100644 --- a/ui/app/styles/components/storage.scss +++ b/ui/app/styles/components/storage.scss @@ -68,7 +68,7 @@ header { grid-column: -1 / 1; - grid-template-areas: "title"; + grid-template-areas: 'title'; } } } diff --git a/ui/app/styles/components/toolbar.scss b/ui/app/styles/components/toolbar.scss index d85fbbf9cf1..b99f38b587e 100644 --- a/ui/app/styles/components/toolbar.scss +++ b/ui/app/styles/components/toolbar.scss @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -@use "sass:math"; +@use 'sass:math'; $spacing: 1.5em; From 68d86c34e02aa72023777214610182d38c77e9fb Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Mon, 16 Mar 2026 01:38:19 -0700 Subject: [PATCH 08/86] ui: stabilize route-model and test regressions --- ui/app/components/job-status/panel/deploying.hbs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/app/components/job-status/panel/deploying.hbs b/ui/app/components/job-status/panel/deploying.hbs index 121468be8fa..04f9d7d7798 100644 --- a/ui/app/components/job-status/panel/deploying.hbs +++ b/ui/app/components/job-status/panel/deploying.hbs @@ -178,10 +178,12 @@ {{/if}} -

New - allocations: +

+ + New allocations: {{this.newRunningHealthyAllocBlocks.length}}/{{this.totalAllocs}} - running and healthy + running and healthy + Date: Mon, 16 Mar 2026 16:03:31 -0700 Subject: [PATCH 09/86] fix the world --- ui/app/components/job-status/panel/deploying.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/job-status/panel/deploying.hbs b/ui/app/components/job-status/panel/deploying.hbs index 04f9d7d7798..90bcd301f21 100644 --- a/ui/app/components/job-status/panel/deploying.hbs +++ b/ui/app/components/job-status/panel/deploying.hbs @@ -212,7 +212,7 @@ {{#each-in this.newAllocsByStatus as |status count|}} Date: Tue, 17 Feb 2026 05:31:14 -0800 Subject: [PATCH 10/86] ui: migrate (action ...) helper to modern patterns in components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace deprecated `(action ...)` helper with modern Ember patterns: - `(action this.method)` → `this.method` with `@action` decorator - `(action (mut this.prop))` → `(fn (mut this.prop))` - `(action ... value=\"target.value\")` → `(pick \"target.value\" (fn (mut ...)))` - `{{action \"method\"}}` → `{{on \"event\" this.method}}` - `(action (queue ...))` → `(queue ...)` with `(fn (mut ...))` wrappers Key fixes: - Add `@action` to locally-used methods needing `this` binding (capture, resetPagination, calculatePosition, etc.) - Do NOT add `@action` to no-op defaults overridden by parent attrs (onToggle, onDismiss, onHamburgerClick) - creates read-only getter - Use `init()` + `.bind(this)` for image-file.js where method is both locally used and overridable by parent - Wrap bare `(mut ...)` with `(fn ...)` when passed to `pick`/`queue` helpers that call it as a function" --- ui/app/components/image-file.js | 5 +++++ ui/app/templates/administration/tokens/index.hbs | 1 + 2 files changed, 6 insertions(+) diff --git a/ui/app/components/image-file.js b/ui/app/components/image-file.js index edcd19b5790..d59a55a9fa4 100644 --- a/ui/app/components/image-file.js +++ b/ui/app/components/image-file.js @@ -27,6 +27,11 @@ export default class ImageFile extends Component { width = 0; height = 0; + init() { + super.init(...arguments); + this.updateImageMeta = this.updateImageMeta.bind(this); + } + @computed('src') get fileName() { if (!this.src) return undefined; diff --git a/ui/app/templates/administration/tokens/index.hbs b/ui/app/templates/administration/tokens/index.hbs index a703de17a61..d73e7621917 100644 --- a/ui/app/templates/administration/tokens/index.hbs +++ b/ui/app/templates/administration/tokens/index.hbs @@ -45,6 +45,7 @@ (hash key="expirationTime" label="Expires" isSortable=true) (hash key="roles" label="Roles") (hash key="policies" label="Policies") + (hash key="delete" label="Delete") }} @sortBy="name" From eba5baf44be4c674207de6239b36d94df38208ae Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Tue, 17 Feb 2026 05:47:13 -0800 Subject: [PATCH 11/86] ui: refactor image-file to use @action wrapper instead of init+bind --- ui/app/components/image-file.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ui/app/components/image-file.js b/ui/app/components/image-file.js index d59a55a9fa4..edcd19b5790 100644 --- a/ui/app/components/image-file.js +++ b/ui/app/components/image-file.js @@ -27,11 +27,6 @@ export default class ImageFile extends Component { width = 0; height = 0; - init() { - super.init(...arguments); - this.updateImageMeta = this.updateImageMeta.bind(this); - } - @computed('src') get fileName() { if (!this.src) return undefined; From 15bb0bbb9001423d8b350a96f24de6390712d1bb Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Mon, 16 Mar 2026 19:04:36 -0700 Subject: [PATCH 12/86] refactor(ui): finish Ember deprecation and action-helper migration --- ui/app/components/job-status/panel/deploying.hbs | 10 ++++------ ui/app/templates/administration/tokens/index.hbs | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/app/components/job-status/panel/deploying.hbs b/ui/app/components/job-status/panel/deploying.hbs index 90bcd301f21..121468be8fa 100644 --- a/ui/app/components/job-status/panel/deploying.hbs +++ b/ui/app/components/job-status/panel/deploying.hbs @@ -178,12 +178,10 @@ {{/if}} -

- - New allocations: +

New + allocations: {{this.newRunningHealthyAllocBlocks.length}}/{{this.totalAllocs}} - running and healthy - + running and healthy {{#each-in this.newAllocsByStatus as |status count|}} Date: Tue, 17 Mar 2026 04:01:11 -0700 Subject: [PATCH 13/86] ember 6.10 LTS --- ui/app/styles/components/node-status-light.scss | 2 +- ui/app/styles/components/storage.scss | 2 +- ui/app/styles/components/toolbar.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/styles/components/node-status-light.scss b/ui/app/styles/components/node-status-light.scss index e326f9e43a0..14937df3ed3 100644 --- a/ui/app/styles/components/node-status-light.scss +++ b/ui/app/styles/components/node-status-light.scss @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -@use 'sass:math'; +@use "sass:math"; $size: 1.6rem; diff --git a/ui/app/styles/components/storage.scss b/ui/app/styles/components/storage.scss index e8be96b6363..710fc561916 100644 --- a/ui/app/styles/components/storage.scss +++ b/ui/app/styles/components/storage.scss @@ -68,7 +68,7 @@ header { grid-column: -1 / 1; - grid-template-areas: 'title'; + grid-template-areas: "title"; } } } diff --git a/ui/app/styles/components/toolbar.scss b/ui/app/styles/components/toolbar.scss index b99f38b587e..d85fbbf9cf1 100644 --- a/ui/app/styles/components/toolbar.scss +++ b/ui/app/styles/components/toolbar.scss @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -@use 'sass:math'; +@use "sass:math"; $spacing: 1.5em; From dba257c6848f8900cb41d24817305a4b9d3bcd64 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 01:20:35 -0700 Subject: [PATCH 14/86] ui: migrate breadcrumbs chain to gjs components --- ui/app/components/app-breadcrumbs.gjs | 38 ++++++ ui/app/components/app-breadcrumbs.hbs | 22 ---- ui/app/components/app-breadcrumbs.js | 14 -- ui/app/components/breadcrumbs/default.gjs | 94 ++++++++++++++ ui/app/components/breadcrumbs/default.hbs | 35 ----- ui/app/components/breadcrumbs/default.js | 38 ------ ui/app/components/breadcrumbs/job.gjs | 121 ++++++++++++++++++ ui/app/components/breadcrumbs/job.hbs | 61 --------- ui/app/components/breadcrumbs/job.js | 35 ----- .../components/app-breadcrumbs-test.gjs | 111 ++++++++++++++++ .../components/app-breadcrumbs-test.js | 65 ---------- 11 files changed, 364 insertions(+), 270 deletions(-) create mode 100644 ui/app/components/app-breadcrumbs.gjs delete mode 100644 ui/app/components/app-breadcrumbs.hbs delete mode 100644 ui/app/components/app-breadcrumbs.js create mode 100644 ui/app/components/breadcrumbs/default.gjs delete mode 100644 ui/app/components/breadcrumbs/default.hbs delete mode 100644 ui/app/components/breadcrumbs/default.js create mode 100644 ui/app/components/breadcrumbs/job.gjs delete mode 100644 ui/app/components/breadcrumbs/job.hbs delete mode 100644 ui/app/components/breadcrumbs/job.js create mode 100644 ui/tests/integration/components/app-breadcrumbs-test.gjs delete mode 100644 ui/tests/integration/components/app-breadcrumbs-test.js diff --git a/ui/app/components/app-breadcrumbs.gjs b/ui/app/components/app-breadcrumbs.gjs new file mode 100644 index 00000000000..cec3cee3df6 --- /dev/null +++ b/ui/app/components/app-breadcrumbs.gjs @@ -0,0 +1,38 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { fn } from '@ember/helper'; +import Breadcrumbs from 'nomad-ui/components/breadcrumbs'; +import BreadcrumbsDefault from 'nomad-ui/components/breadcrumbs/default'; +import BreadcrumbsJob from 'nomad-ui/components/breadcrumbs/job'; + +const isJobType = (type) => type === 'job'; + +export default class AppBreadcrumbsComponent extends Component { + isOneCrumbUp = (iter = 0, totalNum = 0) => { + return iter === totalNum - 2; + }; + + +} diff --git a/ui/app/components/app-breadcrumbs.hbs b/ui/app/components/app-breadcrumbs.hbs deleted file mode 100644 index 12856464724..00000000000 --- a/ui/app/components/app-breadcrumbs.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - - - {{#each breadcrumbs as |crumb iter|}} - {{#let crumb.args.crumb as |c|}} - {{#if (eq c.type "job")}} - - {{else}} - - {{/if}} - {{/let}} - {{/each}} - \ No newline at end of file diff --git a/ui/app/components/app-breadcrumbs.js b/ui/app/components/app-breadcrumbs.js deleted file mode 100644 index fa00136752b..00000000000 --- a/ui/app/components/app-breadcrumbs.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@glimmer/component'; -import { action } from '@ember/object'; - -export default class AppBreadcrumbsComponent extends Component { - @action - isOneCrumbUp(iter = 0, totalNum = 0) { - return iter === totalNum - 2; - } -} diff --git a/ui/app/components/breadcrumbs/default.gjs b/ui/app/components/breadcrumbs/default.gjs new file mode 100644 index 00000000000..58d345339a4 --- /dev/null +++ b/ui/app/components/breadcrumbs/default.gjs @@ -0,0 +1,94 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import { LinkTo } from '@ember/routing'; +import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut'; + +export default class BreadcrumbsTemplate extends Component { + @service router; + + shortcutPattern = ['u']; + + /** + * The route name extracted from the crumb args array. + * @returns {string} + */ + get route() { + return this.args.crumb?.args?.[0]; + } + + /** + * The dynamic segments (models) for the route, extracted from the crumb args array. + * @returns {Array} + */ + get models() { + return this.args.crumb?.args?.slice(1) ?? []; + } + + get isOneCrumbUp() { + return this.args.isOneCrumbUp?.() ?? false; + } + + traverseUpALevel = () => { + this.router.transitionTo(this.route, ...this.models); + }; + + +} diff --git a/ui/app/components/breadcrumbs/default.hbs b/ui/app/components/breadcrumbs/default.hbs deleted file mode 100644 index 2eed07a83b6..00000000000 --- a/ui/app/components/breadcrumbs/default.hbs +++ /dev/null @@ -1,35 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -
  • - - {{#if @crumb.title}} -
    -
    - {{@crumb.title}} -
    -
    - {{@crumb.label}} -
    -
    - {{else}} - {{@crumb.label}} - {{/if}} -
    -
  • \ No newline at end of file diff --git a/ui/app/components/breadcrumbs/default.js b/ui/app/components/breadcrumbs/default.js deleted file mode 100644 index bc7349800f5..00000000000 --- a/ui/app/components/breadcrumbs/default.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut'; -import { service } from '@ember/service'; - -export default class BreadcrumbsTemplate extends Component { - @service router; - - /** - * The route name extracted from the crumb args array. - * @returns {string} - */ - get route() { - return this.args.crumb?.args?.[0]; - } - - /** - * The dynamic segments (models) for the route, extracted from the crumb args array. - * @returns {Array} - */ - get models() { - return this.args.crumb?.args?.slice(1) ?? []; - } - - @action - traverseUpALevel() { - this.router.transitionTo(this.route, ...this.models); - } - - get maybeKeyboardShortcut() { - return this.args.isOneCrumbUp() ? KeyboardShortcutModifier : null; - } -} diff --git a/ui/app/components/breadcrumbs/job.gjs b/ui/app/components/breadcrumbs/job.gjs new file mode 100644 index 00000000000..c7e1f69ff84 --- /dev/null +++ b/ui/app/components/breadcrumbs/job.gjs @@ -0,0 +1,121 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { assert } from '@ember/debug'; +import { LinkTo } from '@ember/routing'; +// eslint-disable-next-line ember/no-at-ember-render-modifiers +import didInsert from '@ember/render-modifiers/modifiers/did-insert'; +import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut'; +import Trigger from 'nomad-ui/components/trigger'; +import BreadcrumbsTemplate from 'nomad-ui/components/breadcrumbs/default'; + +export default class BreadcrumbsJob extends BreadcrumbsTemplate { + shortcutPattern = ['u']; + + get job() { + return this.args.crumb.job; + } + + get hasParent() { + return !!this.job.belongsTo('parent').id(); + } + + traverseUpALevel = () => { + this.router.transitionTo('jobs.job', this.job.idWithNamespace); + }; + + onError = (err) => { + assert(`Error: ${err.message}`); + }; + + fetchParent = () => { + if (this.hasParent) { + return this.job.get('parent'); + } + }; + + +} diff --git a/ui/app/components/breadcrumbs/job.hbs b/ui/app/components/breadcrumbs/job.hbs deleted file mode 100644 index 2b99b63aaa2..00000000000 --- a/ui/app/components/breadcrumbs/job.hbs +++ /dev/null @@ -1,61 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - - - {{did-insert-helper trigger.fns.do}} - {{#if trigger.data.isBusy}} -
  • - - … - -
  • - {{/if}} - {{#if trigger.data.isSuccess}} - {{#if (and trigger.data.result this.hasParent)}} -
  • - -
    -
    - Parent Job -
    -
    - {{trigger.data.result.trimmedName}} -
    -
    -
    -
  • - {{/if}} -
  • - -
    -
    - {{if this.job.hasChildren "Parent Job" "Job"}} -
    -
    - {{this.job.trimmedName}} -
    -
    -
    -
  • - {{/if}} -
    \ No newline at end of file diff --git a/ui/app/components/breadcrumbs/job.js b/ui/app/components/breadcrumbs/job.js deleted file mode 100644 index 4e629c3b30d..00000000000 --- a/ui/app/components/breadcrumbs/job.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { assert } from '@ember/debug'; -import { action } from '@ember/object'; -import BreadcrumbsTemplate from './default'; - -export default class BreadcrumbsJob extends BreadcrumbsTemplate { - get job() { - return this.args.crumb.job; - } - - get hasParent() { - return !!this.job.belongsTo('parent').id(); - } - - @action - traverseUpALevel() { - this.router.transitionTo('jobs.job', this.job.idWithNamespace); - } - - @action - onError(err) { - assert(`Error: ${err.message}`); - } - - @action - fetchParent() { - if (this.hasParent) { - return this.job.get('parent'); - } - } -} diff --git a/ui/tests/integration/components/app-breadcrumbs-test.gjs b/ui/tests/integration/components/app-breadcrumbs-test.gjs new file mode 100644 index 00000000000..ff74f43ca27 --- /dev/null +++ b/ui/tests/integration/components/app-breadcrumbs-test.gjs @@ -0,0 +1,111 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { findAll, render } from '@ember/test-helpers'; +import AppBreadcrumbs from 'nomad-ui/components/app-breadcrumbs'; +import Breadcrumb from 'nomad-ui/components/breadcrumb'; + +module('Integration | Component | app breadcrumbs', function (hooks) { + setupRenderingTest(hooks); + + const commonCrumbs = [ + { label: 'Jobs', args: ['jobs.index'] }, + { label: 'Job', args: ['jobs.job.index'] }, + ]; + + test('every breadcrumb is rendered correctly', async function (assert) { + this.commonCrumbs = commonCrumbs; + + await render( + , + ); + + assert + .dom('[data-test-breadcrumb-default]') + .exists( + 'We register the default breadcrumb component if no type is specified on the crumb', + ); + + const renderedCrumbs = findAll('[data-test-breadcrumb]'); + + renderedCrumbs.forEach((crumb, index) => { + assert.deepEqual( + crumb.textContent.trim(), + commonCrumbs[index].label, + `Crumb ${index} is ${commonCrumbs[index].label}`, + ); + }); + }); + + test('crumbs without a type default to the default breadcrumb component', async function (assert) { + this.crumbs = [ + { label: 'Jobs', args: ['jobs.index'] }, + { label: 'Job', args: ['jobs.job.index'] }, + ]; + + await render( + , + ); + + assert + .dom('[data-test-breadcrumb-default]') + .exists( + { count: 2 }, + 'All crumbs without a type render as default breadcrumbs', + ); + }); + + test('crumbs with type job render the job breadcrumb component', async function (assert) { + const job = { + idWithNamespace: 'example@default', + trimmedName: 'example', + hasChildren: false, + belongsTo() { + return { + id() { + return null; + }, + }; + }, + get() { + return null; + }, + }; + + this.crumbs = [ + { + label: 'Job', + type: 'job', + args: ['jobs.job.index'], + job, + }, + ]; + + await render( + , + ); + + assert + .dom('[data-test-job-breadcrumb]') + .exists({ count: 1 }, 'Job breadcrumb is rendered for type=job'); + }); +}); diff --git a/ui/tests/integration/components/app-breadcrumbs-test.js b/ui/tests/integration/components/app-breadcrumbs-test.js deleted file mode 100644 index c6cbd02ae17..00000000000 --- a/ui/tests/integration/components/app-breadcrumbs-test.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { findAll, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | app breadcrumbs', function (hooks) { - setupRenderingTest(hooks); - - const commonCrumbs = [ - { label: 'Jobs', args: ['jobs.index'] }, - { label: 'Job', args: ['jobs.job.index'] }, - ]; - - test('every breadcrumb is rendered correctly', async function (assert) { - this.set('commonCrumbs', commonCrumbs); - await render(hbs` - - {{#each this.commonCrumbs as |crumb|}} - - {{/each}} - `); - - assert - .dom('[data-test-breadcrumb-default]') - .exists( - 'We register the default breadcrumb component if no type is specified on the crumb', - ); - - const renderedCrumbs = findAll('[data-test-breadcrumb]'); - - renderedCrumbs.forEach((crumb, index) => { - assert.deepEqual( - crumb.textContent.trim(), - commonCrumbs[index].label, - `Crumb ${index} is ${commonCrumbs[index].label}`, - ); - }); - }); - - test('crumbs without a type default to the default breadcrumb component', async function (assert) { - this.set('crumbs', [ - { label: 'Jobs', args: ['jobs.index'] }, - { label: 'Job', args: ['jobs.job.index'] }, - ]); - - await render(hbs` - - {{#each this.crumbs as |crumb|}} - - {{/each}} - `); - - assert - .dom('[data-test-breadcrumb-default]') - .exists( - { count: 2 }, - 'All crumbs without a type render as default breadcrumbs', - ); - }); -}); From efb196a944a36969c866f0992b77d59cf927da64 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 01:33:38 -0700 Subject: [PATCH 15/86] refactor(ui): migrate placement-failure chain to gjs --- .../job-page/parts/placement-failures.gjs | 28 +++++ .../job-page/parts/placement-failures.hbs | 21 ---- .../job-page/parts/placement-failures.js | 14 --- ui/app/components/placement-failure.gjs | 119 ++++++++++++++++++ ui/app/components/placement-failure.hbs | 80 ------------ ui/app/components/placement-failure.js | 17 --- ...es-test.js => placement-failures-test.gjs} | 20 ++- ...ure-test.js => placement-failure-test.gjs} | 46 +++---- 8 files changed, 175 insertions(+), 170 deletions(-) create mode 100644 ui/app/components/job-page/parts/placement-failures.gjs delete mode 100644 ui/app/components/job-page/parts/placement-failures.hbs delete mode 100644 ui/app/components/job-page/parts/placement-failures.js create mode 100644 ui/app/components/placement-failure.gjs delete mode 100644 ui/app/components/placement-failure.hbs delete mode 100644 ui/app/components/placement-failure.js rename ui/tests/integration/components/job-page/parts/{placement-failures-test.js => placement-failures-test.gjs} (89%) rename ui/tests/integration/components/{placement-failure-test.js => placement-failure-test.gjs} (85%) diff --git a/ui/app/components/job-page/parts/placement-failures.gjs b/ui/app/components/job-page/parts/placement-failures.gjs new file mode 100644 index 00000000000..99b2bc89330 --- /dev/null +++ b/ui/app/components/job-page/parts/placement-failures.gjs @@ -0,0 +1,28 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { HdsAlert } from '@hashicorp/design-system-components/components'; +import PlacementFailure from 'nomad-ui/components/placement-failure'; + +const PlacementFailures = ; + +export default PlacementFailures; diff --git a/ui/app/components/job-page/parts/placement-failures.hbs b/ui/app/components/job-page/parts/placement-failures.hbs deleted file mode 100644 index 5d687f990af..00000000000 --- a/ui/app/components/job-page/parts/placement-failures.hbs +++ /dev/null @@ -1,21 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{#if this.job.hasPlacementFailures}} - - Placement Failures - - {{#each this.job.taskGroups as |taskGroup|}} - - {{/each}} - - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/job-page/parts/placement-failures.js b/ui/app/components/job-page/parts/placement-failures.js deleted file mode 100644 index 6f6558da2e2..00000000000 --- a/ui/app/components/job-page/parts/placement-failures.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; - -@classic -@tagName('') -export default class PlacementFailures extends Component { - job = null; -} diff --git a/ui/app/components/placement-failure.gjs b/ui/app/components/placement-failure.gjs new file mode 100644 index 00000000000..6a3494d8018 --- /dev/null +++ b/ui/app/components/placement-failure.gjs @@ -0,0 +1,119 @@ +/** + * Copyright IBM Corp. 2015, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { HdsBadge } from '@hashicorp/design-system-components/components'; + +const isZero = (value) => value === 0; +const plusOne = (value) => value + 1; +const pluralizedNode = (count) => (count === 1 ? 'node' : 'nodes'); + +export default class PlacementFailure extends Component { + get placementFailures() { + return this.args.taskGroup?.placementFailures ?? this.args.failedTGAlloc; + } + + +} diff --git a/ui/app/components/placement-failure.hbs b/ui/app/components/placement-failure.hbs deleted file mode 100644 index c2ecaaf883a..00000000000 --- a/ui/app/components/placement-failure.hbs +++ /dev/null @@ -1,80 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{#if this.placementFailures}} - {{#let this.placementFailures as |failures|}} -

    - {{this.placementFailures.name}} - -

    -
      - {{#if (eq failures.nodesEvaluated 0)}} -
    • No nodes were - eligible for evaluation
    • - {{/if}} - {{#each-in failures.nodesAvailable as |datacenter available|}} - {{#if (eq available 0)}} -
    • No - nodes are available in datacenter - {{datacenter}}
    • - {{/if}} - {{/each-in}} - {{#each-in failures.classFiltered as |class count|}} -
    • Class - {{class}} - filtered - {{count}} - {{pluralize "node" count}}
    • - {{/each-in}} - {{#each-in failures.constraintFiltered as |constraint count|}} -
    • Constraint - {{constraint}} - filtered - {{count}} - {{pluralize "node" count}}
    • - {{/each-in}} - {{#if failures.nodesExhausted}} -
    • Resources exhausted on - {{failures.nodesExhausted}} - {{pluralize "node" failures.nodesExhausted}}
    • - {{/if}} - {{#each-in failures.classExhausted as |class count|}} -
    • Class - {{class}} - exhausted on - {{count}} - {{pluralize "node" count}}
    • - {{/each-in}} - {{#each-in failures.dimensionExhausted as |dimension count|}} -
    • Dimension - {{dimension}} - exhausted on - {{count}} - {{pluralize "node" count}}
    • - {{/each-in}} - {{#each-in failures.quotaExhausted as |quota dimension|}} -
    • Quota limit - hit - {{dimension}}
    • - {{/each-in}} - {{#each-in failures.scores as |name score|}} -
    • Score - {{name}} - = - {{score}}
    • - {{/each-in}} -
    - {{/let}} -{{/if}} \ No newline at end of file diff --git a/ui/app/components/placement-failure.js b/ui/app/components/placement-failure.js deleted file mode 100644 index 8f69c9aab53..00000000000 --- a/ui/app/components/placement-failure.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { or } from '@ember/object/computed'; -import classic from 'ember-classic-decorator'; - -@classic -export default class PlacementFailure extends Component { - // Either provide a taskGroup or a failedTGAlloc - taskGroup = null; - failedTGAlloc = null; - - @or('taskGroup.placementFailures', 'failedTGAlloc') placementFailures; -} diff --git a/ui/tests/integration/components/job-page/parts/placement-failures-test.js b/ui/tests/integration/components/job-page/parts/placement-failures-test.gjs similarity index 89% rename from ui/tests/integration/components/job-page/parts/placement-failures-test.js rename to ui/tests/integration/components/job-page/parts/placement-failures-test.gjs index 37fe6165e0c..3f46920938d 100644 --- a/ui/tests/integration/components/job-page/parts/placement-failures-test.js +++ b/ui/tests/integration/components/job-page/parts/placement-failures-test.gjs @@ -4,10 +4,10 @@ */ /* Mirage fixtures are random so we can't expect a set number of assertions */ -import { hbs } from 'ember-cli-htmlbars'; import { findAll, find, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; +import JobPagePartsPlacementFailures from 'nomad-ui/components/job-page/parts/placement-failures'; import { startMirage } from 'nomad-ui/tests/helpers/start-mirage'; import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; @@ -41,13 +41,11 @@ module( const job = this.store.peekAll('job').get('firstObject'); await job.reload(); - this.set('job', job); - - await render(hbs` - ) - `); + await render( + , + ); - const failedEvaluation = this.job.evaluations + const failedEvaluation = job.evaluations .filterBy('hasPlacementFailures') .sortBy('modifyIndex') .reverse() @@ -90,11 +88,9 @@ module( const job = this.store.peekAll('job').get('firstObject'); await job.reload(); - this.set('job', job); - - await render(hbs` - ) - `); + await render( + , + ); assert.notOk( find('[data-test-placement-failures]'), diff --git a/ui/tests/integration/components/placement-failure-test.js b/ui/tests/integration/components/placement-failure-test.gjs similarity index 85% rename from ui/tests/integration/components/placement-failure-test.js rename to ui/tests/integration/components/placement-failure-test.gjs index 258d77b27cd..e846916f9a2 100644 --- a/ui/tests/integration/components/placement-failure-test.js +++ b/ui/tests/integration/components/placement-failure-test.gjs @@ -1,36 +1,31 @@ /** - * Copyright IBM Corp. 2015, 2025 + * Copyright IBM Corp. 2015, 2026 * SPDX-License-Identifier: BUSL-1.1 */ import { find, findAll, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { hbs } from 'ember-cli-htmlbars'; -import cleanWhitespace from '../../utils/clean-whitespace'; +import PlacementFailure from 'nomad-ui/components/placement-failure'; +import cleanWhitespace from 'nomad-ui/tests/utils/clean-whitespace'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; module('Integration | Component | placement failures', function (hooks) { setupRenderingTest(hooks); - const commonTemplate = hbs` - - `; - test('should render the placement failure (basic render)', async function (assert) { const name = 'Placement Failure'; const failures = 11; - this.set( - 'taskGroup', - createFixture( - { - coalescedFailures: failures - 1, - }, - name, - ), + const taskGroup = createFixture( + { + coalescedFailures: failures - 1, + }, + name, ); - await render(commonTemplate); + await render( + , + ); assert.deepEqual( cleanWhitespace( @@ -96,15 +91,14 @@ module('Integration | Component | placement failures', function (hooks) { }); test('should render correctly when a node is not evaluated', async function (assert) { - this.set( - 'taskGroup', - createFixture({ - nodesEvaluated: 1, - nodesExhausted: 0, - }), - ); + const taskGroup = createFixture({ + nodesEvaluated: 1, + nodesExhausted: 0, + }); - await render(commonTemplate); + await render( + , + ); assert.deepEqual( findAll('[data-test-placement-failure-no-evaluated-nodes]').length, @@ -122,10 +116,10 @@ module('Integration | Component | placement failures', function (hooks) { function createFixture(obj = {}, name = 'Placement Failure') { return { - name: name, + name, placementFailures: Object.assign( { - name: name, + name, coalescedFailures: 10, nodesEvaluated: 0, nodesAvailable: { From ea403729e07713a02f06d8e107ede0b8d5008410 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 02:18:37 -0700 Subject: [PATCH 16/86] refactor(ui): migrate list-table chain to gjs --- ui/app/components/fs/browser.hbs | 6 +- ui/app/components/job-page/parts/children.hbs | 12 ++-- .../components/job-page/parts/task-groups.hbs | 28 ++++---- ui/app/components/list-table.gjs | 36 ++++++++++ ui/app/components/list-table.hbs | 16 ----- ui/app/components/list-table.js | 25 ------- ui/app/components/list-table/sort-by.gjs | 40 +++++++++++ ui/app/components/list-table/sort-by.hbs | 14 ---- ui/app/components/list-table/sort-by.js | 37 ---------- ui/app/components/list-table/table-body.gjs | 14 ++++ ui/app/components/list-table/table-body.hbs | 8 --- ui/app/components/list-table/table-body.js | 12 ---- ui/app/components/list-table/table-head.gjs | 14 ++++ ui/app/components/list-table/table-head.hbs | 8 --- ui/app/components/list-table/table-head.js | 12 ---- .../allocations/allocation/index.hbs | 12 ++-- ui/app/templates/clients/client/index.hbs | 24 +++---- ui/app/templates/clients/index.hbs | 14 ++-- ui/app/templates/jobs/job/allocations.hbs | 18 ++--- ui/app/templates/jobs/job/clients.hbs | 17 ++--- ui/app/templates/jobs/job/evaluations.hbs | 10 +-- ui/app/templates/jobs/job/services/index.hbs | 6 +- ui/app/templates/jobs/job/task-group.hbs | 24 +++---- ui/app/templates/servers/index.hbs | 16 ++--- ui/app/templates/storage/plugins/index.hbs | 8 +-- .../storage/plugins/plugin/allocations.hbs | 4 +- ...list-table-test.js => list-table-test.gjs} | 71 +++++++++++-------- 27 files changed, 242 insertions(+), 264 deletions(-) create mode 100644 ui/app/components/list-table.gjs delete mode 100644 ui/app/components/list-table.hbs delete mode 100644 ui/app/components/list-table.js create mode 100644 ui/app/components/list-table/sort-by.gjs delete mode 100644 ui/app/components/list-table/sort-by.hbs delete mode 100644 ui/app/components/list-table/sort-by.js create mode 100644 ui/app/components/list-table/table-body.gjs delete mode 100644 ui/app/components/list-table/table-body.hbs delete mode 100644 ui/app/components/list-table/table-body.js create mode 100644 ui/app/components/list-table/table-head.gjs delete mode 100644 ui/app/components/list-table/table-head.hbs delete mode 100644 ui/app/components/list-table/table-head.js rename ui/tests/integration/components/{list-table-test.js => list-table-test.gjs} (70%) diff --git a/ui/app/components/fs/browser.hbs b/ui/app/components/fs/browser.hbs index f670772d621..068fa94c78d 100644 --- a/ui/app/components/fs/browser.hbs +++ b/ui/app/components/fs/browser.hbs @@ -36,9 +36,9 @@ as |t| > - Name - File Size - Last Modified + Name + File Size + Last Modified - + Name - - + + Submitted At - - + + Status - + Completed Allocations diff --git a/ui/app/components/job-page/parts/task-groups.hbs b/ui/app/components/job-page/parts/task-groups.hbs index ea7e244f6bd..b19b01606a9 100644 --- a/ui/app/components/job-page/parts/task-groups.hbs +++ b/ui/app/components/job-page/parts/task-groups.hbs @@ -15,27 +15,27 @@ as |t| > - + Name - - + + Count - - + + Allocation Status - - + + Volume - - + + Reserved CPU - - + + Reserved Memory - - + + Reserved Disk - + ({ + model: row, + })); + } + + +} diff --git a/ui/app/components/list-table.hbs b/ui/app/components/list-table.hbs deleted file mode 100644 index 0de2cd8e08b..00000000000 --- a/ui/app/components/list-table.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{yield - (hash - head=(component "list-table/table-head") - body=(component "list-table/table-body" rows=this.decoratedSource) - sort-by=(component - "list-table/sort-by" - currentProp=this.sortProperty - sortDescending=this.sortDescending - ) - ) -}} \ No newline at end of file diff --git a/ui/app/components/list-table.js b/ui/app/components/list-table.js deleted file mode 100644 index 6de4703901b..00000000000 --- a/ui/app/components/list-table.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { computed as overridable } from 'ember-overridable-computed'; -import { classNames, tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; - -@classic -@tagName('table') -@classNames('table') -export default class ListTable extends Component { - @overridable(() => []) source; - - // Plan for a future with metadata (e.g., isSelected) - @computed('source.{[],isFulfilled}') - get decoratedSource() { - return (this.source || []).map((row) => ({ - model: row, - })); - } -} diff --git a/ui/app/components/list-table/sort-by.gjs b/ui/app/components/list-table/sort-by.gjs new file mode 100644 index 00000000000..8bee5788720 --- /dev/null +++ b/ui/app/components/list-table/sort-by.gjs @@ -0,0 +1,40 @@ +/** + * Copyright IBM Corp. 2015, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { concat, hash } from '@ember/helper'; +import Component from '@glimmer/component'; +import SafeLinkTo from 'nomad-ui/components/safe-link-to'; + +export default class SortBy extends Component { + get isActive() { + return this.args.currentProp === this.args.prop; + } + + get shouldSortDescending() { + return !this.isActive || !this.args.sortDescending; + } + + +} diff --git a/ui/app/components/list-table/sort-by.hbs b/ui/app/components/list-table/sort-by.hbs deleted file mode 100644 index 4263b74f7f2..00000000000 --- a/ui/app/components/list-table/sort-by.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - - - {{yield}} - \ No newline at end of file diff --git a/ui/app/components/list-table/sort-by.js b/ui/app/components/list-table/sort-by.js deleted file mode 100644 index f75b09a4373..00000000000 --- a/ui/app/components/list-table/sort-by.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { - classNames, - attributeBindings, - classNameBindings, - tagName, -} from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; - -@classic -@tagName('th') -@attributeBindings('title') -@classNames('is-selectable') -@classNameBindings('isActive:is-active', 'sortDescending:desc:asc') -export default class SortBy extends Component { - // The prop that the table is currently sorted by - currentProp = ''; - - // The prop this sorter controls - prop = ''; - - @computed('currentProp', 'prop') - get isActive() { - return this.currentProp === this.prop; - } - - @computed('sortDescending', 'isActive') - get shouldSortDescending() { - return !this.isActive || !this.sortDescending; - } -} diff --git a/ui/app/components/list-table/table-body.gjs b/ui/app/components/list-table/table-body.gjs new file mode 100644 index 00000000000..b2178179eed --- /dev/null +++ b/ui/app/components/list-table/table-body.gjs @@ -0,0 +1,14 @@ +/** + * Copyright IBM Corp. 2015, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +const TableBody = ; + +export default TableBody; diff --git a/ui/app/components/list-table/table-body.hbs b/ui/app/components/list-table/table-body.hbs deleted file mode 100644 index c4133e85307..00000000000 --- a/ui/app/components/list-table/table-body.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{#each this.rows key=this.key as |row index|}} - {{yield row index}} -{{/each}} \ No newline at end of file diff --git a/ui/app/components/list-table/table-body.js b/ui/app/components/list-table/table-body.js deleted file mode 100644 index 9d7fa3ea810..00000000000 --- a/ui/app/components/list-table/table-body.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; - -@classic -@tagName('tbody') -export default class TableBody extends Component {} diff --git a/ui/app/components/list-table/table-head.gjs b/ui/app/components/list-table/table-head.gjs new file mode 100644 index 00000000000..64f0976697c --- /dev/null +++ b/ui/app/components/list-table/table-head.gjs @@ -0,0 +1,14 @@ +/** + * Copyright IBM Corp. 2015, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +const TableHead = ; + +export default TableHead; diff --git a/ui/app/components/list-table/table-head.hbs b/ui/app/components/list-table/table-head.hbs deleted file mode 100644 index 907e018f3d7..00000000000 --- a/ui/app/components/list-table/table-head.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - - - {{yield}} - \ No newline at end of file diff --git a/ui/app/components/list-table/table-head.js b/ui/app/components/list-table/table-head.js deleted file mode 100644 index 8e7edc854da..00000000000 --- a/ui/app/components/list-table/table-head.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; - -@classic -@tagName('thead') -export default class TableHead extends Component {} diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 354241b10da..06e10a02688 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -189,18 +189,18 @@ > Driver Health - + Name - - + + State - + Last Event - + Time - + Volumes diff --git a/ui/app/templates/clients/client/index.hbs b/ui/app/templates/clients/client/index.hbs index 7315f080e88..5dac57db368 100644 --- a/ui/app/templates/clients/client/index.hbs +++ b/ui/app/templates/clients/client/index.hbs @@ -560,24 +560,24 @@ Driver Health, Scheduling, and Preemption - + ID - - + + Created - - + + Modified - - + + Status - - + + Job - - + + Version - + Volume diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs index 79ce278d534..382c22bcd40 100644 --- a/ui/app/templates/clients/index.hbs +++ b/ui/app/templates/clients/index.hbs @@ -258,16 +258,16 @@ > Driver Health - ID - ID + Name - State + >Name + State Address - Node Pool - Datacenter - Version + Node Pool + Datacenter + Version # Volumes # Allocs diff --git a/ui/app/templates/jobs/job/allocations.hbs b/ui/app/templates/jobs/job/allocations.hbs index 15f2d53f493..90d3a0fd63c 100644 --- a/ui/app/templates/jobs/job/allocations.hbs +++ b/ui/app/templates/jobs/job/allocations.hbs @@ -73,19 +73,19 @@ Driver Health, Scheduling, and Preemption - ID - Task Group - ID + Task Group + Created - Created + Modified - Status - Version - Client + >Modified + Status + Version + Client Volume CPU Memory diff --git a/ui/app/templates/jobs/job/clients.hbs b/ui/app/templates/jobs/job/clients.hbs index 9a0a568e4b8..dc925aa8e43 100644 --- a/ui/app/templates/jobs/job/clients.hbs +++ b/ui/app/templates/jobs/job/clients.hbs @@ -58,18 +58,15 @@ as |t| > - Client ID - Client - Name - Created - Client ID + Client + Name + Created + Modified - Job Status + >Modified + Job Status Allocation Summary diff --git a/ui/app/templates/jobs/job/evaluations.hbs b/ui/app/templates/jobs/job/evaluations.hbs index a8c83d58b7c..367283d5957 100644 --- a/ui/app/templates/jobs/job/evaluations.hbs +++ b/ui/app/templates/jobs/job/evaluations.hbs @@ -15,11 +15,11 @@ > ID - Priority - Created - Triggered By - Status - Placement Failures + Priority + Created + Triggered By + Status + Placement Failures diff --git a/ui/app/templates/jobs/job/services/index.hbs b/ui/app/templates/jobs/job/services/index.hbs index 4b2f123c347..dbe4f4f9351 100644 --- a/ui/app/templates/jobs/job/services/index.hbs +++ b/ui/app/templates/jobs/job/services/index.hbs @@ -12,10 +12,10 @@ as |t| > - Name - Level + Name + Level Tags - Number of Allocations + Number of Allocations Driver Health, Scheduling, and Preemption - + ID - - + + Created - - + + Modified - - + + Status - - + + Version - - + + Client - + Volume diff --git a/ui/app/templates/servers/index.hbs b/ui/app/templates/servers/index.hbs index 72babcf4ac3..1e2cb57c80e 100644 --- a/ui/app/templates/servers/index.hbs +++ b/ui/app/templates/servers/index.hbs @@ -22,16 +22,16 @@ as |t| > - Name - Status - Leader - Name + Status + Leader + Address - port - Datacenter - Version + >Address + port + Datacenter + Version diff --git a/ui/app/templates/storage/plugins/index.hbs b/ui/app/templates/storage/plugins/index.hbs index 5b07ff86ab2..b767b65ef82 100644 --- a/ui/app/templates/storage/plugins/index.hbs +++ b/ui/app/templates/storage/plugins/index.hbs @@ -36,10 +36,10 @@ as |t| > - ID - Controller Health - Node Health - Provider + ID + Controller Health + Node Health + Provider ID Created - Modified - Health + Modified + Health Client Job Version diff --git a/ui/tests/integration/components/list-table-test.js b/ui/tests/integration/components/list-table-test.gjs similarity index 70% rename from ui/tests/integration/components/list-table-test.js rename to ui/tests/integration/components/list-table-test.gjs index ffb66f2aa74..b3d76bf6a90 100644 --- a/ui/tests/integration/components/list-table-test.js +++ b/ui/tests/integration/components/list-table-test.gjs @@ -6,9 +6,9 @@ import { findAll, find, render } from '@ember/test-helpers'; import { module, skip, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import faker from 'nomad-ui/mirage/faker'; -import { hbs } from 'ember-cli-htmlbars'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; +import ListTable from 'nomad-ui/components/list-table'; +import faker from 'nomad-ui/mirage/faker'; module('Integration | Component | list table', function (hooks) { setupRenderingTest(hooks); @@ -23,16 +23,19 @@ module('Integration | Component | list table', function (hooks) { // thead test('component exposes a thead contextual component', async function (assert) { - this.set('source', commonTable); - await render(hbs` - - - First Name - Last Name - Age - - - `); + const source = commonTable; + + await render( + , + ); assert.ok(findAll('.head').length, 'Table head is rendered'); assert.deepEqual( @@ -44,23 +47,29 @@ module('Integration | Component | list table', function (hooks) { // tbody test('component exposes a tbody contextual component', async function (assert) { - this.setProperties({ - source: commonTable, - sortProperty: 'firstName', - sortDescending: false, - }); - await render(hbs` - - - - {{row.model.firstName}} - {{row.model.lastName}} - {{row.model.age}} - {{index}} - - - - `); + const source = commonTable; + const sortProperty = 'firstName'; + const sortDescending = false; + + await render( + , + ); assert.ok(findAll('.body').length, 'Table body is rendered'); assert.deepEqual( @@ -71,13 +80,13 @@ module('Integration | Component | list table', function (hooks) { assert.deepEqual( findAll('.item').length, - this.source.length, + source.length, 'Each item gets its own row', ); // list-table is not responsible for sorting, only dispatching sort events. The table is still // rendered in index-order. - this.source.forEach((item, index) => { + source.forEach((item, index) => { const $item = this.element.querySelectorAll('.item')[index]; assert.strictEqual( $item.querySelectorAll('td')[0].textContent.trim(), From ea7134486258818b2c0cd45e1eb6b1bd5580dc4a Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 02:34:52 -0700 Subject: [PATCH 17/86] refactor(ui): migrate list-pagination chain to gjs --- ui/app/components/list-pagination.gjs | 121 ++++++++++++ ui/app/components/list-pagination.hbs | 45 ----- ui/app/components/list-pagination.js | 58 ------ .../components/list-pagination/list-pager.gjs | 58 ++++++ .../components/list-pagination/list-pager.hbs | 21 -- .../components/list-pagination/list-pager.js | 34 ---- ui/app/components/list-table.gjs | 2 +- ...ation-test.js => list-pagination-test.gjs} | 182 +++++++++++------- 8 files changed, 290 insertions(+), 231 deletions(-) create mode 100644 ui/app/components/list-pagination.gjs delete mode 100644 ui/app/components/list-pagination.hbs delete mode 100644 ui/app/components/list-pagination.js create mode 100644 ui/app/components/list-pagination/list-pager.gjs delete mode 100644 ui/app/components/list-pagination/list-pager.hbs delete mode 100644 ui/app/components/list-pagination/list-pager.js rename ui/tests/integration/components/{list-pagination-test.js => list-pagination-test.gjs} (59%) diff --git a/ui/app/components/list-pagination.gjs b/ui/app/components/list-pagination.gjs new file mode 100644 index 00000000000..53cd19c2685 --- /dev/null +++ b/ui/app/components/list-pagination.gjs @@ -0,0 +1,121 @@ +/** + * Copyright IBM Corp. 2015, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; +import ListPager from 'nomad-ui/components/list-pagination/list-pager'; + +export default class ListPagination extends Component { + get source() { + return this.args.source ?? []; + } + + get size() { + return this.args.size ?? 25; + } + + get page() { + return this.args.page ?? 1; + } + + get spread() { + return this.args.spread ?? 2; + } + + get startsAt() { + return (this.page - 1) * this.size + 1; + } + + get endsAt() { + return Math.min(this.page * this.size, this.source.length); + } + + get lastPage() { + return Math.ceil(this.source.length / this.size); + } + + get pageLinks() { + const { spread, page, lastPage } = this; + + // When there is only one page, don't bother with page links + if (lastPage === 1) { + return []; + } + + const lowerBound = Math.max(1, page - spread); + const upperBound = Math.min(lastPage, page + spread) + 1; + + return Array(upperBound - lowerBound) + .fill(null) + .map((_, index) => ({ + pageNumber: lowerBound + index, + })); + } + + get list() { + const size = this.size; + const start = (this.page - 1) * size; + return this.source.slice(start, start + size); + } + + get firstOrPrevVisible() { + return this.page !== 1; + } + + get nextOrLastVisible() { + return this.page !== this.lastPage; + } + + get prevPage() { + return this.page - 1; + } + + get nextPage() { + return this.page + 1; + } + + +} diff --git a/ui/app/components/list-pagination.hbs b/ui/app/components/list-pagination.hbs deleted file mode 100644 index 03a91151d6c..00000000000 --- a/ui/app/components/list-pagination.hbs +++ /dev/null @@ -1,45 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{#if this.source.length}} - {{yield - (hash - first=(component - "list-pagination/list-pager" - test="first" - label="First page" - page=1 - visible=(not-eq this.page 1) - ) - prev=(component - "list-pagination/list-pager" - test="prev" - label="Previous page" - page=(dec this.page) - visible=(not-eq this.page 1) - ) - next=(component - "list-pagination/list-pager" - test="next" - label="Next page" - page=(inc this.page) - visible=(not-eq this.page this.lastPage) - ) - last=(component - "list-pagination/list-pager" - test="last" - label="Last page" - page=this.lastPage - visible=(not-eq this.page this.lastPage) - ) - pageLinks=this.pageLinks - currentPage=this.page - totalPages=this.lastPage - startsAt=this.startsAt - endsAt=this.endsAt - list=this.list - ) - }} -{{/if}} \ No newline at end of file diff --git a/ui/app/components/list-pagination.js b/ui/app/components/list-pagination.js deleted file mode 100644 index 3edc66ffb5c..00000000000 --- a/ui/app/components/list-pagination.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { computed as overridable } from 'ember-overridable-computed'; -import classic from 'ember-classic-decorator'; - -@classic -export default class ListPagination extends Component { - @overridable(() => []) source; - size = 25; - page = 1; - spread = 2; - - @computed('size', 'page') - get startsAt() { - return (this.page - 1) * this.size + 1; - } - - @computed('source.[]', 'size', 'page') - get endsAt() { - return Math.min(this.page * this.size, this.get('source.length')); - } - - @computed('source.[]', 'size') - get lastPage() { - return Math.ceil(this.get('source.length') / this.size); - } - - @computed('source.[]', 'page', 'spread') - get pageLinks() { - const { spread, page, lastPage } = this; - - // When there is only one page, don't bother with page links - if (lastPage === 1) { - return []; - } - - const lowerBound = Math.max(1, page - spread); - const upperBound = Math.min(lastPage, page + spread) + 1; - - return Array(upperBound - lowerBound) - .fill(null) - .map((_, index) => ({ - pageNumber: lowerBound + index, - })); - } - - @computed('source.[]', 'page', 'size') - get list() { - const size = this.size; - const start = (this.page - 1) * size; - return this.source.slice(start, start + size); - } -} diff --git a/ui/app/components/list-pagination/list-pager.gjs b/ui/app/components/list-pagination/list-pager.gjs new file mode 100644 index 00000000000..48d01645884 --- /dev/null +++ b/ui/app/components/list-pagination/list-pager.gjs @@ -0,0 +1,58 @@ +/** + * Copyright IBM Corp. 2015, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut'; + +export default class ListPager extends Component { + @service router; + + // Even though we don't currently use "first" / "last" pagination in the app, + // the option is there at a component level, so let's make sure that we + // only append keyNav to the "next" and "prev" links. + // We use this to make the modifier conditional, per https://v5.chriskrycho.com/journal/conditional-modifiers-and-helpers-in-emberjs/ + get includeKeyboardNav() { + return this.args.label === 'Next page' || + this.args.label === 'Previous page' + ? KeyboardShortcutModifier + : null; + } + + get keyboardLabel() { + return this.args.label === 'Next page' ? 'Next Page' : 'Previous Page'; + } + + get keyboardPattern() { + return this.args.label === 'Next page' ? [']', ']'] : ['[', '[']; + } + + gotoRoute = () => { + this.router.transitionTo({ + queryParams: { page: this.args.page }, + }); + }; + + +} diff --git a/ui/app/components/list-pagination/list-pager.hbs b/ui/app/components/list-pagination/list-pager.hbs deleted file mode 100644 index eabfd33d1b5..00000000000 --- a/ui/app/components/list-pagination/list-pager.hbs +++ /dev/null @@ -1,21 +0,0 @@ -{{! - Copyright IBM Corp. 2015, 2025 - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{#if this.visible}} - - {{yield}} - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/list-pagination/list-pager.js b/ui/app/components/list-pagination/list-pager.js deleted file mode 100644 index 79910a4b77c..00000000000 --- a/ui/app/components/list-pagination/list-pager.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright IBM Corp. 2015, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import KeyboardShortcutModifier from 'nomad-ui/modifiers/keyboard-shortcut'; - -@classic -@tagName('') -export default class ListPager extends Component { - @service router; - - // Even though we don't currently use "first" / "last" pagination in the app, - // the option is there at a component level, so let's make sure that we - // only append keyNav to the "next" and "prev" links. - // We use this to make the modifier conditional, per https://v5.chriskrycho.com/journal/conditional-modifiers-and-helpers-in-emberjs/ - get includeKeyboardNav() { - return this.label === 'Next page' || this.label === 'Previous page' - ? KeyboardShortcutModifier - : null; - } - - @action - gotoRoute() { - this.router.transitionTo({ - queryParams: { page: this.page }, - }); - } -} diff --git a/ui/app/components/list-table.gjs b/ui/app/components/list-table.gjs index 66553f77764..75a0f1ac265 100644 --- a/ui/app/components/list-table.gjs +++ b/ui/app/components/list-table.gjs @@ -19,7 +19,7 @@ export default class ListTable extends Component { }