From ecdf75d30b2fede2b485de14ef0c1cd65a769a19 Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 5 Dec 2025 19:01:25 +0530 Subject: [PATCH 01/10] feat: Add app token details page --- addons/core/addon/helpers/format-date-long.js | 28 ++++++ addons/core/app/helpers/format-date-long.js | 6 ++ addons/core/translations/resources/en-us.yaml | 26 +++++ .../app/components/description-list/index.hbs | 29 ++++++ .../app/components/description-list/item.hbs | 18 ++++ .../app/components/form/app-token/index.hbs | 99 +++++++++++++++++++ .../app/components/form/app-token/index.js | 89 +++++++++++++++++ ui/admin/app/router.js | 6 +- .../routes/scopes/scope/app-tokens/token.js | 13 +++ ui/admin/app/styles/app.scss | 37 +++++++ .../scopes/scope/app-tokens/index.hbs | 10 +- .../scopes/scope/app-tokens/token.hbs | 46 +++++++++ .../scopes/scope/app-tokens/token/index.hbs | 6 ++ .../scope/app-tokens/token/permissions.hbs | 8 ++ 14 files changed, 417 insertions(+), 4 deletions(-) create mode 100644 addons/core/addon/helpers/format-date-long.js create mode 100644 addons/core/app/helpers/format-date-long.js create mode 100644 ui/admin/app/components/description-list/index.hbs create mode 100644 ui/admin/app/components/description-list/item.hbs create mode 100644 ui/admin/app/components/form/app-token/index.hbs create mode 100644 ui/admin/app/components/form/app-token/index.js create mode 100644 ui/admin/app/routes/scopes/scope/app-tokens/token.js create mode 100644 ui/admin/app/templates/scopes/scope/app-tokens/token.hbs create mode 100644 ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs create mode 100644 ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs diff --git a/addons/core/addon/helpers/format-date-long.js b/addons/core/addon/helpers/format-date-long.js new file mode 100644 index 0000000000..3683e31ca3 --- /dev/null +++ b/addons/core/addon/helpers/format-date-long.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { helper } from '@ember/component/helper'; + +/** + * Takes a `Date` instance and returns a string formatted in long format + * e.g. "Oct 27, 2025, 3:30 PM PST" + */ +export default helper(function formatDateLong([date] /*, hash*/) { + if (!date) return ''; + + const formatted = date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + timeZone: 'America/Los_Angeles', + timeZoneName: 'short', + }); + + // Replace "at" with "," to match the design + return formatted.replace(' at ', ', '); +}); diff --git a/addons/core/app/helpers/format-date-long.js b/addons/core/app/helpers/format-date-long.js new file mode 100644 index 0000000000..dbda0e67b6 --- /dev/null +++ b/addons/core/app/helpers/format-date-long.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export { default } from 'core/helpers/format-date-long'; diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 077dd53a5e..9b67346c60 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -1238,13 +1238,39 @@ app-token: description: App tokens are long-lived, fine grained access tokens for authorization/authentication. titles: new: New App Token + tabs: + permissions: Permissions + about: + title: About this token + ttl: + title: Time to live (TTL) + tts: + title: Time to stale (TTS) form: + name: + help: Name to identify this app token + description: + help: A description of this app token approximate_last_access_time: label: Last used at expires_in: label: Expires in status: label: Status + created_by: + label: Created by + scope: + label: Scope + ttl: + label: TTL + expiration_date: + label: Expiration date + created_time: + label: Created at + tts: + label: TTS + last_used: + label: Last used status: unknown: Unknown active: Active diff --git a/ui/admin/app/components/description-list/index.hbs b/ui/admin/app/components/description-list/index.hbs new file mode 100644 index 0000000000..1b816b35eb --- /dev/null +++ b/ui/admin/app/components/description-list/index.hbs @@ -0,0 +1,29 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 + + A reusable component for displaying a bordered container with a title + and a list of label-value pairs in a 3-column grid layout. + + Usage example in templates: + + Value 1 + Value 2 + + + Arguments: + @title (string, optional) - Section title displayed at the top +}} + +
+ {{! Optional title spanning full width }} + {{#if @title}} +
+ + {{@title}} + +
+ {{/if}} + {{! Yield DL.Item component for each label-value pair }} + {{yield (hash Item=(component 'description-list/item'))}} +
\ No newline at end of file diff --git a/ui/admin/app/components/description-list/item.hbs b/ui/admin/app/components/description-list/item.hbs new file mode 100644 index 0000000000..f8d48be562 --- /dev/null +++ b/ui/admin/app/components/description-list/item.hbs @@ -0,0 +1,18 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 + + Individual item within a DescriptionList. + Displays a label (dt) above a value (dd) with 8px vertical spacing. + + Arguments: + @label (string, required) - The label text for this item + Block content - The value to display (can be text, components, etc.) +}} + +
+
{{@label}}
+
+ {{yield}} +
+
\ No newline at end of file diff --git a/ui/admin/app/components/form/app-token/index.hbs b/ui/admin/app/components/form/app-token/index.hbs new file mode 100644 index 0000000000..29fef4a270 --- /dev/null +++ b/ui/admin/app/components/form/app-token/index.hbs @@ -0,0 +1,99 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + + + {{t 'form.name.label'}} + {{t 'resources.app-token.form.name.help'}} + + + + {{t 'form.description.label'}} + {{t + 'resources.app-token.form.description.help' + }} + + + + + + + {{! TODO: Check where to navigate the user for global/org/proj levels }} + +
+ + {{@model.created_by_user_id}} +
+
+ +
+ + + {{this.scopeInfo.text}} + +
+
+
+ + + + {{#if this.ttlFormatted}} + {{format-day-year this.ttlFormatted}} + {{else}} + {{t 'labels.none'}} + {{/if}} + + + {{#if @model.expire_time}} + {{format-date-long @model.expire_time}} + {{else}} + {{t 'labels.none'}} + {{/if}} + + + {{#if @model.created_time}} + {{format-date-long @model.created_time}} + {{else}} + {{t 'labels.none'}} + {{/if}} + + + + + + {{#if this.ttsFormatted}} + {{format-day-year this.ttsFormatted}} + {{else}} + {{t 'labels.none'}} + {{/if}} + + + {{#if @model.approximate_last_access_time}} + {{relative-datetime-live @model.approximate_last_access_time}} + {{else}} + {{t 'labels.none'}} + {{/if}} + + +
\ No newline at end of file diff --git a/ui/admin/app/components/form/app-token/index.js b/ui/admin/app/components/form/app-token/index.js new file mode 100644 index 0000000000..837ab81c15 --- /dev/null +++ b/ui/admin/app/components/form/app-token/index.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { service } from '@ember/service'; + +export default class FormAppTokenComponent extends Component { + @service intl; + + /** + * Returns status badge configuration for app tokens + * @returns {object} + */ + get statusBadge() { + const status = this.args.model?.status; + if (!status) return { text: '', color: 'neutral' }; + + const statusConfig = { + active: { color: 'success' }, + expired: { color: 'critical' }, + revoked: { color: 'critical' }, + stale: { color: 'critical' }, + unknown: { color: 'neutral' }, + }; + + const config = statusConfig[status] || { color: 'neutral' }; + return { + text: this.intl.t(`resources.app-token.status.${status}`), + color: config.color, + }; + } + + /** + * Returns scope information (icon, text, route) based on scope type + * @returns {object} + */ + get scopeInfo() { + const scope = this.args.model?.scope; + if (!scope) return { icon: 'globe', text: '', route: '#' }; + + if (scope.isGlobal) { + return { + icon: 'globe', + text: this.intl.t('resources.scope.types.global'), + route: '#', // TODO: Add proper route + }; + } + + if (scope.isOrg) { + return { + icon: 'org', + text: this.intl.t('resources.scope.types.org'), + route: '#', // TODO: Add proper route + }; + } + + return { + icon: 'grid', + text: this.intl.t('resources.scope.types.project'), + route: '#', // TODO: Add proper route + }; + } + + /** + * Returns formatted TTL using format-time-duration helper + * @returns {number|null} TTL in days for format-day-year helper, or null + */ + get ttlFormatted() { + const ttl = this.args.model?.TTL; + if (!ttl) return null; + + // Convert milliseconds to days for format-day-year helper + return Math.floor(ttl / (1000 * 60 * 60 * 24)); + } + + /** + * Returns formatted TTS in days for format-day-year helper + * @returns {number|null} + */ + get ttsFormatted() { + const tts = this.args.model?.TTS; + if (!tts) return null; + + // Convert milliseconds to days for format-day-year helper + return Math.floor(tts / (1000 * 60 * 60 * 24)); + } +} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index 55b57c336d..9e41443e3b 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -187,7 +187,11 @@ Router.map(function () { this.route('new'); this.route('policy', { path: ':policy_id' }, function () {}); }); - this.route('app-tokens', function () {}); + this.route('app-tokens', function () { + this.route('token', { path: ':token_id' }, function () { + this.route('permissions'); + }); + }); }); }); diff --git a/ui/admin/app/routes/scopes/scope/app-tokens/token.js b/ui/admin/app/routes/scopes/scope/app-tokens/token.js new file mode 100644 index 0000000000..b3eabc67b9 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/app-tokens/token.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class ScopesScopeAppTokensTokenRoute extends Route { + @service store; + + async model(params) { + // Fetch the app-token details using the token id and scope id + return this.store.findRecord('app-token', params.token_id, { + adapterOptions: { scope_id: params.scope_id }, + }); + } +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 9e9bce29e3..b63ded9ed6 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -1071,3 +1071,40 @@ .grant-scope-selection-alert-list { list-style: disc; } + +// App token details page - description list styling +.rose-form { + .description-list { + display: flex; + flex-wrap: wrap; + padding: sizing.rems(m) sizing.rems(l); // 16px top/bottom, 24px left/right + border: sizing.rems(xxxxs) solid var(--token-color-border-primary); + border-radius: var(--token-border-radius-medium); + margin-bottom: sizing.rems(xl); + + .description-list-title { + width: 100%; + margin-bottom: sizing.rems(l); // 24px gap between title and items + + h3 { + margin: 0; + } + } + + .description-list-item { + display: flex; + flex-direction: column; + gap: sizing.rems(xs); // 8px vertical gap between dt and dd + margin-right: sizing.rems(l); // 24px horizontal gap between items + } + + dt { + font-size: 14px; + font-weight: var(--token-typography-font-weight-semibold); + } + + dd { + font-size: var(--token-typography-body-200-font-size); // size/medium + } + } +} diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs index d2bdee1d41..f35798b134 100644 --- a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs +++ b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs @@ -84,8 +84,13 @@ {{#each @model.appTokens as |token|}} - {{! TODO: Add the LinkTo component when route is implemented }} - {{token.name}} + + {{token.name}} + {{#if token.description}} {{token.description}} @@ -160,7 +165,6 @@ {{else}} - {{! TODO: Check with the product/design team for status text of translations }} + + + + + + + + + {{t 'resources.app-token.title_plural'}} + + + + {{t 'resources.app-token.description'}} + + + + + + + + + + + {{t 'titles.details'}} + + + {{t 'resources.app-token.tabs.permissions'}} + + + + + + {{outlet}} + + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs new file mode 100644 index 0000000000..18b0b24b4c --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs @@ -0,0 +1,6 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs new file mode 100644 index 0000000000..183764e8b9 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs @@ -0,0 +1,8 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + + {{! TODO: Add permissions content }} + \ No newline at end of file From a09548975ac4b0b6f015949b367638389e3fd150 Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 5 Dec 2025 22:29:21 +0530 Subject: [PATCH 02/10] Fixed few UI alighnment issue and added tooltip --- addons/core/translations/resources/en-us.yaml | 1 + .../app/components/form/app-token/index.hbs | 39 ++++++++++++------- ui/admin/app/styles/app.scss | 15 +++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 9b67346c60..77c22d3431 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -1271,6 +1271,7 @@ app-token: label: TTS last_used: label: Last used + tooltip: This value is an approximate value, not an exact value. status: unknown: Unknown active: Active diff --git a/ui/admin/app/components/form/app-token/index.hbs b/ui/admin/app/components/form/app-token/index.hbs index 29fef4a270..2f7ad7f267 100644 --- a/ui/admin/app/components/form/app-token/index.hbs +++ b/ui/admin/app/components/form/app-token/index.hbs @@ -36,7 +36,7 @@ {{! TODO: Check where to navigate the user for global/org/proj levels }}
- + {{@model.created_by_user_id}} @@ -44,11 +44,7 @@
- + {{this.scopeInfo.text}} @@ -88,12 +84,29 @@ {{t 'labels.none'}} {{/if}} - - {{#if @model.approximate_last_access_time}} - {{relative-datetime-live @model.approximate_last_access_time}} - {{else}} - {{t 'labels.none'}} - {{/if}} - +
+
+ {{t 'resources.app-token.form.last_used.label'}} + +
+
+ {{#if @model.approximate_last_access_time}} + + {{relative-datetime-live @model.approximate_last_access_time}} + + {{else}} + {{t 'labels.none'}} + {{/if}} +
+
\ No newline at end of file diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index b63ded9ed6..41d14ce66d 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -1072,6 +1072,15 @@ list-style: disc; } +// Styling for last used tooltip with underlined text +.last-used-tooltip { + font-weight: 500; + font-size: var(--token-typography-body-200-font-size); // size/medium + text-decoration: underline; + text-decoration-style: solid; + cursor: pointer; +} + // App token details page - description list styling .rose-form { .description-list { @@ -1101,6 +1110,12 @@ dt { font-size: 14px; font-weight: var(--token-typography-font-weight-semibold); + + &.label-with-tooltip { + display: inline-flex; + align-items: center; + gap: sizing.rems(xs); // 8px gap between label and icon + } } dd { From 6895234a5fed5df705a3180aa7514de426eb2dbf Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 5 Dec 2025 23:47:29 +0530 Subject: [PATCH 03/10] =?UTF-8?q?test:=20=F0=9F=92=8D=20Added=20tests=20fo?= =?UTF-8?q?r=20app-token=20details=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18007 --- .../helpers/format-date-long-test.js | 95 +++++++ .../tests/acceptance/app-tokens/read-test.js | 223 ++++++++++++++++ .../components/description-list/index-test.js | 93 +++++++ .../components/description-list/item-test.js | 95 +++++++ .../components/form/app-token/index-test.js | 237 ++++++++++++++++++ 5 files changed, 743 insertions(+) create mode 100644 addons/core/tests/integration/helpers/format-date-long-test.js create mode 100644 ui/admin/tests/acceptance/app-tokens/read-test.js create mode 100644 ui/admin/tests/integration/components/description-list/index-test.js create mode 100644 ui/admin/tests/integration/components/description-list/item-test.js create mode 100644 ui/admin/tests/integration/components/form/app-token/index-test.js diff --git a/addons/core/tests/integration/helpers/format-date-long-test.js b/addons/core/tests/integration/helpers/format-date-long-test.js new file mode 100644 index 0000000000..a500facb67 --- /dev/null +++ b/addons/core/tests/integration/helpers/format-date-long-test.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | format-date-long', function (hooks) { + setupRenderingTest(hooks); + + test('it renders date in long format', async function (assert) { + // Create a specific date: Oct 27, 2025, 3:30 PM PST + const testDate = new Date('2025-10-27T15:30:00-07:00'); + this.set('date', testDate); + + await render(hbs`{{format-date-long this.date}}`); + + // Should render as "Oct 27, 2025, 3:30 PM PDT" or "Oct 27, 2025, 3:30 PM PST" + // depending on daylight saving time + const text = this.element.textContent.trim(); + assert.ok( + text.includes('Oct 27, 2025'), + 'Date should include month, day, and year', + ); + assert.ok(text.includes('3:30 PM'), 'Date should include time'); + // Check for timezone - should be either PDT or PST + const hasPacificTimezone = text.includes('PDT') || text.includes('PST'); + assert.ok(hasPacificTimezone, 'Date should include Pacific timezone'); + }); + + test('it returns empty string for null date', async function (assert) { + this.set('date', null); + + await render(hbs`{{format-date-long this.date}}`); + + assert.strictEqual(this.element.textContent.trim(), ''); + }); + + test('it returns empty string for undefined date', async function (assert) { + this.set('date', undefined); + + await render(hbs`{{format-date-long this.date}}`); + + assert.strictEqual(this.element.textContent.trim(), ''); + }); + + test('it formats date with Pacific timezone', async function (assert) { + const testDate = new Date('2025-12-05T12:00:00-08:00'); + this.set('date', testDate); + + await render(hbs`{{format-date-long this.date}}`); + + const text = this.element.textContent.trim(); + // Should include PST or PDT based on daylight saving + const hasPacificTimezone = text.includes('PST') || text.includes('PDT'); + assert.ok(hasPacificTimezone, 'Should include Pacific timezone'); + }); + + test('it replaces "at" with comma in the format', async function (assert) { + const testDate = new Date('2025-10-27T15:30:00-07:00'); + this.set('date', testDate); + + await render(hbs`{{format-date-long this.date}}`); + + const text = this.element.textContent.trim(); + // Should use comma separator, not " at " + assert.notOk(text.includes(' at '), 'Should not contain " at "'); + // Format should be like "Oct 27, 2025, 3:30 PM PDT" + const parts = text.split(','); + assert.strictEqual(parts.length, 3, 'Should have 3 comma-separated parts'); + }); + + test('it formats different months correctly', async function (assert) { + assert.expect(3); // Expect 3 assertions (one for each month) + + const dates = [ + { date: new Date('2025-01-15T10:00:00-08:00'), month: 'Jan' }, + { date: new Date('2025-06-20T14:30:00-07:00'), month: 'Jun' }, + { date: new Date('2025-12-31T23:59:00-08:00'), month: 'Dec' }, + ]; + + for (const { date, month } of dates) { + this.set('date', date); + await render(hbs`{{format-date-long this.date}}`); + const text = this.element.textContent.trim(); + assert.ok( + text.includes(month), + `Should correctly format month as ${month}`, + ); + } + }); +}); diff --git a/ui/admin/tests/acceptance/app-tokens/read-test.js b/ui/admin/tests/acceptance/app-tokens/read-test.js new file mode 100644 index 0000000000..73a04ce14a --- /dev/null +++ b/ui/admin/tests/acceptance/app-tokens/read-test.js @@ -0,0 +1,223 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { visit, currentURL, click } from '@ember/test-helpers'; +import { setupApplicationTest } from 'admin/tests/helpers'; +import { setupSqlite } from 'api/test-support/helpers/sqlite'; +import * as commonSelectors from 'admin/tests/helpers/selectors'; +import { setRunOptions } from 'ember-a11y-testing/test-support'; + +module('Acceptance | app-tokens | read', function (hooks) { + setupApplicationTest(hooks); + setupSqlite(hooks); + + const instances = { + scopes: { + global: null, + org: null, + }, + appToken: null, + }; + + const urls = { + globalScope: null, + appTokens: null, + appToken: null, + appTokenPermissions: null, + unknownAppToken: null, + }; + + hooks.beforeEach(async function () { + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.appToken = this.server.create('app-token', { + scope: instances.scopes.org, + name: 'Test App Token', + description: 'Test token description', + status: 'active', + }); + + urls.globalScope = `/scopes/global`; + urls.orgScope = `/scopes/${instances.scopes.org.id}`; + urls.appTokens = `${urls.orgScope}/app-tokens`; + urls.appToken = `${urls.appTokens}/${instances.appToken.id}`; + urls.appTokenPermissions = `${urls.appToken}/permissions`; + urls.unknownAppToken = `${urls.appTokens}/at_unknown123`; + }); + + test('visiting an app token detail page', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.orgScope); + await click(commonSelectors.HREF(urls.appTokens)); + await click(commonSelectors.HREF(urls.appToken)); + + assert.strictEqual(currentURL(), urls.appToken); + }); + + test('app token detail page displays correct title and breadcrumbs', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check page header title + assert.dom('.hds-page-header__title').exists(); + assert.dom('.hds-page-header__title').containsText('App Tokens'); + + // Check breadcrumbs + assert.dom('.hds-breadcrumb').exists(); + }); + + test('app token detail page displays token ID copy snippet', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check for copy snippet with token ID + assert.dom('.hds-copy-snippet').exists(); + assert + .dom('.hds-copy-snippet') + .containsText(instances.appToken.id.substring(0, 10)); + }); + + test('app token detail page displays tabs', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check for Details tab + assert.dom(commonSelectors.HREF(urls.appToken)).exists(); + + // Check for Permissions tab + assert.dom(commonSelectors.HREF(urls.appTokenPermissions)).exists(); + }); + + test('can navigate between Details and Permissions tabs', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + assert.strictEqual(currentURL(), urls.appToken); + + // Navigate to Permissions tab + await click(commonSelectors.HREF(urls.appTokenPermissions)); + assert.strictEqual(currentURL(), urls.appTokenPermissions); + + // Navigate back to Details tab + await click(commonSelectors.HREF(urls.appToken)); + assert.strictEqual(currentURL(), urls.appToken); + }); + + test('Details tab displays app token form', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check that the form component is rendered + assert.dom('.rose-form').exists(); + + // Check for form field labels + assert.dom('.rose-form').containsText('Name'); + assert.dom('.rose-form').containsText('Description'); + + // Check for section titles + assert.dom('.rose-form').containsText('About this token'); + assert.dom('.rose-form').containsText('Time to live'); + assert.dom('.rose-form').containsText('Time to stale'); + + // Check for disabled inputs (HDS form fields render as disabled, not readonly) + assert.dom('input[disabled]').exists('Should have disabled fields'); + + // Check for description lists (About, TTL, TTS sections) + assert.dom('.description-list').exists({ count: 3 }); + + // Check for status badge + assert.dom('.hds-badge').exists(); + }); + + test('app token page displays status badge', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check for status badge + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Active'); + }); + + test('Permissions tab renders placeholder', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appTokenPermissions); + + // Permissions tab should have a form (even if empty for now) + assert.dom('.rose-form').exists(); + }); + + test('page title is set to app token display name', async function (assert) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); + + await visit(urls.appToken); + + // Check that page title includes token name + assert.dom('.hds-page-header__title').exists(); + }); +}); diff --git a/ui/admin/tests/integration/components/description-list/index-test.js b/ui/admin/tests/integration/components/description-list/index-test.js new file mode 100644 index 0000000000..cb05b775a7 --- /dev/null +++ b/ui/admin/tests/integration/components/description-list/index-test.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'admin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupIntl } from 'ember-intl/test-support'; + +module('Integration | Component | description-list', function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks, 'en-us'); + + test('it renders with title', async function (assert) { + await render(hbs` + + Value 1 + Value 2 + + `); + + assert.dom('.description-list').exists(); + assert.dom('.description-list-title').exists(); + assert.dom('.description-list-title h3').hasText('Section Title'); + }); + + test('it renders without title', async function (assert) { + await render(hbs` + + Value 1 + + `); + + assert.dom('.description-list').exists(); + assert.dom('.description-list-title').doesNotExist(); + }); + + test('it yields Item component correctly', async function (assert) { + await render(hbs` + + Test Value + + `); + + assert.dom('.description-list').exists(); + assert.dom('.description-list-item').exists(); + assert.dom('.description-list-item dt').hasText('Test Label'); + assert.dom('.description-list-item dd').hasText('Test Value'); + }); + + test('it renders multiple items', async function (assert) { + await render(hbs` + + Value 1 + Value 2 + Value 3 + + `); + + assert.dom('.description-list').exists(); + assert.dom('.description-list-item').exists({ count: 3 }); + }); + + test('it applies correct CSS classes', async function (assert) { + await render(hbs` + + Value + + `); + + assert.dom('.description-list').exists(); + assert.dom('.description-list').hasClass('description-list'); + }); + + test('it renders with complex content in items', async function (assert) { + await render(hbs` + + +
+ Complex + Content +
+
+
+ `); + + assert.dom('.description-list-item dd .test-content').exists(); + assert.dom('.description-list-item dd span').hasText('Complex'); + assert.dom('.description-list-item dd strong').hasText('Content'); + }); +}); diff --git a/ui/admin/tests/integration/components/description-list/item-test.js b/ui/admin/tests/integration/components/description-list/item-test.js new file mode 100644 index 0000000000..a44f908ab9 --- /dev/null +++ b/ui/admin/tests/integration/components/description-list/item-test.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'admin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupIntl } from 'ember-intl/test-support'; + +module('Integration | Component | description-list/item', function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks, 'en-us'); + + test('it renders label (dt)', async function (assert) { + await render(hbs` + + Test Value + + `); + + assert.dom('.description-list-item').exists(); + assert.dom('.description-list-item dt').exists(); + assert.dom('.description-list-item dt').hasText('Test Label'); + }); + + test('it renders yielded content (dd)', async function (assert) { + await render(hbs` + + Test Value Content + + `); + + assert.dom('.description-list-item dd').exists(); + assert.dom('.description-list-item dd').hasText('Test Value Content'); + }); + + test('it applies correct structure', async function (assert) { + await render(hbs` + + Value + + `); + + assert.dom('.description-list-item').exists(); + assert.dom('.description-list-item dt').exists(); + assert.dom('.description-list-item dd').exists(); + // dt should come before dd + const dt = this.element.querySelector('.description-list-item dt'); + const dd = this.element.querySelector('.description-list-item dd'); + assert.ok( + dt.compareDocumentPosition(dd) & Node.DOCUMENT_POSITION_FOLLOWING, + 'dt should come before dd', + ); + }); + + test('it renders with complex content', async function (assert) { + await render(hbs` + +
+ Nested Content +
+
+ `); + + assert.dom('.description-list-item dd .nested').exists(); + assert + .dom('.description-list-item dd .nested span') + .hasText('Nested Content'); + }); + + test('it renders with empty content', async function (assert) { + await render(hbs` + + + `); + + assert.dom('.description-list-item').exists(); + assert.dom('.description-list-item dt').hasText('Empty'); + assert.dom('.description-list-item dd').exists(); + assert.dom('.description-list-item dd').hasText(''); + }); + + test('it renders with component content', async function (assert) { + await render(hbs` + + + + `); + + assert.dom('.description-list-item dd .hds-badge').exists(); + assert.dom('.description-list-item dd .hds-badge').hasText('Active'); + }); +}); diff --git a/ui/admin/tests/integration/components/form/app-token/index-test.js b/ui/admin/tests/integration/components/form/app-token/index-test.js new file mode 100644 index 0000000000..04c50e3a94 --- /dev/null +++ b/ui/admin/tests/integration/components/form/app-token/index-test.js @@ -0,0 +1,237 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'admin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupIntl } from 'ember-intl/test-support'; + +module('Integration | Component | form/app-token', function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks, 'en-us'); + + hooks.beforeEach(function () { + this.model = { + name: 'Test Token', + description: 'Test Description', + status: 'active', + created_by_user_id: 'u_1234567890', + scope: { + id: 's_1234567890', + isGlobal: false, + isOrg: false, + }, + TTL: 86400000, // 1 day in milliseconds + TTS: 172800000, // 2 days in milliseconds + expire_time: new Date('2025-12-31T23:59:59Z'), + created_time: new Date('2025-12-01T10:00:00Z'), + approximate_last_access_time: new Date('2025-12-05T15:30:00Z'), + }; + }); + + test('it renders all form fields', async function (assert) { + await render(hbs``); + + // HDS Form components render disabled inputs - textarea might be disabled differently + assert.dom('input[disabled]').exists('Should have disabled text input'); + assert.dom('textarea[disabled]').exists('Should have disabled textarea'); + + // Check for form field labels + assert.dom('.rose-form').containsText('Name'); + assert.dom('.rose-form').containsText('Description'); + }); + + test('it shows disabled fields correctly', async function (assert) { + await render(hbs``); + + // HDS disabled fields - check for both input and textarea + assert.dom('input[disabled]').exists('Should have disabled input'); + assert.dom('textarea[disabled]').exists('Should have disabled textarea'); + }); + + test('it displays all DescriptionList sections', async function (assert) { + await render(hbs``); + + // About section + assert + .dom('.description-list') + .exists({ count: 3 }, 'Should have 3 description lists'); + + // Check for section titles + const titles = Array.from( + this.element.querySelectorAll('.description-list-title h3'), + ).map((el) => el.textContent.trim()); + + assert.ok( + titles.some((title) => title.includes('About')), + 'Should have About section', + ); + assert.ok( + titles.some((title) => title.includes('Time to live')), + 'Should have TTL section', + ); + assert.ok( + titles.some((title) => title.includes('Time to stale')), + 'Should have TTS section', + ); + }); + + test('statusBadge returns correct color and text for active status', async function (assert) { + this.model.status = 'active'; + await render(hbs``); + + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Active'); + assert.dom('.hds-badge--color-success').exists(); + }); + + test('statusBadge returns correct color and text for expired status', async function (assert) { + this.model.status = 'expired'; + await render(hbs``); + + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Expired'); + assert.dom('.hds-badge--color-critical').exists(); + }); + + test('statusBadge returns correct color and text for revoked status', async function (assert) { + this.model.status = 'revoked'; + await render(hbs``); + + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Revoked'); + assert.dom('.hds-badge--color-critical').exists(); + }); + + test('statusBadge returns correct color and text for stale status', async function (assert) { + this.model.status = 'stale'; + await render(hbs``); + + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Stale'); + assert.dom('.hds-badge--color-critical').exists(); + }); + + test('statusBadge returns correct color and text for unknown status', async function (assert) { + this.model.status = 'unknown'; + await render(hbs``); + + assert.dom('.hds-badge').exists(); + assert.dom('.hds-badge').containsText('Unknown'); + assert.dom('.hds-badge--color-neutral').exists(); + }); + + test('statusBadge handles null status', async function (assert) { + this.model.status = null; + await render(hbs``); + + // Should render but with neutral color + assert.dom('.hds-badge').exists(); + }); + + test('scopeInfo returns correct icon and text for global scope', async function (assert) { + this.model.scope.isGlobal = true; + await render(hbs``); + + assert.dom('[data-test-icon="globe"]').exists(); + }); + + test('scopeInfo returns correct icon and text for org scope', async function (assert) { + this.model.scope.isOrg = true; + await render(hbs``); + + assert.dom('[data-test-icon="org"]').exists(); + }); + + test('scopeInfo returns correct icon and text for project scope', async function (assert) { + this.model.scope.isGlobal = false; + this.model.scope.isOrg = false; + await render(hbs``); + + assert.dom('[data-test-icon="grid"]').exists(); + }); + + test('ttlFormatted converts milliseconds to days correctly', async function (assert) { + // 1 day = 86400000 milliseconds + this.model.TTL = 86400000; + await render(hbs``); + + // The format-day-year helper should display "1 day" + const text = this.element.textContent; + const displaysTTL = text.includes('1') || text.includes('day'); + assert.ok(displaysTTL, 'Should display TTL'); + }); + + test('ttsFormatted converts milliseconds to days correctly', async function (assert) { + // 2 days = 172800000 milliseconds + this.model.TTS = 172800000; + await render(hbs``); + + // The format-day-year helper should display "2 days" + const text = this.element.textContent; + const displaysTTS = text.includes('2') || text.includes('day'); + assert.ok(displaysTTS, 'Should display TTS'); + }); + + test('it displays expiration date', async function (assert) { + await render(hbs``); + + // Should use format-date-long helper to display expiration date + const text = this.element.textContent; + const hasExpiration = text.includes('Dec') && text.includes('2025'); + assert.ok(hasExpiration, 'Should display expiration date'); + }); + + test('it displays created time', async function (assert) { + await render(hbs``); + + const text = this.element.textContent; + const hasCreatedTime = text.includes('Dec') && text.includes('2025'); + assert.ok(hasCreatedTime, 'Should display created time'); + }); + + test('it displays last used time with tooltip', async function (assert) { + await render(hbs``); + + // Should have tooltip icon + assert.dom('[data-test-icon="info"]').exists(); + + // Should have last-used-tooltip class for the value + assert.dom('.last-used-tooltip').exists(); + }); + + test('it renders created by user with link', async function (assert) { + await render(hbs``); + + assert.dom('[data-test-icon="user"]').exists(); + assert.dom('.hds-link-inline').exists(); + assert.dom('.hds-link-inline').containsText('u_1234567890'); + }); + + test('it renders scope with link and icon', async function (assert) { + await render(hbs``); + + // Should have scope icon (grid for project) + assert.dom('[data-test-icon="grid"]').exists(); + assert.dom('.hds-link-inline').exists(); + }); + + test('it handles model with minimal data', async function (assert) { + this.model = { + name: 'Minimal Token', + status: 'active', + scope: {}, + }; + + await render(hbs``); + + // Check that the form renders without crashing + assert.dom('.rose-form').exists(); + assert.dom('.hds-badge').exists(); + // Check for form structure + assert.dom('.description-list').exists({ count: 3 }); + }); +}); From ba23582dab192eb76f70da31e00dee8c535a1a20 Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 5 Dec 2025 23:51:09 +0530 Subject: [PATCH 04/10] Clean up --- ui/admin/app/components/description-list/index.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/admin/app/components/description-list/index.hbs b/ui/admin/app/components/description-list/index.hbs index 1b816b35eb..3d86189a0a 100644 --- a/ui/admin/app/components/description-list/index.hbs +++ b/ui/admin/app/components/description-list/index.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 A reusable component for displaying a bordered container with a title - and a list of label-value pairs in a 3-column grid layout. + and a list of label-value pairs. Usage example in templates: From 7aa42f1d4923d3eb84b5ca3279729004e64ae33c Mon Sep 17 00:00:00 2001 From: sohail-hashicorp <198640889+sohail-hashicorp@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:32:29 +0000 Subject: [PATCH 05/10] Add missing copyright headers --- ui/admin/app/routes/scopes/scope/app-tokens/token.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/admin/app/routes/scopes/scope/app-tokens/token.js b/ui/admin/app/routes/scopes/scope/app-tokens/token.js index b3eabc67b9..13d3e0080a 100644 --- a/ui/admin/app/routes/scopes/scope/app-tokens/token.js +++ b/ui/admin/app/routes/scopes/scope/app-tokens/token.js @@ -1,3 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + import Route from '@ember/routing/route'; import { service } from '@ember/service'; From 013c810bb5f9e44b6c96ec484045068922130754 Mon Sep 17 00:00:00 2001 From: Sohail Date: Sat, 6 Dec 2025 00:31:43 +0530 Subject: [PATCH 06/10] fix the linting issue is scss --- ui/admin/app/styles/app.scss | 84 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 41d14ce66d..aba9620487 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -47,6 +47,47 @@ &:not(.full-width) { width: 66%; } + + // App token details page - description list styling + .description-list { + display: flex; + flex-wrap: wrap; + padding: sizing.rems(m) sizing.rems(l); // 16px top/bottom, 24px left/right + border: sizing.rems(xxxxs) solid var(--token-color-border-primary); + border-radius: var(--token-border-radius-medium); + margin-bottom: sizing.rems(xl); + + .description-list-title { + width: 100%; + margin-bottom: sizing.rems(l); // 24px gap between title and items + + h3 { + margin: 0; + } + } + + .description-list-item { + display: flex; + flex-direction: column; + gap: sizing.rems(xs); // 8px vertical gap between dt and dd + margin-right: sizing.rems(l); // 24px horizontal gap between items + } + + dt { + font-size: 14px; + font-weight: var(--token-typography-font-weight-semibold); + + &.label-with-tooltip { + display: inline-flex; + align-items: center; + gap: sizing.rems(xs); // 8px gap between label and icon + } + } + + dd { + font-size: var(--token-typography-body-200-font-size); // size/medium + } + } } .rose-header { @@ -1080,46 +1121,3 @@ text-decoration-style: solid; cursor: pointer; } - -// App token details page - description list styling -.rose-form { - .description-list { - display: flex; - flex-wrap: wrap; - padding: sizing.rems(m) sizing.rems(l); // 16px top/bottom, 24px left/right - border: sizing.rems(xxxxs) solid var(--token-color-border-primary); - border-radius: var(--token-border-radius-medium); - margin-bottom: sizing.rems(xl); - - .description-list-title { - width: 100%; - margin-bottom: sizing.rems(l); // 24px gap between title and items - - h3 { - margin: 0; - } - } - - .description-list-item { - display: flex; - flex-direction: column; - gap: sizing.rems(xs); // 8px vertical gap between dt and dd - margin-right: sizing.rems(l); // 24px horizontal gap between items - } - - dt { - font-size: 14px; - font-weight: var(--token-typography-font-weight-semibold); - - &.label-with-tooltip { - display: inline-flex; - align-items: center; - gap: sizing.rems(xs); // 8px gap between label and icon - } - } - - dd { - font-size: var(--token-typography-body-200-font-size); // size/medium - } - } -} From 05cf8aff3c688b052846e9db0a08bda309180902 Mon Sep 17 00:00:00 2001 From: Sohail Date: Sat, 6 Dec 2025 00:45:54 +0530 Subject: [PATCH 07/10] fixed the linting again --- .../components/description-list/index-test.js | 56 ++++++++++++++----- .../components/description-list/item-test.js | 39 +++++++++---- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/ui/admin/tests/integration/components/description-list/index-test.js b/ui/admin/tests/integration/components/description-list/index-test.js index cb05b775a7..6b8f11be21 100644 --- a/ui/admin/tests/integration/components/description-list/index-test.js +++ b/ui/admin/tests/integration/components/description-list/index-test.js @@ -14,10 +14,16 @@ module('Integration | Component | description-list', function (hooks) { setupIntl(hooks, 'en-us'); test('it renders with title', async function (assert) { + this.set('title', 'Section Title'); + this.set('label1', 'Label 1'); + this.set('value1', 'Value 1'); + this.set('label2', 'Label 2'); + this.set('value2', 'Value 2'); + await render(hbs` - - Value 1 - Value 2 + + {{this.value1}} + {{this.value2}} `); @@ -27,9 +33,12 @@ module('Integration | Component | description-list', function (hooks) { }); test('it renders without title', async function (assert) { + this.set('label', 'Label 1'); + this.set('value', 'Value 1'); + await render(hbs` - Value 1 + {{this.value}} `); @@ -38,9 +47,12 @@ module('Integration | Component | description-list', function (hooks) { }); test('it yields Item component correctly', async function (assert) { + this.set('label', 'Test Label'); + this.set('value', 'Test Value'); + await render(hbs` - Test Value + {{this.value}} `); @@ -51,11 +63,19 @@ module('Integration | Component | description-list', function (hooks) { }); test('it renders multiple items', async function (assert) { + this.set('title', 'Multiple Items'); + this.set('label1', 'Item 1'); + this.set('value1', 'Value 1'); + this.set('label2', 'Item 2'); + this.set('value2', 'Value 2'); + this.set('label3', 'Item 3'); + this.set('value3', 'Value 3'); + await render(hbs` - - Value 1 - Value 2 - Value 3 + + {{this.value1}} + {{this.value2}} + {{this.value3}} `); @@ -64,9 +84,13 @@ module('Integration | Component | description-list', function (hooks) { }); test('it applies correct CSS classes', async function (assert) { + this.set('title', 'Test'); + this.set('label', 'Label'); + this.set('value', 'Value'); + await render(hbs` - - Value + + {{this.value}} `); @@ -75,12 +99,16 @@ module('Integration | Component | description-list', function (hooks) { }); test('it renders with complex content in items', async function (assert) { + this.set('label', 'Complex Item'); + this.set('text1', 'Complex'); + this.set('text2', 'Content'); + await render(hbs` - +
- Complex - Content + {{this.text1}} + {{this.text2}}
diff --git a/ui/admin/tests/integration/components/description-list/item-test.js b/ui/admin/tests/integration/components/description-list/item-test.js index a44f908ab9..2b417f1dc7 100644 --- a/ui/admin/tests/integration/components/description-list/item-test.js +++ b/ui/admin/tests/integration/components/description-list/item-test.js @@ -14,9 +14,12 @@ module('Integration | Component | description-list/item', function (hooks) { setupIntl(hooks, 'en-us'); test('it renders label (dt)', async function (assert) { + this.set('label', 'Test Label'); + this.set('value', 'Test Value'); + await render(hbs` - - Test Value + + {{this.value}} `); @@ -26,9 +29,12 @@ module('Integration | Component | description-list/item', function (hooks) { }); test('it renders yielded content (dd)', async function (assert) { + this.set('label', 'Label'); + this.set('value', 'Test Value Content'); + await render(hbs` - - Test Value Content + + {{this.value}} `); @@ -37,9 +43,12 @@ module('Integration | Component | description-list/item', function (hooks) { }); test('it applies correct structure', async function (assert) { + this.set('label', 'Structure Test'); + this.set('value', 'Value'); + await render(hbs` - - Value + + {{this.value}} `); @@ -56,10 +65,13 @@ module('Integration | Component | description-list/item', function (hooks) { }); test('it renders with complex content', async function (assert) { + this.set('label', 'Complex'); + this.set('nestedText', 'Nested Content'); + await render(hbs` - +
- Nested Content + {{this.nestedText}}
`); @@ -71,8 +83,10 @@ module('Integration | Component | description-list/item', function (hooks) { }); test('it renders with empty content', async function (assert) { + this.set('label', 'Empty'); + await render(hbs` - + `); @@ -83,9 +97,12 @@ module('Integration | Component | description-list/item', function (hooks) { }); test('it renders with component content', async function (assert) { + this.set('label', 'With Component'); + this.set('badgeText', 'Active'); + await render(hbs` - - + + `); From 4a372322af6c7833d01723210cb8507328d30f75 Mon Sep 17 00:00:00 2001 From: Sohail Date: Mon, 8 Dec 2025 16:34:47 +0530 Subject: [PATCH 08/10] Fixed PR comments --- addons/core/addon/helpers/format-date-long.js | 28 ------ addons/core/app/helpers/format-date-long.js | 6 -- .../helpers/format-date-long-test.js | 95 ------------------- .../app/components/form/app-token/index.hbs | 16 ++-- ui/admin/app/router.js | 2 +- .../app-tokens/{token.js => app-token.js} | 0 ui/admin/app/styles/app.scss | 9 -- .../app-tokens/{token.hbs => app-token.hbs} | 6 +- .../app-tokens/{token => app-token}/index.hbs | 0 .../{token => app-token}/permissions.hbs | 0 .../scopes/scope/app-tokens/index.hbs | 2 +- .../tests/acceptance/app-tokens/read-test.js | 73 +++++++------- .../components/form/app-token/index-test.js | 9 +- 13 files changed, 56 insertions(+), 190 deletions(-) delete mode 100644 addons/core/addon/helpers/format-date-long.js delete mode 100644 addons/core/app/helpers/format-date-long.js delete mode 100644 addons/core/tests/integration/helpers/format-date-long-test.js rename ui/admin/app/routes/scopes/scope/app-tokens/{token.js => app-token.js} (100%) rename ui/admin/app/templates/scopes/scope/app-tokens/{token.hbs => app-token.hbs} (84%) rename ui/admin/app/templates/scopes/scope/app-tokens/{token => app-token}/index.hbs (100%) rename ui/admin/app/templates/scopes/scope/app-tokens/{token => app-token}/permissions.hbs (100%) diff --git a/addons/core/addon/helpers/format-date-long.js b/addons/core/addon/helpers/format-date-long.js deleted file mode 100644 index 3683e31ca3..0000000000 --- a/addons/core/addon/helpers/format-date-long.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { helper } from '@ember/component/helper'; - -/** - * Takes a `Date` instance and returns a string formatted in long format - * e.g. "Oct 27, 2025, 3:30 PM PST" - */ -export default helper(function formatDateLong([date] /*, hash*/) { - if (!date) return ''; - - const formatted = date.toLocaleString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - timeZone: 'America/Los_Angeles', - timeZoneName: 'short', - }); - - // Replace "at" with "," to match the design - return formatted.replace(' at ', ', '); -}); diff --git a/addons/core/app/helpers/format-date-long.js b/addons/core/app/helpers/format-date-long.js deleted file mode 100644 index dbda0e67b6..0000000000 --- a/addons/core/app/helpers/format-date-long.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export { default } from 'core/helpers/format-date-long'; diff --git a/addons/core/tests/integration/helpers/format-date-long-test.js b/addons/core/tests/integration/helpers/format-date-long-test.js deleted file mode 100644 index a500facb67..0000000000 --- a/addons/core/tests/integration/helpers/format-date-long-test.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'dummy/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Helper | format-date-long', function (hooks) { - setupRenderingTest(hooks); - - test('it renders date in long format', async function (assert) { - // Create a specific date: Oct 27, 2025, 3:30 PM PST - const testDate = new Date('2025-10-27T15:30:00-07:00'); - this.set('date', testDate); - - await render(hbs`{{format-date-long this.date}}`); - - // Should render as "Oct 27, 2025, 3:30 PM PDT" or "Oct 27, 2025, 3:30 PM PST" - // depending on daylight saving time - const text = this.element.textContent.trim(); - assert.ok( - text.includes('Oct 27, 2025'), - 'Date should include month, day, and year', - ); - assert.ok(text.includes('3:30 PM'), 'Date should include time'); - // Check for timezone - should be either PDT or PST - const hasPacificTimezone = text.includes('PDT') || text.includes('PST'); - assert.ok(hasPacificTimezone, 'Date should include Pacific timezone'); - }); - - test('it returns empty string for null date', async function (assert) { - this.set('date', null); - - await render(hbs`{{format-date-long this.date}}`); - - assert.strictEqual(this.element.textContent.trim(), ''); - }); - - test('it returns empty string for undefined date', async function (assert) { - this.set('date', undefined); - - await render(hbs`{{format-date-long this.date}}`); - - assert.strictEqual(this.element.textContent.trim(), ''); - }); - - test('it formats date with Pacific timezone', async function (assert) { - const testDate = new Date('2025-12-05T12:00:00-08:00'); - this.set('date', testDate); - - await render(hbs`{{format-date-long this.date}}`); - - const text = this.element.textContent.trim(); - // Should include PST or PDT based on daylight saving - const hasPacificTimezone = text.includes('PST') || text.includes('PDT'); - assert.ok(hasPacificTimezone, 'Should include Pacific timezone'); - }); - - test('it replaces "at" with comma in the format', async function (assert) { - const testDate = new Date('2025-10-27T15:30:00-07:00'); - this.set('date', testDate); - - await render(hbs`{{format-date-long this.date}}`); - - const text = this.element.textContent.trim(); - // Should use comma separator, not " at " - assert.notOk(text.includes(' at '), 'Should not contain " at "'); - // Format should be like "Oct 27, 2025, 3:30 PM PDT" - const parts = text.split(','); - assert.strictEqual(parts.length, 3, 'Should have 3 comma-separated parts'); - }); - - test('it formats different months correctly', async function (assert) { - assert.expect(3); // Expect 3 assertions (one for each month) - - const dates = [ - { date: new Date('2025-01-15T10:00:00-08:00'), month: 'Jan' }, - { date: new Date('2025-06-20T14:30:00-07:00'), month: 'Jun' }, - { date: new Date('2025-12-31T23:59:00-08:00'), month: 'Dec' }, - ]; - - for (const { date, month } of dates) { - this.set('date', date); - await render(hbs`{{format-date-long this.date}}`); - const text = this.element.textContent.trim(); - assert.ok( - text.includes(month), - `Should correctly format month as ${month}`, - ); - } - }); -}); diff --git a/ui/admin/app/components/form/app-token/index.hbs b/ui/admin/app/components/form/app-token/index.hbs index 2f7ad7f267..7460cb8ff8 100644 --- a/ui/admin/app/components/form/app-token/index.hbs +++ b/ui/admin/app/components/form/app-token/index.hbs @@ -62,14 +62,14 @@ {{#if @model.expire_time}} - {{format-date-long @model.expire_time}} + {{else}} {{t 'labels.none'}} {{/if}} {{#if @model.created_time}} - {{format-date-long @model.created_time}} + {{else}} {{t 'labels.none'}} {{/if}} @@ -95,14 +95,10 @@
{{#if @model.approximate_last_access_time}} - - {{relative-datetime-live @model.approximate_last_access_time}} - + {{else}} {{t 'labels.none'}} {{/if}} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index 9e41443e3b..cbd0480475 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -188,7 +188,7 @@ Router.map(function () { this.route('policy', { path: ':policy_id' }, function () {}); }); this.route('app-tokens', function () { - this.route('token', { path: ':token_id' }, function () { + this.route('app-token', { path: ':token_id' }, function () { this.route('permissions'); }); }); diff --git a/ui/admin/app/routes/scopes/scope/app-tokens/token.js b/ui/admin/app/routes/scopes/scope/app-tokens/app-token.js similarity index 100% rename from ui/admin/app/routes/scopes/scope/app-tokens/token.js rename to ui/admin/app/routes/scopes/scope/app-tokens/app-token.js diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index aba9620487..255ed308b8 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -1112,12 +1112,3 @@ .grant-scope-selection-alert-list { list-style: disc; } - -// Styling for last used tooltip with underlined text -.last-used-tooltip { - font-weight: 500; - font-size: var(--token-typography-body-200-font-size); // size/medium - text-decoration: underline; - text-decoration-style: solid; - cursor: pointer; -} diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/token.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/app-token.hbs similarity index 84% rename from ui/admin/app/templates/scopes/scope/app-tokens/token.hbs rename to ui/admin/app/templates/scopes/scope/app-tokens/app-token.hbs index 8c77c6e2c7..e721f70e17 100644 --- a/ui/admin/app/templates/scopes/scope/app-tokens/token.hbs +++ b/ui/admin/app/templates/scopes/scope/app-tokens/app-token.hbs @@ -6,7 +6,7 @@ {{page-title @model.displayName}} @@ -31,10 +31,10 @@ - + {{t 'titles.details'}} - + {{t 'resources.app-token.tabs.permissions'}} diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs similarity index 100% rename from ui/admin/app/templates/scopes/scope/app-tokens/token/index.hbs rename to ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/app-token/permissions.hbs similarity index 100% rename from ui/admin/app/templates/scopes/scope/app-tokens/token/permissions.hbs rename to ui/admin/app/templates/scopes/scope/app-tokens/app-token/permissions.hbs diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs index f35798b134..26a087cfa1 100644 --- a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs +++ b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs @@ -85,7 +85,7 @@ diff --git a/ui/admin/tests/acceptance/app-tokens/read-test.js b/ui/admin/tests/acceptance/app-tokens/read-test.js index 73a04ce14a..52d3bae1cc 100644 --- a/ui/admin/tests/acceptance/app-tokens/read-test.js +++ b/ui/admin/tests/acceptance/app-tokens/read-test.js @@ -59,11 +59,10 @@ module('Acceptance | app-tokens | read', function (hooks) { }, }); - await visit(urls.orgScope); - await click(commonSelectors.HREF(urls.appTokens)); - await click(commonSelectors.HREF(urls.appToken)); + await visit(urls.appToken); assert.strictEqual(currentURL(), urls.appToken); + assert.dom('.hds-page-header__title').containsText('App Tokens'); }); test('app token detail page displays correct title and breadcrumbs', async function (assert) { @@ -78,7 +77,6 @@ module('Acceptance | app-tokens | read', function (hooks) { await visit(urls.appToken); // Check page header title - assert.dom('.hds-page-header__title').exists(); assert.dom('.hds-page-header__title').containsText('App Tokens'); // Check breadcrumbs @@ -97,7 +95,6 @@ module('Acceptance | app-tokens | read', function (hooks) { await visit(urls.appToken); // Check for copy snippet with token ID - assert.dom('.hds-copy-snippet').exists(); assert .dom('.hds-copy-snippet') .containsText(instances.appToken.id.substring(0, 10)); @@ -175,21 +172,48 @@ module('Acceptance | app-tokens | read', function (hooks) { assert.dom('.hds-badge').exists(); }); - test('app token page displays status badge', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - enabled: false, - }, + test.each( + 'app token page displays correct status badge', + { + active: { + status: 'active', + expectedText: 'Active', }, - }); + expired: { + status: 'expired', + expectedText: 'Expired', + }, + revoked: { + status: 'revoked', + expectedText: 'Revoked', + }, + stale: { + status: 'stale', + expectedText: 'Stale', + }, + unknown: { + status: 'unknown', + expectedText: 'Unknown', + }, + }, + async function (assert, { status, expectedText }) { + setRunOptions({ + rules: { + 'color-contrast': { + enabled: false, + }, + }, + }); - await visit(urls.appToken); + // Update the token status + instances.appToken.update({ status }); - // Check for status badge - assert.dom('.hds-badge').exists(); - assert.dom('.hds-badge').containsText('Active'); - }); + await visit(urls.appToken); + + // Check for status badge with correct text + assert.dom('.hds-badge').containsText(expectedText); + }, + ); test('Permissions tab renders placeholder', async function (assert) { setRunOptions({ @@ -205,19 +229,4 @@ module('Acceptance | app-tokens | read', function (hooks) { // Permissions tab should have a form (even if empty for now) assert.dom('.rose-form').exists(); }); - - test('page title is set to app token display name', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - enabled: false, - }, - }, - }); - - await visit(urls.appToken); - - // Check that page title includes token name - assert.dom('.hds-page-header__title').exists(); - }); }); diff --git a/ui/admin/tests/integration/components/form/app-token/index-test.js b/ui/admin/tests/integration/components/form/app-token/index-test.js index 04c50e3a94..19ad4a36c9 100644 --- a/ui/admin/tests/integration/components/form/app-token/index-test.js +++ b/ui/admin/tests/integration/components/form/app-token/index-test.js @@ -179,7 +179,6 @@ module('Integration | Component | form/app-token', function (hooks) { test('it displays expiration date', async function (assert) { await render(hbs``); - // Should use format-date-long helper to display expiration date const text = this.element.textContent; const hasExpiration = text.includes('Dec') && text.includes('2025'); assert.ok(hasExpiration, 'Should display expiration date'); @@ -193,14 +192,14 @@ module('Integration | Component | form/app-token', function (hooks) { assert.ok(hasCreatedTime, 'Should display created time'); }); - test('it displays last used time with tooltip', async function (assert) { + test('it displays last used time', async function (assert) { await render(hbs``); - // Should have tooltip icon + // Should have tooltip icon for the label assert.dom('[data-test-icon="info"]').exists(); - // Should have last-used-tooltip class for the value - assert.dom('.last-used-tooltip').exists(); + // Use HDS Time component with relative format + assert.dom('.hds-time').exists(); }); test('it renders created by user with link', async function (assert) { From 5426abc7beed2cbcad32cd95be97c9d686c3ce29 Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 12 Dec 2025 18:04:09 +0530 Subject: [PATCH 09/10] fixed PR comments --- addons/core/translations/resources/en-us.yaml | 14 ++-- .../app/components/description-list/item.hbs | 6 +- .../app/components/form/app-token/index.hbs | 40 ++++++----- .../app/components/form/app-token/index.js | 26 +++---- .../scopes/scope/app-tokens/index.js | 18 ++--- .../scopes/scope/app-tokens/app-token.js | 3 +- ui/admin/app/styles/app.scss | 3 - .../scopes/scope/app-tokens/index.hbs | 1 - .../tests/acceptance/app-tokens/read-test.js | 4 +- .../components/description-list/index-test.js | 27 ++----- .../components/description-list/item-test.js | 71 ------------------- .../components/form/app-token/index-test.js | 19 ++--- 12 files changed, 73 insertions(+), 159 deletions(-) diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 77c22d3431..d6b77a872c 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -1242,10 +1242,6 @@ app-token: permissions: Permissions about: title: About this token - ttl: - title: Time to live (TTL) - tts: - title: Time to stale (TTS) form: name: help: Name to identify this app token @@ -1262,16 +1258,22 @@ app-token: scope: label: Scope ttl: - label: TTL + label: + 0: Time to live (TTL) + 1: TTL expiration_date: label: Expiration date created_time: label: Created at tts: - label: TTS + label: + 0: Time to stale (TTS) + 1: TTS last_used: label: Last used tooltip: This value is an approximate value, not an exact value. + permissions: + label: Permissions status: unknown: Unknown active: Active diff --git a/ui/admin/app/components/description-list/item.hbs b/ui/admin/app/components/description-list/item.hbs index f8d48be562..0bf008577d 100644 --- a/ui/admin/app/components/description-list/item.hbs +++ b/ui/admin/app/components/description-list/item.hbs @@ -11,7 +11,11 @@ }}
-
{{@label}}
+
+ + {{@label}} + +
{{yield}}
diff --git a/ui/admin/app/components/form/app-token/index.hbs b/ui/admin/app/components/form/app-token/index.hbs index 7460cb8ff8..0ac3e3f004 100644 --- a/ui/admin/app/components/form/app-token/index.hbs +++ b/ui/admin/app/components/form/app-token/index.hbs @@ -7,7 +7,7 @@ {{t 'form.name.label'}} @@ -17,7 +17,7 @@ {{t 'form.description.label'}} @@ -35,25 +35,29 @@
{{! TODO: Check where to navigate the user for global/org/proj levels }} -
- - {{@model.created_by_user_id}} -
+ + {{@model.created_by_user_id}} +
-
- - - {{this.scopeInfo.text}} - -
+ + {{this.scopeInfo.text}} +
- - + + {{#if this.ttlFormatted}} {{format-day-year this.ttlFormatted}} {{else}} @@ -76,8 +80,8 @@ - - + + {{#if this.ttsFormatted}} {{format-day-year this.ttsFormatted}} {{else}} diff --git a/ui/admin/app/components/form/app-token/index.js b/ui/admin/app/components/form/app-token/index.js index 837ab81c15..77fb48c8ae 100644 --- a/ui/admin/app/components/form/app-token/index.js +++ b/ui/admin/app/components/form/app-token/index.js @@ -9,6 +9,14 @@ import { service } from '@ember/service'; export default class FormAppTokenComponent extends Component { @service intl; + statusConfig = { + active: { color: 'success' }, + expired: { color: 'critical' }, + revoked: { color: 'critical' }, + stale: { color: 'critical' }, + unknown: { color: 'neutral' }, + }; + /** * Returns status badge configuration for app tokens * @returns {object} @@ -17,15 +25,7 @@ export default class FormAppTokenComponent extends Component { const status = this.args.model?.status; if (!status) return { text: '', color: 'neutral' }; - const statusConfig = { - active: { color: 'success' }, - expired: { color: 'critical' }, - revoked: { color: 'critical' }, - stale: { color: 'critical' }, - unknown: { color: 'neutral' }, - }; - - const config = statusConfig[status] || { color: 'neutral' }; + const config = this.statusConfig[status] || { color: 'neutral' }; return { text: this.intl.t(`resources.app-token.status.${status}`), color: config.color, @@ -33,18 +33,16 @@ export default class FormAppTokenComponent extends Component { } /** - * Returns scope information (icon, text, route) based on scope type + * Returns scope information (icon, text) based on scope type * @returns {object} */ get scopeInfo() { - const scope = this.args.model?.scope; - if (!scope) return { icon: 'globe', text: '', route: '#' }; + const scope = this.args.model.scope; if (scope.isGlobal) { return { icon: 'globe', text: this.intl.t('resources.scope.types.global'), - route: '#', // TODO: Add proper route }; } @@ -52,14 +50,12 @@ export default class FormAppTokenComponent extends Component { return { icon: 'org', text: this.intl.t('resources.scope.types.org'), - route: '#', // TODO: Add proper route }; } return { icon: 'grid', text: this.intl.t('resources.scope.types.project'), - route: '#', // TODO: Add proper route }; } diff --git a/ui/admin/app/controllers/scopes/scope/app-tokens/index.js b/ui/admin/app/controllers/scopes/scope/app-tokens/index.js index 76d0d89de4..8513f3491b 100644 --- a/ui/admin/app/controllers/scopes/scope/app-tokens/index.js +++ b/ui/admin/app/controllers/scopes/scope/app-tokens/index.js @@ -32,6 +32,14 @@ export default class ScopesScopeAppTokensIndexController extends Controller { @tracked sortDirection; @tracked statuses = []; + statusConfig = { + active: { color: 'success' }, + expired: { color: 'critical' }, + revoked: { color: 'critical' }, + stale: { color: 'critical' }, + unknown: { color: 'neutral' }, + }; + /** * Status options for filtering */ @@ -49,15 +57,7 @@ export default class ScopesScopeAppTokensIndexController extends Controller { */ @action getStatusBadge(status) { - const statusConfig = { - active: { color: 'success' }, - expired: { color: 'critical' }, - revoked: { color: 'critical' }, - stale: { color: 'critical' }, - unknown: { color: 'neutral' }, - }; - - const config = statusConfig[status] || { color: 'neutral' }; + const config = this.statusConfig[status] || { color: 'neutral' }; return { text: this.intl.t(`resources.app-token.status.${status}`), color: config.color, diff --git a/ui/admin/app/routes/scopes/scope/app-tokens/app-token.js b/ui/admin/app/routes/scopes/scope/app-tokens/app-token.js index 13d3e0080a..29c0b19c33 100644 --- a/ui/admin/app/routes/scopes/scope/app-tokens/app-token.js +++ b/ui/admin/app/routes/scopes/scope/app-tokens/app-token.js @@ -6,12 +6,13 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -export default class ScopesScopeAppTokensTokenRoute extends Route { +export default class ScopesScopeAppTokensAppTokenRoute extends Route { @service store; async model(params) { // Fetch the app-token details using the token id and scope id return this.store.findRecord('app-token', params.token_id, { + reload: true, adapterOptions: { scope_id: params.scope_id }, }); } diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 255ed308b8..55aed03129 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -74,9 +74,6 @@ } dt { - font-size: 14px; - font-weight: var(--token-typography-font-weight-semibold); - &.label-with-tooltip { display: inline-flex; align-items: center; diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs index 26a087cfa1..e747047763 100644 --- a/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs +++ b/ui/admin/app/templates/scopes/scope/app-tokens/index.hbs @@ -87,7 +87,6 @@ {{token.name}} diff --git a/ui/admin/tests/acceptance/app-tokens/read-test.js b/ui/admin/tests/acceptance/app-tokens/read-test.js index 52d3bae1cc..d7c7ebfbbd 100644 --- a/ui/admin/tests/acceptance/app-tokens/read-test.js +++ b/ui/admin/tests/acceptance/app-tokens/read-test.js @@ -162,8 +162,8 @@ module('Acceptance | app-tokens | read', function (hooks) { assert.dom('.rose-form').containsText('Time to live'); assert.dom('.rose-form').containsText('Time to stale'); - // Check for disabled inputs (HDS form fields render as disabled, not readonly) - assert.dom('input[disabled]').exists('Should have disabled fields'); + // Check for readonly inputs for accessibility + assert.dom('input[readonly]').exists('Should have readonly fields'); // Check for description lists (About, TTL, TTS sections) assert.dom('.description-list').exists({ count: 3 }); diff --git a/ui/admin/tests/integration/components/description-list/index-test.js b/ui/admin/tests/integration/components/description-list/index-test.js index 6b8f11be21..a76ea0b305 100644 --- a/ui/admin/tests/integration/components/description-list/index-test.js +++ b/ui/admin/tests/integration/components/description-list/index-test.js @@ -27,8 +27,6 @@ module('Integration | Component | description-list', function (hooks) { `); - assert.dom('.description-list').exists(); - assert.dom('.description-list-title').exists(); assert.dom('.description-list-title h3').hasText('Section Title'); }); @@ -56,8 +54,6 @@ module('Integration | Component | description-list', function (hooks) { `); - assert.dom('.description-list').exists(); - assert.dom('.description-list-item').exists(); assert.dom('.description-list-item dt').hasText('Test Label'); assert.dom('.description-list-item dd').hasText('Test Value'); }); @@ -79,23 +75,15 @@ module('Integration | Component | description-list', function (hooks) { `); - assert.dom('.description-list').exists(); assert.dom('.description-list-item').exists({ count: 3 }); - }); - - test('it applies correct CSS classes', async function (assert) { - this.set('title', 'Test'); - this.set('label', 'Label'); - this.set('value', 'Value'); - - await render(hbs` - - {{this.value}} - - `); - assert.dom('.description-list').exists(); - assert.dom('.description-list').hasClass('description-list'); + const items = this.element.querySelectorAll('.description-list-item'); + assert.dom(items[0].querySelector('dt')).hasText('Item 1'); + assert.dom(items[0].querySelector('dd')).hasText('Value 1'); + assert.dom(items[1].querySelector('dt')).hasText('Item 2'); + assert.dom(items[1].querySelector('dd')).hasText('Value 2'); + assert.dom(items[2].querySelector('dt')).hasText('Item 3'); + assert.dom(items[2].querySelector('dd')).hasText('Value 3'); }); test('it renders with complex content in items', async function (assert) { @@ -114,7 +102,6 @@ module('Integration | Component | description-list', function (hooks) {
`); - assert.dom('.description-list-item dd .test-content').exists(); assert.dom('.description-list-item dd span').hasText('Complex'); assert.dom('.description-list-item dd strong').hasText('Content'); }); diff --git a/ui/admin/tests/integration/components/description-list/item-test.js b/ui/admin/tests/integration/components/description-list/item-test.js index 2b417f1dc7..246844c743 100644 --- a/ui/admin/tests/integration/components/description-list/item-test.js +++ b/ui/admin/tests/integration/components/description-list/item-test.js @@ -23,8 +23,6 @@ module('Integration | Component | description-list/item', function (hooks) { `); - assert.dom('.description-list-item').exists(); - assert.dom('.description-list-item dt').exists(); assert.dom('.description-list-item dt').hasText('Test Label'); }); @@ -38,75 +36,6 @@ module('Integration | Component | description-list/item', function (hooks) { `); - assert.dom('.description-list-item dd').exists(); assert.dom('.description-list-item dd').hasText('Test Value Content'); }); - - test('it applies correct structure', async function (assert) { - this.set('label', 'Structure Test'); - this.set('value', 'Value'); - - await render(hbs` - - {{this.value}} - - `); - - assert.dom('.description-list-item').exists(); - assert.dom('.description-list-item dt').exists(); - assert.dom('.description-list-item dd').exists(); - // dt should come before dd - const dt = this.element.querySelector('.description-list-item dt'); - const dd = this.element.querySelector('.description-list-item dd'); - assert.ok( - dt.compareDocumentPosition(dd) & Node.DOCUMENT_POSITION_FOLLOWING, - 'dt should come before dd', - ); - }); - - test('it renders with complex content', async function (assert) { - this.set('label', 'Complex'); - this.set('nestedText', 'Nested Content'); - - await render(hbs` - -
- {{this.nestedText}} -
-
- `); - - assert.dom('.description-list-item dd .nested').exists(); - assert - .dom('.description-list-item dd .nested span') - .hasText('Nested Content'); - }); - - test('it renders with empty content', async function (assert) { - this.set('label', 'Empty'); - - await render(hbs` - - - `); - - assert.dom('.description-list-item').exists(); - assert.dom('.description-list-item dt').hasText('Empty'); - assert.dom('.description-list-item dd').exists(); - assert.dom('.description-list-item dd').hasText(''); - }); - - test('it renders with component content', async function (assert) { - this.set('label', 'With Component'); - this.set('badgeText', 'Active'); - - await render(hbs` - - - - `); - - assert.dom('.description-list-item dd .hds-badge').exists(); - assert.dom('.description-list-item dd .hds-badge').hasText('Active'); - }); }); diff --git a/ui/admin/tests/integration/components/form/app-token/index-test.js b/ui/admin/tests/integration/components/form/app-token/index-test.js index 19ad4a36c9..a47213c202 100644 --- a/ui/admin/tests/integration/components/form/app-token/index-test.js +++ b/ui/admin/tests/integration/components/form/app-token/index-test.js @@ -35,21 +35,21 @@ module('Integration | Component | form/app-token', function (hooks) { test('it renders all form fields', async function (assert) { await render(hbs``); - // HDS Form components render disabled inputs - textarea might be disabled differently - assert.dom('input[disabled]').exists('Should have disabled text input'); - assert.dom('textarea[disabled]').exists('Should have disabled textarea'); + // HDS Form components render readonly inputs for accessibility + assert.dom('input[readonly]').exists('Should have readonly text input'); + assert.dom('textarea[readonly]').exists('Should have readonly textarea'); // Check for form field labels assert.dom('.rose-form').containsText('Name'); assert.dom('.rose-form').containsText('Description'); }); - test('it shows disabled fields correctly', async function (assert) { + test('it shows readonly fields correctly', async function (assert) { await render(hbs``); - // HDS disabled fields - check for both input and textarea - assert.dom('input[disabled]').exists('Should have disabled input'); - assert.dom('textarea[disabled]').exists('Should have disabled textarea'); + // HDS readonly fields - check for both input and textarea + assert.dom('input[readonly]').exists('Should have readonly input'); + assert.dom('textarea[readonly]').exists('Should have readonly textarea'); }); test('it displays all DescriptionList sections', async function (assert) { @@ -83,7 +83,6 @@ module('Integration | Component | form/app-token', function (hooks) { this.model.status = 'active'; await render(hbs``); - assert.dom('.hds-badge').exists(); assert.dom('.hds-badge').containsText('Active'); assert.dom('.hds-badge--color-success').exists(); }); @@ -92,7 +91,6 @@ module('Integration | Component | form/app-token', function (hooks) { this.model.status = 'expired'; await render(hbs``); - assert.dom('.hds-badge').exists(); assert.dom('.hds-badge').containsText('Expired'); assert.dom('.hds-badge--color-critical').exists(); }); @@ -101,7 +99,6 @@ module('Integration | Component | form/app-token', function (hooks) { this.model.status = 'revoked'; await render(hbs``); - assert.dom('.hds-badge').exists(); assert.dom('.hds-badge').containsText('Revoked'); assert.dom('.hds-badge--color-critical').exists(); }); @@ -110,7 +107,6 @@ module('Integration | Component | form/app-token', function (hooks) { this.model.status = 'stale'; await render(hbs``); - assert.dom('.hds-badge').exists(); assert.dom('.hds-badge').containsText('Stale'); assert.dom('.hds-badge--color-critical').exists(); }); @@ -119,7 +115,6 @@ module('Integration | Component | form/app-token', function (hooks) { this.model.status = 'unknown'; await render(hbs``); - assert.dom('.hds-badge').exists(); assert.dom('.hds-badge').containsText('Unknown'); assert.dom('.hds-badge--color-neutral').exists(); }); From 3153e3534b4c3b8207695ce841f0e223bc9b7b26 Mon Sep 17 00:00:00 2001 From: Sohail Date: Fri, 12 Dec 2025 18:40:56 +0530 Subject: [PATCH 10/10] Add read-only form component for app token details --- .../form/app-token/{ => read}/index.hbs | 1 - .../form/app-token/{ => read}/index.js | 2 +- .../scope/app-tokens/app-token/index.hbs | 2 +- .../form/app-token/{ => read}/index-test.js | 42 +++++++++---------- 4 files changed, 23 insertions(+), 24 deletions(-) rename ui/admin/app/components/form/app-token/{ => read}/index.hbs (99%) rename ui/admin/app/components/form/app-token/{ => read}/index.js (96%) rename ui/admin/tests/integration/components/form/app-token/{ => read}/index-test.js (82%) diff --git a/ui/admin/app/components/form/app-token/index.hbs b/ui/admin/app/components/form/app-token/read/index.hbs similarity index 99% rename from ui/admin/app/components/form/app-token/index.hbs rename to ui/admin/app/components/form/app-token/read/index.hbs index 0ac3e3f004..300776f386 100644 --- a/ui/admin/app/components/form/app-token/index.hbs +++ b/ui/admin/app/components/form/app-token/read/index.hbs @@ -16,7 +16,6 @@ diff --git a/ui/admin/app/components/form/app-token/index.js b/ui/admin/app/components/form/app-token/read/index.js similarity index 96% rename from ui/admin/app/components/form/app-token/index.js rename to ui/admin/app/components/form/app-token/read/index.js index 77fb48c8ae..85b5248cbf 100644 --- a/ui/admin/app/components/form/app-token/index.js +++ b/ui/admin/app/components/form/app-token/read/index.js @@ -6,7 +6,7 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -export default class FormAppTokenComponent extends Component { +export default class FormAppTokenReadComponent extends Component { @service intl; statusConfig = { diff --git a/ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs b/ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs index 18b0b24b4c..f4aacc3cf4 100644 --- a/ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs +++ b/ui/admin/app/templates/scopes/scope/app-tokens/app-token/index.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/admin/tests/integration/components/form/app-token/index-test.js b/ui/admin/tests/integration/components/form/app-token/read/index-test.js similarity index 82% rename from ui/admin/tests/integration/components/form/app-token/index-test.js rename to ui/admin/tests/integration/components/form/app-token/read/index-test.js index a47213c202..9e82965b22 100644 --- a/ui/admin/tests/integration/components/form/app-token/index-test.js +++ b/ui/admin/tests/integration/components/form/app-token/read/index-test.js @@ -9,7 +9,7 @@ import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupIntl } from 'ember-intl/test-support'; -module('Integration | Component | form/app-token', function (hooks) { +module('Integration | Component | form/app-token/read', function (hooks) { setupRenderingTest(hooks); setupIntl(hooks, 'en-us'); @@ -33,7 +33,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it renders all form fields', async function (assert) { - await render(hbs``); + await render(hbs``); // HDS Form components render readonly inputs for accessibility assert.dom('input[readonly]').exists('Should have readonly text input'); @@ -45,7 +45,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it shows readonly fields correctly', async function (assert) { - await render(hbs``); + await render(hbs``); // HDS readonly fields - check for both input and textarea assert.dom('input[readonly]').exists('Should have readonly input'); @@ -53,7 +53,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it displays all DescriptionList sections', async function (assert) { - await render(hbs``); + await render(hbs``); // About section assert @@ -81,7 +81,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge returns correct color and text for active status', async function (assert) { this.model.status = 'active'; - await render(hbs``); + await render(hbs``); assert.dom('.hds-badge').containsText('Active'); assert.dom('.hds-badge--color-success').exists(); @@ -89,7 +89,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge returns correct color and text for expired status', async function (assert) { this.model.status = 'expired'; - await render(hbs``); + await render(hbs``); assert.dom('.hds-badge').containsText('Expired'); assert.dom('.hds-badge--color-critical').exists(); @@ -97,7 +97,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge returns correct color and text for revoked status', async function (assert) { this.model.status = 'revoked'; - await render(hbs``); + await render(hbs``); assert.dom('.hds-badge').containsText('Revoked'); assert.dom('.hds-badge--color-critical').exists(); @@ -105,7 +105,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge returns correct color and text for stale status', async function (assert) { this.model.status = 'stale'; - await render(hbs``); + await render(hbs``); assert.dom('.hds-badge').containsText('Stale'); assert.dom('.hds-badge--color-critical').exists(); @@ -113,7 +113,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge returns correct color and text for unknown status', async function (assert) { this.model.status = 'unknown'; - await render(hbs``); + await render(hbs``); assert.dom('.hds-badge').containsText('Unknown'); assert.dom('.hds-badge--color-neutral').exists(); @@ -121,7 +121,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('statusBadge handles null status', async function (assert) { this.model.status = null; - await render(hbs``); + await render(hbs``); // Should render but with neutral color assert.dom('.hds-badge').exists(); @@ -129,14 +129,14 @@ module('Integration | Component | form/app-token', function (hooks) { test('scopeInfo returns correct icon and text for global scope', async function (assert) { this.model.scope.isGlobal = true; - await render(hbs``); + await render(hbs``); assert.dom('[data-test-icon="globe"]').exists(); }); test('scopeInfo returns correct icon and text for org scope', async function (assert) { this.model.scope.isOrg = true; - await render(hbs``); + await render(hbs``); assert.dom('[data-test-icon="org"]').exists(); }); @@ -144,7 +144,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('scopeInfo returns correct icon and text for project scope', async function (assert) { this.model.scope.isGlobal = false; this.model.scope.isOrg = false; - await render(hbs``); + await render(hbs``); assert.dom('[data-test-icon="grid"]').exists(); }); @@ -152,7 +152,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('ttlFormatted converts milliseconds to days correctly', async function (assert) { // 1 day = 86400000 milliseconds this.model.TTL = 86400000; - await render(hbs``); + await render(hbs``); // The format-day-year helper should display "1 day" const text = this.element.textContent; @@ -163,7 +163,7 @@ module('Integration | Component | form/app-token', function (hooks) { test('ttsFormatted converts milliseconds to days correctly', async function (assert) { // 2 days = 172800000 milliseconds this.model.TTS = 172800000; - await render(hbs``); + await render(hbs``); // The format-day-year helper should display "2 days" const text = this.element.textContent; @@ -172,7 +172,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it displays expiration date', async function (assert) { - await render(hbs``); + await render(hbs``); const text = this.element.textContent; const hasExpiration = text.includes('Dec') && text.includes('2025'); @@ -180,7 +180,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it displays created time', async function (assert) { - await render(hbs``); + await render(hbs``); const text = this.element.textContent; const hasCreatedTime = text.includes('Dec') && text.includes('2025'); @@ -188,7 +188,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it displays last used time', async function (assert) { - await render(hbs``); + await render(hbs``); // Should have tooltip icon for the label assert.dom('[data-test-icon="info"]').exists(); @@ -198,7 +198,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it renders created by user with link', async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom('[data-test-icon="user"]').exists(); assert.dom('.hds-link-inline').exists(); @@ -206,7 +206,7 @@ module('Integration | Component | form/app-token', function (hooks) { }); test('it renders scope with link and icon', async function (assert) { - await render(hbs``); + await render(hbs``); // Should have scope icon (grid for project) assert.dom('[data-test-icon="grid"]').exists(); @@ -220,7 +220,7 @@ module('Integration | Component | form/app-token', function (hooks) { scope: {}, }; - await render(hbs``); + await render(hbs``); // Check that the form renders without crashing assert.dom('.rose-form').exists();