From e8c196aa625a134c1d414a057df09d8b8a5bb515 Mon Sep 17 00:00:00 2001 From: claire bontempo <68122737+hellobontempo@users.noreply.github.com> Date: Mon, 12 May 2025 11:13:24 -0700 Subject: [PATCH] UI: Update template helpers and cleanup component args (#30580) * make displayName a global helper * rename authTabTypes to visibleMountsByType * remove superfluous arg * move all of mount displaying to component * rename hasMountData to isVisibleMount, update comment --- ui/app/components/auth/form-template.hbs | 24 ++++------- ui/app/components/auth/form-template.ts | 43 ++++++++----------- ui/app/components/auth/mounts-display.hbs | 33 ++++++++++++++ ui/app/components/auth/page.hbs | 7 ++- ui/app/components/auth/page.js | 4 +- ui/app/components/auth/single-mount.hbs | 16 ------- ui/app/components/auth/tabs.hbs | 26 +++-------- ui/app/components/auth/tabs.ts | 13 +++--- ui/app/helpers/auth-display-name.ts | 11 +++++ ui/app/routes/vault/cluster/auth.js | 4 +- ui/tests/acceptance/auth/auth-test.js | 6 +-- ui/tests/helpers/auth/auth-form-selectors.ts | 2 +- .../components/auth/form-template-test.js | 22 +++++----- .../integration/components/auth/page-test.js | 8 ++-- ui/types/vault/auth/form.d.ts | 2 +- 15 files changed, 112 insertions(+), 109 deletions(-) create mode 100644 ui/app/components/auth/mounts-display.hbs delete mode 100644 ui/app/components/auth/single-mount.hbs create mode 100644 ui/app/helpers/auth-display-name.ts diff --git a/ui/app/components/auth/form-template.hbs b/ui/app/components/auth/form-template.hbs index a006dfcef9..50250f9fdc 100644 --- a/ui/app/components/auth/form-template.hbs +++ b/ui/app/components/auth/form-template.hbs @@ -40,22 +40,16 @@ <:authSelectOptions>
{{#if this.showCustomAuthOptions}} - {{! URL contains "with" query param and it references a visible mount }} - {{! treat it as a "preferred" mount and hide all other tabs }} - {{#if @directLinkData.hasMountData}} - {{#let @directLinkData as |mount|}} - - {{/let}} + {{#if @directLinkData.isVisibleMount}} + {{! URL contains a "with" query param that references a mount with listing_visibility="unauth" }} + {{! Treat it as a "preferred" mount and hide all other tabs }} + {{else}} @@ -72,7 +66,7 @@ {{#each this.availableMethodTypes as |type|}} {{/each}} diff --git a/ui/app/components/auth/form-template.ts b/ui/app/components/auth/form-template.ts index 779277fa1f..8b7a04b0a3 100644 --- a/ui/app/components/auth/form-template.ts +++ b/ui/app/components/auth/form-template.ts @@ -7,7 +7,7 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { ALL_LOGIN_METHODS, supportedTypes } from 'vault/utils/supported-login-methods'; +import { supportedTypes } from 'vault/utils/supported-login-methods'; import { getRelativePath } from 'core/utils/sanitize-path'; import type AuthService from 'vault/vault/services/auth'; @@ -15,7 +15,7 @@ import type FlagsService from 'vault/services/flags'; import type Store from '@ember-data/store'; import type VersionService from 'vault/services/version'; import type ClusterModel from 'vault/models/cluster'; -import type { AuthTabData, AuthTabMountData } from 'vault/vault/auth/form'; +import type { UnauthMountsByType, AuthTabMountData } from 'vault/vault/auth/form'; import type { HTMLElementEvent } from 'vault/forms'; /** @@ -29,26 +29,26 @@ import type { HTMLElementEvent } from 'vault/forms'; * dynamically renders the corresponding form. * * - * @param {object} authTabData - auth methods to render as tabs, contains mount data for any mounts with listing_visibility="unauth" - * @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby - * @param {function} handleNamespaceUpdate - callback task that passes user input to the controller and updates the namespace query param in the url - * @param {boolean} hasVisibleAuthMounts - whether or not any mounts have been tuned with listing_visibility="unauth" - * @param {string} namespaceQueryParam - namespace query param from the url - * @param {function} onSuccess - callback after the initial authentication request, if an mfa_requirement exists the parent renders the mfa form otherwise it fires the authSuccess action in the auth controller and handles transitioning to the app * @param {string} canceledMfaAuth - saved auth type from a cancelled mfa verification + * @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby * @param {object} directLinkData - mount data built from the "with" query param. If param is a mount path and maps to a visible mount, the login form defaults to this mount. Otherwise the form preselects the passed auth type. + * @param {function} handleNamespaceUpdate - callback task that passes user input to the controller and updates the namespace query param in the url + * @param {string} namespaceQueryParam - namespace query param from the url + * @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider". if present, disables the namespace input + * @param {function} onSuccess - callback after the initial authentication request, if an mfa_requirement exists the parent renders the mfa form otherwise it fires the authSuccess action in the auth controller and handles transitioning to the app + * @param {object} visibleMountsByType - auth methods to render as tabs, contains mount data for any mounts with listing_visibility="unauth" * * */ interface Args { - authTabData: AuthTabData; - cluster: ClusterModel; - handleNamespaceUpdate: CallableFunction; - hasVisibleAuthMounts: boolean; - namespaceQueryParam: string; - onSuccess: CallableFunction; canceledMfaAuth: string; - directLinkData: (AuthTabMountData & { hasMountData: boolean }) | null; + cluster: ClusterModel; + directLinkData: (AuthTabMountData & { isVisibleMount: boolean }) | null; + handleNamespaceUpdate: CallableFunction; + namespaceQueryParam: string; + oidcProviderQueryParam: string; + onSuccess: CallableFunction; + visibleMountsByType: UnauthMountsByType; } export default class AuthFormTemplate extends Component { @@ -64,13 +64,8 @@ export default class AuthFormTemplate extends Component { @tracked selectedAuthMethod = ''; @tracked errorMessage = ''; - displayName = (type: string) => { - const displayName = ALL_LOGIN_METHODS?.find((t) => t.type === type)?.displayName; - return displayName || type; - }; - get authTabTypes() { - const visibleMounts = this.args.authTabData; + const visibleMounts = this.args.visibleMountsByType; return visibleMounts ? Object.keys(visibleMounts) : []; } @@ -111,7 +106,7 @@ export default class AuthFormTemplate extends Component { // This getter determines whether to render an alternative view (e.g., tabs or a preferred mount). // If `true`, the "Sign in with other methods →" link is shown. get showCustomAuthOptions() { - const hasLoginCustomization = this.args?.directLinkData?.hasMountData || this.args.hasVisibleAuthMounts; + const hasLoginCustomization = this.args?.directLinkData?.isVisibleMount || this.args.visibleMountsByType; // Show if customization exists and the user has NOT clicked "Sign in with other methods →" return hasLoginCustomization && !this.showOtherMethods; } @@ -123,12 +118,12 @@ export default class AuthFormTemplate extends Component { this.setAuthType(this.preselectedType); } else { // if nothing has been preselected, select first tab or set to 'token' - const authType = this.args.hasVisibleAuthMounts ? (this.authTabTypes[0] as string) : 'token'; + const authType = this.args.visibleMountsByType ? (this.authTabTypes[0] as string) : 'token'; this.setAuthType(authType); } // DETERMINES INITIAL RENDER: custom selection (direct link or tabs) vs dropdown - if (this.args.hasVisibleAuthMounts) { + if (this.args.visibleMountsByType) { // render tabs if selectedAuthMethod is one, otherwise render dropdown (i.e. showOtherMethods = false) this.showOtherMethods = this.authTabTypes.includes(this.selectedAuthMethod) ? false : true; } else { diff --git a/ui/app/components/auth/mounts-display.hbs b/ui/app/components/auth/mounts-display.hbs new file mode 100644 index 0000000000..841986f326 --- /dev/null +++ b/ui/app/components/auth/mounts-display.hbs @@ -0,0 +1,33 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + +{{#if (gt @mounts.length 1)}} + {{! render dropdown of mount paths }} + + Mount path + + {{#each @mounts as |mount|}} + + {{/each}} + + +{{else}} + {{! render a single mount path }} + {{#let (get @mounts "0") as |mount|}} + {{#unless @hideType}} + + {{auth-display-name mount.type}} + + {{/unless}} + {{#if mount.description}} + {{mount.description}} + {{/if}} + {{! the token auth method does't support custom paths so no need to render an input }} + {{#if @shouldRenderPath}} + {{! path is hidden so it is submitted with FormData but does not clutter the login form }} + + {{/if}} + {{/let}} +{{/if}} \ No newline at end of file diff --git a/ui/app/components/auth/page.hbs b/ui/app/components/auth/page.hbs index 68306d7672..a0fd536c65 100644 --- a/ui/app/components/auth/page.hbs +++ b/ui/app/components/auth/page.hbs @@ -50,15 +50,14 @@ /> {{else}} {{/if}} diff --git a/ui/app/components/auth/page.js b/ui/app/components/auth/page.js index 6b13114096..dbb0841525 100644 --- a/ui/app/components/auth/page.js +++ b/ui/app/components/auth/page.js @@ -30,7 +30,7 @@ import { action } from '@ember/object'; * @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider" * @param {function} onAuthSuccess - callback task in controller that receives the auth response (after MFA, if enabled) when login is successful * @param {function} onNamespaceUpdate - callback task that passes user input to the controller to update the login namespace in the url query params - * @param {object} visibleAuthMounts - keys are auth methods to render as tabs, values is an array of mount data for mounts with listing_visibility="unauth" + * @param {object} visibleAuthMounts - mount paths with listing_visibility="unauth", keys are the mount path and value is it's mount data such as "type" or "description," if it exists * */ export const CSP_ERROR = @@ -43,7 +43,7 @@ export default class AuthPage extends Component { @tracked mfaAuthData; @tracked mfaErrors = ''; - get authTabData() { + get visibleMountsByType() { const visibleAuthMounts = this.args.visibleAuthMounts; if (visibleAuthMounts) { const authMounts = visibleAuthMounts; diff --git a/ui/app/components/auth/single-mount.hbs b/ui/app/components/auth/single-mount.hbs deleted file mode 100644 index 7e8deb5873..0000000000 --- a/ui/app/components/auth/single-mount.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - -{{#if @type}} - {{@type}} -{{/if}} -{{#if @description}} - {{@description}} data-test-description -{{/if}} -{{! the token auth method does't support custom paths so no need to render an input }} -{{#if @shouldRenderPath}} - {{! path is hidden so it is submitted with FormData but does not clutter the login form }} - -{{/if}} \ No newline at end of file diff --git a/ui/app/components/auth/tabs.hbs b/ui/app/components/auth/tabs.hbs index b9aa66bdfa..1a8c22c172 100644 --- a/ui/app/components/auth/tabs.hbs +++ b/ui/app/components/auth/tabs.hbs @@ -5,32 +5,18 @@ {{#each-in @authTabData as |methodType mounts|}} - {{@displayNameHelper methodType}} + {{auth-display-name methodType}}
{{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown. However, for accessibility, we only want to render form inputs relevant to the selected method. By wrapping the elements in this conditional, it only renders them when the tab is selected. }} {{#if (eq @selectedAuthMethod methodType)}} - {{#if (gt mounts.length 1)}} - {{! render dropdown of mount paths }} - - Mount path - - {{#each mounts as |mount|}} - - {{/each}} - - - {{else}} - {{#let (get mounts "0") as |mount|}} - - {{/let}} - {{/if}} + {{/if}}
diff --git a/ui/app/components/auth/tabs.ts b/ui/app/components/auth/tabs.ts index 0961fdb2d8..c14a2eba43 100644 --- a/ui/app/components/auth/tabs.ts +++ b/ui/app/components/auth/tabs.ts @@ -6,24 +6,27 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import type { AuthTabData } from 'vault/vault/auth/form'; +import type { UnauthMountsByType } from 'vault/vault/auth/form'; interface Args { - authTabData: AuthTabData; - authTabTypes: string[]; + authTabData: UnauthMountsByType; handleTabClick: CallableFunction; selectedAuthMethod: string; } export default class AuthTabs extends Component { + get tabTypes() { + return this.args.authTabData ? Object.keys(this.args.authTabData) : []; + } + get selectedTabIndex() { - const index = this.args.authTabTypes.indexOf(this.args.selectedAuthMethod); + const index = this.tabTypes.indexOf(this.args.selectedAuthMethod); // negative index means the selected method isn't a tab, default to first tab return index < 0 ? 0 : index; } @action onClickTab(_event: Event, idx: number) { - this.args.handleTabClick(this.args.authTabTypes[idx]); + this.args.handleTabClick(this.tabTypes[idx]); } } diff --git a/ui/app/helpers/auth-display-name.ts b/ui/app/helpers/auth-display-name.ts new file mode 100644 index 0000000000..6014a0947d --- /dev/null +++ b/ui/app/helpers/auth-display-name.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { ALL_LOGIN_METHODS } from 'vault/utils/supported-login-methods'; + +export default function authDisplayName(methodType: string) { + const displayName = ALL_LOGIN_METHODS?.find((t) => t.type === methodType)?.displayName; + return displayName || methodType; +} diff --git a/ui/app/routes/vault/cluster/auth.js b/ui/app/routes/vault/cluster/auth.js index 58b145ec47..cb97fbbd25 100644 --- a/ui/app/routes/vault/cluster/auth.js +++ b/ui/app/routes/vault/cluster/auth.js @@ -106,11 +106,11 @@ export default class AuthRoute extends ClusterRouteBase { */ getMountOrTypeData(authMount, visibleAuthMounts) { if (visibleAuthMounts?.[authMount]) { - return { path: authMount, ...visibleAuthMounts[authMount], hasMountData: true }; + return { path: authMount, ...visibleAuthMounts[authMount], isVisibleMount: true }; } const types = supportedTypes(this.version.isEnterprise); if (types.includes(sanitizePath(authMount))) { - return { type: authMount, hasMountData: false }; + return { type: authMount, isVisibleMount: false }; } // `type` is necessary because it determines which login fields to render. // If we can't safely glean it from the query param, ignore it and return null diff --git a/ui/tests/acceptance/auth/auth-test.js b/ui/tests/acceptance/auth/auth-test.js index e8c748c62f..4b24db27b1 100644 --- a/ui/tests/acceptance/auth/auth-test.js +++ b/ui/tests/acceptance/auth/auth-test.js @@ -103,8 +103,8 @@ module('Acceptance | auth login form', function (hooks) { test('it renders preferred mount view if "with" query param is a mount path with listing_visibility="unauth"', async function (assert) { await visit('/vault/auth?with=my-oidc%2F'); - await waitFor(AUTH_FORM.preferredMethod('OIDC')); - assert.dom(AUTH_FORM.preferredMethod('OIDC')).hasText('OIDC', 'it renders mount type'); + await waitFor(AUTH_FORM.preferredMethod('oidc')); + assert.dom(AUTH_FORM.preferredMethod('oidc')).hasText('OIDC', 'it renders mount type'); assert.dom(GENERAL.inputByAttr('role')).exists(); assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden'); assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/'); @@ -122,7 +122,7 @@ module('Acceptance | auth login form', function (hooks) { assert .dom(AUTH_FORM.tabBtn('oidc')) .hasAttribute('aria-selected', 'true', 'it selects tab matching query param'); - assert.dom(AUTH_FORM.preferredMethod('OIDC')).doesNotExist('it does not render single mount view'); + assert.dom(AUTH_FORM.preferredMethod('oidc')).doesNotExist('it does not render single mount view'); assert.dom(GENERAL.backButton).doesNotExist(); }); diff --git a/ui/tests/helpers/auth/auth-form-selectors.ts b/ui/tests/helpers/auth/auth-form-selectors.ts index 7cf6b35aa2..5f4f34cf90 100644 --- a/ui/tests/helpers/auth/auth-form-selectors.ts +++ b/ui/tests/helpers/auth/auth-form-selectors.ts @@ -7,7 +7,7 @@ export const AUTH_FORM = { selectMethod: '[data-test-select="auth type"]', form: '[data-test-auth-form]', login: '[data-test-auth-submit]', - preferredMethod: (displayName: string) => `p[data-test-auth-method="${displayName}"]`, // display name => i.e "OIDC" not "oidc" + preferredMethod: (method: string) => `p[data-test-auth-method="${method}"]`, tabs: '[data-test-auth-tab]', tabBtn: (method: string) => `[data-test-auth-tab="${method}"] button`, // method is all lowercased description: '[data-test-description]', diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js index 5907df9e03..99eeee1304 100644 --- a/ui/tests/integration/components/auth/form-template-test.js +++ b/ui/tests/integration/components/auth/form-template-test.js @@ -25,12 +25,12 @@ module('Integration | Component | auth | form template', function (hooks) { setupMirage(hooks); hooks.beforeEach(function () { + window.localStorage.clear(); this.version = this.owner.lookup('service:version'); - this.authTabData = null; + this.visibleMountsByType = null; this.cluster = { id: '1' }; this.directLinkData = null; this.handleNamespaceUpdate = sinon.spy(); - this.hasVisibleAuthMounts = false; this.namespaceQueryParam = ''; this.oidcProviderQueryParam = ''; this.onSuccess = sinon.spy(); @@ -39,11 +39,10 @@ module('Integration | Component | auth | form template', function (hooks) { this.renderComponent = () => { return render(hbs`