UI: Feature Descriptions (#12737) (#13055)

* add and update feature descriptions

* add description doc links, improve spacing,  remove dividers

* update tests

* Update ui/app/components/recovery/page/snapshots/load.hbs



* update seal action and tests

* use description argument for simple descriptions

* clean up

* update with finalized descriptions

* updated policy descriptions

* update client count description

* fix typo and update tests

* update tests

* more test updates

* Apply suggestions from code review



---------

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
Vault Automation 2026-03-18 12:03:29 -04:00 committed by GitHub
parent 7cd77ffaeb
commit 9932623861
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 413 additions and 370 deletions

View file

@ -7,9 +7,6 @@
<div class="is-flex-column align-items-end">
{{! Enterprise should always have a @billingStartTime but as a fallback allow the user to query dates manually }}
{{#if (and @billingStartTime this.version.isEnterprise)}}
<Hds::Text::Display @tag="p" @size="100" class="has-bottom-margin-xs" data-test-text-display="change-billing-data">
{{if this.flags.isHvdManaged "Change data period" "Change billing period"}}
</Hds::Text::Display>
<Hds::Dropdown class="has-left-margin-xs" as |D|>
<D.ToggleButton @text={{this.formatDate @startTimestamp}} @color="secondary" data-test-date-range-edit />
<D.Description @text="Current period" />

View file

@ -9,21 +9,6 @@
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Client usage")}}
/>
</:breadcrumbs>
<:subtitle>
{{#if @activityTimestamp}}
Dashboard last updated:
{{date-format @activityTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
<Hds::Button
@color="tertiary"
@icon="reload"
@isIconOnly={{true}}
@size="small"
@text="Refresh page"
data-test-button="Refresh page"
{{on "click" this.refreshRoute}}
/>
{{/if}}
</:subtitle>
<:description>
{{#if (and this.version.isEnterprise @billingStartTime)}}
{{! Enterprise should always have a @billingStartTime but as a fallback allow the user to query dates manually. }}
@ -56,6 +41,27 @@
</Hds::Text::Body>
</div>
{{/if}}
View the number and source of active Vault clients on the cluster to track license compliance.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/client-count"}}>
Learn more
</Hds::Link::Inline>
{{#if @activityTimestamp}}
<Hds::Text::Body @tag="p" @color="faint">
Dashboard last updated:
{{date-format @activityTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
<Hds::Button
@color="tertiary"
@icon="reload"
@isIconOnly={{true}}
@size="small"
@text="Refresh page"
data-test-button="Refresh page"
{{on "click" this.refreshRoute}}
/>
</Hds::Text::Body>
{{/if}}
</:description>
<:actions>
{{#if this.showExportButton}}

View file

@ -2,7 +2,7 @@
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<div class="has-border-bottom-light">
<div>
<Clients::PageHeader
@billingStartTime={{@config.billing_start_timestamp}}
@retentionMonths={{@config.retention_months}}
@ -16,13 +16,6 @@
</div>
<div class="has-top-bottom-margin">
<Hds::Text::Body @tag="p" @size="100" class="has-bottom-margin-s">
This is the dashboard for your overall client count usage. Review Vault's
<Hds::Link::Inline @href={{doc-link "/vault/docs/concepts/client-count"}} @isHrefExternal={{true}}>
client counting documentation</Hds::Link::Inline>
for more information.
</Hds::Text::Body>
{{#if this.trackingDisabled}}
<Hds::Alert @type="inline" @color="warning" class="has-bottom-margin-s" as |A|>
<A.Title data-test-counts-disabled>Tracking is disabled</A.Title>

View file

@ -3,12 +3,16 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Vault {{@version.versionDisplay}}">
<Page::Header
@title="Vault {{@version.versionDisplay}}"
@description="Review the status of your Vault cluster, including: currently enabled secrets engines and authentication methods, and system configurations."
>
<:badges>
{{#if @version.isEnterprise}}
<Hds::Badge @text={{this.namespace.currentNamespace}} @icon="org" data-test-badge-namespace />
{{/if}}
</:badges>
</Page::Header>
<hr class="has-top-margin-xxs has-bottom-margin-l has-background-gray-200" />

View file

@ -3,12 +3,12 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title={{titleize (pluralize this.identityType)}}>
<Page::Header @title={{titleize (pluralize @identityType)}} @description={{this.description}}>
<:breadcrumbs>
<Page::Breadcrumbs
@breadcrumbs={{array
(hash label="Vault" route="vault.cluster.dashboard" icon="vault")
(hash label=(titleize (pluralize this.identityType)))
(hash label=(titleize (pluralize @identityType)))
}}
/>
</:breadcrumbs>
@ -17,12 +17,12 @@
<nav class="tabs" aria-label="navigation for entities">
<ul>
<li>
<LinkTo @route="vault.cluster.access.identity.index" @model={{pluralize this.identityType}}>
{{capitalize (pluralize this.identityType)}}
<LinkTo @route="vault.cluster.access.identity.index" @model={{pluralize @identityType}}>
{{capitalize (pluralize @identityType)}}
</LinkTo>
</li>
<li>
<LinkTo @route="vault.cluster.access.identity.aliases.index" @model={{pluralize this.identityType}}>
<LinkTo @route="vault.cluster.access.identity.aliases.index" @model={{pluralize @identityType}}>
Aliases
</LinkTo>
</li>
@ -30,30 +30,30 @@
</nav>
<Toolbar>
{{#if this.model.meta.total}}
{{#if @model.meta.total}}
<ToolbarFilters>
<Identity::LookupInput @type={{this.identityType}} />
<Identity::LookupInput @type={{@identityType}} />
</ToolbarFilters>
{{/if}}
<ToolbarActions>
{{#if (eq this.identityType "entity")}}
{{#if (eq @identityType "entity")}}
<ToolbarLink
@route={{"vault.cluster.access.identity.merge"}}
@model={{pluralize this.identityType}}
@model={{pluralize @identityType}}
data-test-entity-merge-link={{true}}
>
Merge
{{pluralize this.identityType}}
{{pluralize @identityType}}
</ToolbarLink>
{{/if}}
<ToolbarLink
@route="vault.cluster.access.identity.create"
@model={{pluralize this.identityType}}
@model={{pluralize @identityType}}
@type="add"
data-test-entity-create-link={{true}}
>
Create
{{this.identityType}}
{{@identityType}}
</ToolbarLink>
</ToolbarActions>
</Toolbar>

View file

@ -1,8 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
export default Component.extend({});

View file

@ -0,0 +1,23 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
interface Args {
identityType: 'entity' | 'group';
model: {
meta: {
total: number;
};
};
}
export default class EntityNavComponent extends Component<Args> {
get description() {
return this.args.identityType === 'entity'
? 'Create and manage unique identities for human and non-human identities to serve as the canonical reference ID for policies and metadata.'
: 'Create and name logical collections of entities to simplify policy management and permission scaling across your organization.';
}
}

View file

@ -9,6 +9,12 @@
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="License")}}
/>
</:breadcrumbs>
<:description>
View your Vault Enterprise license ID, status, expiration date, and feature entitlements.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/license"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<section class="box is-sideless is-marginless is-shadowless is-fullwidth">

View file

@ -8,6 +8,12 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:description>
Configure authentication methods for accessing Vault.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/mfa"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.showIntroButton}}
<Hds::Button

View file

@ -14,6 +14,13 @@
<:badges>
<Hds::Badge @icon="org" @text={{this.namespacePath}} data-test-badge="namespace-path" />
</:badges>
<:description>
Create logically separated, multi-tenant environments so teams can manage secrets, policies, and authentication
methods independently.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/namespaces"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.showIntroButton}}
<Hds::Button
@ -48,7 +55,7 @@
{{#if this.showContent}}
{{! Show namespace list }}
{{#if this.hasNamespaces}}
<Toolbar>
<Toolbar class="top-margin-16">
<ToolbarFilters>
<FilterInputExplicit
@query={{@model.pageFilter}}

View file

@ -18,6 +18,12 @@
}}
/>
</:breadcrumbs>
<:description>
{{this.description}}
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/policies"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.showIntroButton}}
<Hds::Button

View file

@ -44,6 +44,18 @@ export default class PagePoliciesComponent extends Component<Args> {
this.filter = this.args.filter || '';
}
get description() {
const policyType = this.args.policyType;
if (policyType === PolicyTypes.ACL) {
return 'Define fine-grained rules to explicitly grant or forbid access to specific paths and operations within your cluster. Because Vault is a “default deny” system, if a permission is not granted in a policy, an entity would not have permission.';
} else if (policyType === PolicyTypes.EGP) {
return 'Use Sentinel to specify policies as code that apply to discrete API paths and enforce organizational compliance standards.';
} else if (policyType === PolicyTypes.RGP) {
return 'Use Sentinel to specify policies as code that apply to tokens, entities, groups and enforce organizational compliance standards.';
}
return '';
}
// Check if the filter exactly matches a policy ID
get filterMatchesKey(): boolean {
const filter = this.filter;
@ -94,7 +106,7 @@ export default class PagePoliciesComponent extends Component<Args> {
// Show when it is not in a dismissed state and there are no non-default policies and
get showWizard() {
if (this.args.policyType !== 'acl') return false;
if (this.args.policyType !== PolicyTypes.ACL) return false;
// Use total instead of filtered total to avoid flashing wizard when filtering with no results
return !this.wizard.isDismissed(WIZARD_ID) && this.hasOnlyDefaultPolicies;
}
@ -106,9 +118,9 @@ export default class PagePoliciesComponent extends Component<Args> {
const policyType = this.args.policyType;
// Use the appropriate sys endpoint based on policy type
if (policyType === 'egp') {
if (policyType === PolicyTypes.EGP) {
await this.api.sys.systemDeletePoliciesEgpName(policyName);
} else if (policyType === 'rgp') {
} else if (policyType === PolicyTypes.RGP) {
await this.api.sys.systemDeletePoliciesRgpName(policyName);
} else {
await this.api.sys.policiesDeleteAclPolicy(policyName);

View file

@ -1,29 +0,0 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title={{@title}} @subtitle={{@subtitle}}>
<:breadcrumbs>
{{#if @breadcrumbs}}
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
{{/if}}
</:breadcrumbs>
<:badges>
{{#if this.version.isCommunity}}
<Hds::Badge @text="Enterprise" @color="highlight" data-test-badge="enterprise" />
{{/if}}
</:badges>
<:actions>
{{#if @action}}
<Hds::Button
@text={{@action.text}}
@icon={{@action.icon}}
@iconPosition={{@action.iconPosition}}
@color={{@action.color}}
@route={{@action.route}}
@models={{@action.models}}
/>
{{/if}}
</:actions>
</Page::Header>

View file

@ -1,18 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type VersionService from 'vault/services/version';
interface Args {
title: string;
subtitle?: string;
action?: unknown;
}
export default class Header extends Component<Args> {
@service declare readonly version: VersionService;
}

View file

@ -3,11 +3,22 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Recovery::Page::Header
@title="Secrets recovery"
@subtitle="Recover lost or deleted data from a raft snapshot. Supported data includes KV v1 and Cubbyhole secrets or Database static roles."
@breadcrumbs={{@breadcrumbs}}
/>
<Page::Header @title="Secrets recovery" class="bottom-margin-16">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:badges>
{{#if @model.showCommunityMessage}}
<Hds::Badge @text="Enterprise" @color="highlight" data-test-badge="enterprise" />
{{/if}}
</:badges>
<:description>
Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/recover-secrets"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
{{#if @model.snapshots.showRaftStorageMessage}}
<Hds::ApplicationState class="top-padding-32 is-marginless" as |A|>

View file

@ -3,7 +3,11 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Recovery::Page::Header @title="Upload snapshot" @breadcrumbs={{@breadcrumbs}} />
<Page::Header @title="Upload snapshot">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
{{#if this.bannerError}}
<div class="has-top-padding-m has-bottom-padding-m">

View file

@ -3,21 +3,29 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Recovery::Page::Header
@title="Secrets recovery"
@subtitle="Recover lost or deleted data from a raft snapshot. Supported data includes KV v1 and Cubbyhole secrets or Database static roles."
@action={{(hash
text="Recover secrets"
icon="reload"
iconPosition="leading"
color="primary"
route="vault.cluster.recovery.snapshots.snapshot.manage"
models=(array @model.snapshot.snapshot_id)
)}}
@breadcrumbs={{@breadcrumbs}}
/>
<Page::Header @title="Secrets recovery">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:description>
Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/recover-secrets"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
<Hds::Button
@text="Recover secrets"
@icon="reload"
@iconPosition="leading"
@color="primary"
@route="vault.cluster.recovery.snapshots.snapshot.manage"
@models={{(array @model.snapshot.snapshot_id)}}
/>
</:actions>
</Page::Header>
<Hds::Table data-test-table="details">
<Hds::Table class="top-margin-16" data-test-table="details">
<:head as |H|>
<H.Tr>
{{#each this.tableColumns as |col|}}

View file

@ -3,11 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Recovery::Page::Header
@title="Secrets recovery"
@subtitle="Recover lost or deleted data from a raft snapshot. Supported data includes KV v1 and Cubbyhole secrets or Database static roles."
@breadcrumbs={{@breadcrumbs}}
/>
<Page::Header @title="Secrets recovery">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:description>
Restore specific secrets from cluster snapshots without impacting the availability of the active Vault cluster.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/recover-secrets"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
{{#let @model.snapshot as |snapshot|}}
<Hds::Card::Container
@ -32,8 +38,6 @@
</Hds::Card::Container>
{{/let}}
<hr class="has-background-gray-300" />
<Hds::Text::Display @tag="h3" class="has-top-padding-m has-bottom-margin-l">Recover or read data</Hds::Text::Display>
{{#if this.recoveryData}}
<Hds::Alert @type="inline" @color="success" class="has-top-margin-m has-bottom-margin-m" data-test-inline-alert as |A|>

View file

@ -3,28 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
}}
<div class="box is-sideless is-fullwidth is-marginless">
{{#if this.error}}
<Hds::Alert @type="inline" @color="critical" class="has-bottom-margin-m" data-test-seal-error as |A|>
<A.Title>Error</A.Title>
<A.Description>
{{this.error}}
</A.Description>
</Hds::Alert>
{{/if}}
<p>
Sealing a vault tells the Vault server to stop responding to any access operations until it is unsealed again. A sealed
vault throws away its root key to unlock the data, so it physically is blocked from responding to operations again until
the Vault is unsealed again with the "unseal" command or via the API.
</p>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<ConfirmAction
@buttonText="Seal"
@confirmTitle="Seal this cluster?"
@confirmMessage="You will not be able to read or write any data until the cluster is unsealed again."
@onConfirmAction={{this.handleSeal}}
data-test-seal
/>
</div>
<ConfirmAction
@buttonText="Seal"
@confirmTitle="Seal this cluster?"
@confirmMessage="You will not be able to read or write any data until the cluster is unsealed again."
@onConfirmAction={{this.handleSeal}}
data-test-button="Seal"
/>

View file

@ -1,22 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
export default class SealActionComponent extends Component {
@tracked error;
@action
async handleSeal() {
try {
await this.args.onSeal();
} catch (e) {
this.error = errorMessage(e, 'Seal attempt failed. Check Vault logs for details.');
}
}
}

View file

@ -0,0 +1,33 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import type ApiService from 'vault/services/api';
import type FlashMessageService from 'vault/services/flash-messages';
interface Args {
onSeal: CallableFunction;
}
export default class SealActionComponent extends Component<Args> {
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@action
async handleSeal() {
try {
await this.args.onSeal();
} catch (error) {
const message = await this.api.parseError(error, 'Check Vault logs for details.');
this.flashMessages.danger(message.message, {
title: 'Seal attempt failed',
});
}
}
}

View file

@ -3,14 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header
@title="Secrets engines"
@subtitle="Secrets engines available in the {{this.namespace.currentNamespace}} namespace of the {{this.version.clusterName}} cluster."
@icon="key"
>
<Page::Header @title="Secrets engines" @icon="key">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
View and manage your configured secrets engines in the current cluster, ranging from key value store (kv) to dynamic
database credentials and more.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/secret-engines"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.showIntroButton}}
<Hds::Button

View file

@ -16,8 +16,6 @@ import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
import type FlashMessageService from 'vault/services/flash-messages';
import type NamespaceService from 'vault/services/namespace';
import type VersionService from 'vault/services/version';
import type WizardService from 'vault/services/wizard';
/**
@ -38,9 +36,7 @@ interface Args {
export default class SecretEngineList extends Component<Args> {
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly namespace: NamespaceService;
@service declare readonly router: RouterService;
@service declare readonly version: VersionService;
@service declare readonly wizard: WizardService;
@tracked engineTypeFilters: Array<string> = [];

View file

@ -3,14 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Hash data">
<Page::Header
@title="Hash data"
@description="Generate secure cryptographic hashes of input data to verify integrity without storing the raw data."
>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
{{#if this.sum}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<div class="field">
<label for="sum" class="is-input">Sum</label>
<Hds::Copy::Snippet
@ -26,7 +29,7 @@
</div>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorMessage}} />
<div class="field">
<label for="hash-input" class="is-label">

View file

@ -3,14 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Lookup token">
<Page::Header
@title="Lookup token"
@description="Inspect the metadata of a response-wrapping token, such as its creation time and TTL without revealing the underlying secret."
>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
{{#if this.lookupData}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
<div class="box is-fullwidth is-paddingless is-marginless">
{{#each-in this.lookupData as |key value|}}
{{#let (if (eq key "creation_ttl") "Creation TTL" (to-label key)) as |label|}}
<InfoTableRow @label={{label}} @value={{value}} />
@ -26,7 +29,7 @@
</div>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="lookup" />
<MessageError @errorMessage={{this.errorMessage}} />
<div class="field">

View file

@ -3,14 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Random bytes">
<Page::Header
@title="Random bytes"
@description="Use Vault as a cryptographically secure entropy source to generate high-quality random bytes for external applications."
>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
{{#if this.randomBytes}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<label for="rand" class="is-label">Random bytes</label>
<Hds::Copy::Snippet
@textToCopy={{this.randomBytes}}
@ -24,7 +27,7 @@
</div>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorMessage}} />
<div class="field is-horizontal">
<div class="field-body">

View file

@ -3,14 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Rewrap token">
<Page::Header
@title="Rewrap token"
@description="Migrate a wrapped secret to a new token to extend its lifetime without exposing the underlying secret data."
>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
{{#if this.rewrappedToken}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<div class="field">
<label class="is-label">Rewrapped token</label>
<Hds::Copy::Snippet
@ -26,7 +29,7 @@
</div>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="rewrap" />
<MessageError @errorMessage={{this.errorMessage}} />
<div class="field">

View file

@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Unwrap data">
<Page::Header
@title="Unwrap data"
@description="Retrieve the original secret data from a wrapping token and automatically invalidate the token."
>
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
@ -40,7 +43,7 @@
</Hds::ButtonSet>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="unwrap" />
<MessageError @errorMessage={{this.errorMessage}} />
<div class="field">

View file

@ -7,10 +7,17 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
Generate single-use tokens that encrypt sensitive data short-term to provide secure secret delivery and intercept
detection.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/wrapping"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
{{#if this.token}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<div class="field">
<label for="wrap-info" class="is-label">Wrapped token</label>
<Hds::Copy::Snippet
@ -35,7 +42,7 @@
</div>
{{else}}
<form {{on "submit" this.handleSubmit}}>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="wrap" />
<MessageError @errorMessage={{this.errorMessage}} />
<Toolbar>

View file

@ -179,6 +179,10 @@
margin-bottom: size_variables.$spacing-12;
}
.bottom-margin-16 {
margin-bottom: size_variables.$spacing-16;
}
.has-bottom-margin-m {
margin-bottom: size_variables.$spacing-16;
}
@ -207,6 +211,10 @@
margin-top: size_variables.$spacing-8;
}
.top-margin-16 {
margin-top: size_variables.$spacing-16;
}
.has-top-margin-m {
margin-top: size_variables.$spacing-16;
}

View file

@ -4,7 +4,10 @@
}}
{{#if (has-feature "Control Groups")}}
<Page::Header @title="Approval workflow">
<Page::Header
@title="Approval workflow"
@description="Implement manual or automated checkpoints to require authorization before sensitive operations or accessing secrets."
>
<:breadcrumbs>
<Page::Breadcrumbs
@breadcrumbs={{array
@ -13,6 +16,7 @@
}}
/>
</:breadcrumbs>
</Page::Header>
{{#if this.model.canConfigure}}
<Toolbar>

View file

@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Leases">
<Page::Header
@title="Leases"
@description="Monitor and manage the lifetime of dynamic secrets, including tracking time to live (TTL), renewals, and manual revocations."
>
<:breadcrumbs>
<KeyValueHeader
@baseKey={{this.baseKey}}
@ -13,6 +16,7 @@
@showCurrent={{true}}
/>
</:breadcrumbs>
<:actions>
<Hds::Button
@text="Back to Leases"

View file

@ -7,6 +7,12 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
Enforce a secondary layer of identity verification (such as TOTP, Okta, or Duo) during the login process.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/login-mfa"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<Mfa::Nav />

View file

@ -5,12 +5,10 @@
<Page::Header @title=" Multi-factor authentication">
<:description>
<Hds::Text::Body>
Configure and enforce multi-factor authentication (MFA) for users logging into Vault, for any
<br />
authentication method.
<Hds::Link::Inline @href={{doc-link "/vault/tutorials/auth-methods/multi-factor-authentication"}}>Learn more</Hds::Link::Inline>
</Hds::Text::Body>
Enforce a secondary layer of identity verification (such as TOTP, Okta, or Duo) during the login process.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/login-mfa"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
<Hds::Button @text="Configure MFA" @route="vault.cluster.access.mfa.methods.create" data-test-mfa-configure />

View file

@ -13,14 +13,25 @@
}}
/>
</:breadcrumbs>
<:description>
Enforce a secondary layer of identity verification (such as TOTP, Okta, or Duo) during the login process.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/login-mfa"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<div class="has-border-top-light has-top-padding-l">
<div class="has-top-padding-l">
{{#if this.showForms}}
<h3 class="is-size-4 has-text-weight-semibold">Settings</h3>
<p class="has-border-top-light has-top-padding-l">
{{this.description}}
<DocLink @path={{concat "/vault/api-docs/secret/identity/mfa/" this.type}}>Learn more.</DocLink>
<Hds::Link::Inline
@isHrefExternal={{true}}
@href={{doc-link (concat "/vault/api-docs/secret/identity/mfa/" this.type)}}
>
Learn more
</Hds::Link::Inline>
</p>
<Mfa::MethodForm @model={{this.method}} @validations={{this.methodErrors}} class="is-shadowless" />
<Mfa::MfaLoginEnforcementHeader
@ -38,12 +49,7 @@
/>
{{/if}}
{{else}}
<p>
Multi-factor authentication (MFA) allows you to set up another layer of security on top of existing authentication
methods. Vault has four available methods.
<DocLink @path="/vault/api-docs/secret/identity/mfa">Learn more.</DocLink>
</p>
<div class="is-flex-row has-top-margin-xl">
<div class="is-flex-row">
{{#each this.methods as |method|}}
<RadioCard
@value={{lowercase method.name}}

View file

@ -7,6 +7,12 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
Enforce a secondary layer of identity verification (such as TOTP, Okta, or Duo) during the login process.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/login-mfa"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<Mfa::Nav />

View file

@ -8,26 +8,22 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
</Page::Header>
<div class="box is-fullwidth is-sideless is-flex-between is-shadowless is-marginless" data-test-oidc-header>
<p>
Configure Vault to act as an OIDC identity provider, and offer
{{"Vaults"}}
various authentication
<:description>
Use Vault as an identity provider for external authentication methods that use the OpenID Connect (OIDC) protocol.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/oidc-provider"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.isCta}}
<br />
<Hds::Button
@text="Create your first app"
@route="vault.cluster.access.oidc.clients.create"
data-test-oidc-configure
/>
{{/if}}
methods and source of identity to any client applications.
<Hds::Link::Inline @href={{doc-link "/vault/tutorials/auth-methods/oidc-identity-provider"}}>Learn more</Hds::Link::Inline>
</p>
{{#if this.isCta}}
<Hds::Button
@text="Create your first app"
@route="vault.cluster.access.oidc.clients.create"
data-test-oidc-configure
/>
{{/if}}
</div>
</:actions>
</Page::Header>
{{#unless this.isCta}}
{{! show tab links in list routes }}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-oidc-tabs>

View file

@ -10,11 +10,21 @@
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Seal Vault")}}
/>
</:breadcrumbs>
<:description>
Stop Vault from responding to all access operations by discarding its in-memory root key. You must use the CLI or API to
unseal Vault and restore decryption capabilities.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/seal-vault"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{#if this.model.seal.canUpdate}}
<SealAction @onSeal={{action "seal"}} />
{{/if}}
</:actions>
</Page::Header>
{{#if this.model.seal.canUpdate}}
<SealAction @onSeal={{action "seal"}} />
{{else}}
{{#unless this.model.seal.canUpdate}}
<Hds::ApplicationState class="top-padding-32 is-marginless" as |A|>
<A.Header
@title="This token does not have sufficient capabilities to seal this vault"
@ -22,4 +32,4 @@
data-test-empty-state-title
/>
</Hds::ApplicationState>
{{/if}}
{{/unless}}

View file

@ -7,9 +7,15 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
Set a default and backup authentication methods to customize GUI login behavior.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/custom-login"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<Toolbar />
<Toolbar class="top-margin-16" />
{{#if @loginRules}}
{{#each @loginRules as |rule|}}

View file

@ -7,6 +7,15 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</:breadcrumbs>
<:description>
{{#if @showTabs}}
Configure global banners or modal notifications to display important system updates or compliance reminders to GUI
users.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/custom-msg"}}>
Learn more
</Hds::Link::Inline>
{{/if}}
</:description>
</Page::Header>
{{#if @showTabs}}

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::PageHeader class="page-header" as |PH|>
<Hds::PageHeader class="page-header" ...attributes as |PH|>
<PH.Title>{{@title}}</PH.Title>
{{#if (has-block "breadcrumbs")}}
<PH.Breadcrumb>

View file

@ -7,9 +7,15 @@
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:description>
Call Vault and plugin API endpoints directly from the browser to accelerate development and debugging.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/api-docs"}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<div class="box is-fullwidth is-sideless is-marginless">
<div class="box is-fullwidth is-marginless">
<NamespaceReminder as |R|>
Requests use the header
<code>X-Vault-Namespace: {{R.namespace.path}}</code>. You can also use

View file

@ -6,7 +6,7 @@
<form {{on "submit" (fn this.onSubmit this.data)}} data-test-replication-enable-form>
<MessageError @errorMessage={{this.error}} />
<div class="box is-sideless is-fullwidth is-marginless">
<div class="box is-fullwidth is-marginless">
<label for="replication-mode" class="is-label">
Cluster mode
</label>

View file

@ -10,33 +10,14 @@
@breadcrumbs={{array (hash label="Vault" route="vault" icon="vault" linkExternal=true) (hash label=this.title)}}
/>
</:breadcrumbs>
<:description>
{{this.description}}
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link this.docLink}}>
Learn more
</Hds::Link::Inline>
</:description>
</Page::Header>
<div class="box is-sideless is-fullwidth is-marginless">
{{#if (eq @replicationMode "dr")}}
<h2 class="title is-flex-center is-5 is-marginless">
<Icon @size="24" @name="replication-direct" />
Disaster recovery (DR) replication
</h2>
<p class="help has-text-grey-dark">
{{replication-mode-description "dr"}}
</p>
{{else if (eq @replicationMode "performance")}}
<h2 class="title is-flex-center is-5 is-marginless">
<Icon @size="24" @name="replication-perf" />
Performance replication
</h2>
{{#if (has-feature "Performance Replication")}}
<p class="help has-text-grey-dark">
{{replication-mode-description "performance"}}
</p>
{{else}}
<p class="help has-text-grey-dark">
Performance replication is a feature of Vault Enterprise Premium
</p>
{{/if}}
{{/if}}
</div>
<EnableReplicationForm
@replicationMode={{@replicationMode}}
@canEnablePrimary={{this.canEnable "Primary"}}

View file

@ -33,6 +33,17 @@ export default class PageModeIndex extends Component {
return 'Enable replication';
}
get description() {
if (this.args.replicationMode === 'dr') {
return 'Maintain a synchronized standby cluster to enable business continuity and data preservation in the event of a primary site failure.';
}
return 'Pair geographically distributed clusters that share a common configuration to scale Vault.';
}
get docLink() {
return this.args.replicationMode === 'dr' ? '/vault/gui/dr' : '/vault/gui/pr';
}
canEnable = (type) => {
const { cluster, replicationMode } = this.args;
let perm;

View file

@ -12,6 +12,13 @@
<Hds::Badge @text="Plus feature" @color="highlight" @size="large" data-test-badge="Plus feature" />
{{/if}}
</:badges>
<:description>
Centralize your secret governance by synchronizing Vault-managed data to external secret managers in cloud services
providers, GitHub, & Vercel.
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/gui/secret-sync"}}>
Learn more
</Hds::Link::Inline>
</:description>
<:actions>
{{! Only allow users who have activated the feature to create a destination. }}
{{#if @isActivated}}
@ -20,35 +27,19 @@
</:actions>
</Page::Header>
<div class="box is-fullwidth is-sideless is-flex-between is-shadowless" data-test-cta-container>
{{! One cta message regardless of OSS vs Enterprise with/without secrets sync or managed cluster. }}
<p>
This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need
them.
<Hds::Link::Standalone
@text="Learn more about Secrets Sync"
@icon="docs-link"
@iconPosition="trailing"
@isHrefExternal={{true}}
@href={{doc-link "/vault/tutorials/enterprise/secrets-sync"}}
data-test-cta-doc-link
/>
</p>
</div>
<div class="is-flex-row gap-24 has-bottom-margin-m">
<div>
<Hds::Layout::Flex @gap="24" class="top-margin-16" as |LF|>
<LF.Item @tag="div">
<img src={{img-path "~/sync-landing-1.png"}} alt="Secrets sync destinations diagram" aria-describedby="sync-step-1" />
<p id="sync-step-1" class="has-top-margin-m">
<Hds::Text::Body @tag="p">
<b>Step 1:</b>
Create a destination, and set up the connection details to allow Vault access.
</p>
</div>
<div>
</Hds::Text::Body>
</LF.Item>
<LF.Item @tag="div">
<img src={{img-path "~/sync-landing-2.png"}} alt="Syncing secrets diagram" aria-describedby="sync-step-2" />
<p id="sync-step-2" class="has-top-margin-m">
<Hds::Text::Body @tag="p">
<b>Step 2:</b>
Select secrets from your KV v2 engine and sync secrets to your destination.
</p>
</div>
</div>
</Hds::Text::Body>
</LF.Item>
</Hds::Layout::Flex>

View file

@ -11,6 +11,7 @@
@color="warning"
@onDismiss={{if @canActivateSecretsSync (fn (mut this.hideOptIn) true) undefined}}
data-test-secrets-sync-opt-in-banner
class="bottom-margin-16"
as |A|
>
<A.Title>Enable Secrets Sync feature</A.Title>

View file

@ -154,7 +154,7 @@ module('Acceptance | clients | counts', function (hooks) {
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.dropdownOption(1));
assert
.dom(GENERAL.hdsPageHeaderSubtitle)
.dom(GENERAL.hdsPageHeaderDescription)
.hasTextContaining(`Dashboard last updated: ${format(STATIC_NOW, 'MMM d yyyy')}`);
// Save URL with query params before clicking refresh
const url = currentURL();
@ -166,7 +166,7 @@ module('Acceptance | clients | counts', function (hooks) {
assert.true(this.refreshSpy.calledOnce, 'router.refresh() is called once');
assert.strictEqual(currentURL(), url, 'url is the same after clicking refresh');
assert
.dom(GENERAL.hdsPageHeaderSubtitle)
.dom(GENERAL.hdsPageHeaderDescription)
.hasTextContaining(`Dashboard last updated: ${format(fakeUpdatedNow, 'MMM d yyyy')}`);
});
@ -181,7 +181,7 @@ module('Acceptance | clients | counts', function (hooks) {
await click(CLIENT_COUNT.dateRange.edit);
await click(CLIENT_COUNT.dateRange.dropdownOption(1));
assert
.dom(GENERAL.hdsPageHeaderSubtitle)
.dom(GENERAL.hdsPageHeaderDescription)
.hasTextContaining(`Dashboard last updated: ${format(STATIC_NOW, 'MMM d yyyy')}`);
// Save URL with query params before clicking refresh
const url = currentURL();
@ -193,7 +193,7 @@ module('Acceptance | clients | counts', function (hooks) {
assert.true(this.refreshSpy.calledOnce, 'router.refresh() is called once');
assert.strictEqual(currentURL(), url, 'url is the same after clicking refresh');
assert
.dom(GENERAL.hdsPageHeaderSubtitle)
.dom(GENERAL.hdsPageHeaderDescription)
.hasTextContaining(`Dashboard last updated: ${format(fakeUpdatedNow, 'MMM d yyyy')}`);
});
});

View file

@ -350,17 +350,12 @@ module('Acceptance | oidc-config clients', function (hooks) {
});
test('it renders empty state when no clients are configured', async function (assert) {
assert.expect(5);
assert.expect(4);
this.server.get('/identity/oidc/client', () => overrideResponse(404));
await visit(OIDC_BASE_URL);
assert.strictEqual(currentURL(), '/vault/access/oidc');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('OIDC provider');
assert.dom(SELECTORS.oidcHeader).hasText(
`Configure Vault to act as an OIDC identity provider, and offer Vaults various authentication
methods and source of identity to any client applications. Learn more Create your first app`,
'renders call to action header when no clients are configured'
);
assert.dom('[data-test-oidc-landing]').exists('landing page renders when no clients are configured');
assert
.dom(SELECTORS.oidcLandingImg)

View file

@ -90,7 +90,7 @@ module('Acceptance | reduced disclosure test', function (hooks) {
assert.strictEqual(currentURL(), '/vault/settings/seal');
// seal
await click('[data-test-seal]');
await click(GENERAL.button('Seal'));
await click(GENERAL.confirmButton);
await pollCluster(this.owner);
await settled();

View file

@ -56,7 +56,7 @@ module('Acceptance | unseal', function (hooks) {
assert.strictEqual(currentURL(), '/vault/settings/seal');
// seal
await click('[data-test-seal]');
await click(GENERAL.button('Seal'));
await click(GENERAL.confirmButton);
await pollCluster(this.owner);

View file

@ -8,7 +8,6 @@ import { debug } from '@ember/debug';
export const OIDC_BASE_URL = `/vault/access/oidc`;
export const SELECTORS = {
oidcHeader: '[data-test-oidc-header]',
oidcClientCreateButton: '[data-test-oidc-configure]',
oidcRouteTabs: '[data-test-oidc-tabs]',
oidcLandingImg: '[data-test-oidc-img]',

View file

@ -182,19 +182,6 @@ module('Integration | Component | clients/date-range', function (hooks) {
.hasAttribute('aria-expanded', 'false', 'it closes dropdown after selection');
});
test('it renders billing period text', async function (assert) {
await this.renderComponent();
assert
.dom(this.element)
.hasText('Change billing period January 2018', 'it renders billing related text');
});
test('it renders data period text for HVD managed clusters', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
await this.renderComponent();
assert.dom(this.element).hasText('Change data period January 2018');
});
test('it should send an empty string for start_time when selecting current period', async function (assert) {
await this.renderComponent();

View file

@ -276,9 +276,6 @@ module('Integration | Component | clients/page-header', function (hooks) {
.dom(GENERAL.hdsPageHeaderTitle)
.hasTextContaining('Client usage', 'it renders page header title');
assert.dom(GENERAL.breadcrumbs).hasTextContaining('Vault Client usage', 'it renders breadcrumbs');
assert
.dom(GENERAL.textDisplay('change-billing-data'))
.hasTextContaining('Change billing period', 'it renders change billing period text');
});
test('it renders data period text for HVD managed clusters', async function (assert) {
@ -289,9 +286,6 @@ module('Integration | Component | clients/page-header', function (hooks) {
.dom(GENERAL.hdsPageHeaderTitle)
.hasTextContaining('Client usage', 'it renders page header title');
assert.dom(GENERAL.breadcrumbs).hasTextContaining('Vault Client usage', 'it renders breadcrumbs');
assert
.dom(GENERAL.textDisplay('change-billing-data'))
.hasTextContaining('Change data period', 'it renders change data period text');
});
test('it allows date editing if no billing start time is provided', async function (assert) {

View file

@ -31,6 +31,8 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
namespaces = [];
}
this.breadcrumbs = [{ label: 'Secrets Recovery', route: 'vault.cluster.recovery.snapshots' }];
this.model = {
snapshot,
namespaces,
@ -40,7 +42,9 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
this.version.type = 'enterprise';
this.renderComponent = () =>
render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
render(
hbs`<Recovery::Page::Snapshots::SnapshotManage @breadcrumbs={{this.breadcrumbs}} @model={{this.model}}/>`
);
});
const scenarios = [
{
@ -119,7 +123,7 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
// have values if the request was successful, but handling just in case.
data: { secret: {} },
}));
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
assert.dom(GENERAL.inputByAttr('manual-mount-path')).exists();
assert.dom(GENERAL.selectByAttr('mount')).doesNotExist();
});
@ -127,7 +131,7 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
test('it renders manual input if mounts request errors', async function (assert) {
assert.expect(14);
this.server.get('/sys/internal/ui/mounts', () => overrideResponse(403));
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
assert.dom(GENERAL.button('Type')).exists().hasText('Type');
assert.dom(GENERAL.inputByAttr('manual-mount-path')).exists().isDisabled();
assert.dom(GENERAL.selectByAttr('mount')).doesNotExist();
@ -161,18 +165,19 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
});
test('it displays loaded snapshot card', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
assert.dom(GENERAL.badge('status')).hasText('Ready', 'status badge renders');
});
test('it displays namespace selector for root namespace', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
assert.dom(GENERAL.selectByAttr('namespace')).exists('namespace selector is visible in root namespace');
});
test('it validates form fields before read/recover operations', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
// Try to read without selecting mount or resource path
await click(GENERAL.button('read'));
@ -181,14 +186,12 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
});
test('it clears form selections', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
await click(GENERAL.selectByAttr('namespace'));
await click('[data-option-index="1"]');
await click(GENERAL.selectByAttr('mount'));
await click('[data-option-index]');
await fillIn(GENERAL.inputByAttr('resourcePath'), 'test-path');
await click(GENERAL.button('clear'));
const nsSelect = find(GENERAL.selectByAttr('namespace'));
@ -201,8 +204,7 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
});
test('it displays error alert when read operation fails', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
await fillIn(GENERAL.inputByAttr('resourcePath'), 'nonexistent-secret');
await click(GENERAL.selectByAttr('mount'));
await click('[data-option-index="1.0"]');
@ -212,8 +214,7 @@ module('Integration | Component | recovery/snapshots/snapshot-manage', function
});
test('it toggles JSON view in read modal', async function (assert) {
await render(hbs`<Recovery::Page::Snapshots::SnapshotManage @model={{this.model}}/>`);
await this.renderComponent();
await fillIn(GENERAL.inputByAttr('resourcePath'), 'test-secret');
await click(GENERAL.selectByAttr('mount'));
await click('[data-option-index]');

View file

@ -17,17 +17,19 @@ module('Integration | Component | recovery/snapshots', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
this.breadcrumbs = [{ label: 'Secrets Recovery', route: 'vault.cluster.recovery.snapshots' }];
this.model = {
snapshots: [],
canLoadSnapshot: false,
};
this.renderComponent = () => render(hbs`<Recovery::Page::Snapshots @model={{this.model}}/>`);
this.renderComponent = () =>
render(hbs`<Recovery::Page::Snapshots @breadcrumbs={{this.breadcrumbs}} @model={{this.model}}/>`);
});
test('it displays raft empty state if showRaftStorageMessage is passed into model', async function (assert) {
this.model = { snapshots: { showRaftStorageMessage: true } };
await render(hbs`<Recovery::Page::Snapshots @model={{this.model}}/>`);
await this.renderComponent();
assert
.dom(GENERAL.emptyStateTitle)
@ -43,8 +45,8 @@ module('Integration | Component | recovery/snapshots', function (hooks) {
test('it displays empty state in CE', async function (assert) {
this.model = { snapshots: [], showCommunityMessage: true };
await this.renderComponent();
await render(hbs`<Recovery::Page::Snapshots @model={{this.model}}/>`);
assert
.dom(GENERAL.emptyStateTitle)
.hasText('Secrets Recovery is an enterprise feature', 'CE empty state title renders');
@ -64,9 +66,7 @@ module('Integration | Component | recovery/snapshots', function (hooks) {
test('it displays empty state in non root namespace', async function (assert) {
const nsService = this.owner.lookup('service:namespace');
nsService.setNamespace('test-ns');
await this.renderComponent();
assert
.dom(GENERAL.emptyStateTitle)
.hasText('Snapshot upload is restricted', 'non root namespace empty state title renders');

View file

@ -18,6 +18,8 @@ module('Integration | Component | seal-action', function (hooks) {
hooks.beforeEach(function () {
this.sealSuccess = sinon.spy(() => new Promise((resolve) => resolve({})));
this.sealError = sinon.stub().throws({ message: SEAL_WHEN_STANDBY_MSG });
this.flashMessages = this.owner.lookup('service:flash-messages');
this.flashSpy = sinon.spy(this.flashMessages, 'danger');
});
test('it handles success', async function (assert) {
@ -25,11 +27,11 @@ module('Integration | Component | seal-action', function (hooks) {
await render(hbs`<SealAction @onSeal={{action this.handleSeal}} />`);
// attempt seal
await click('[data-test-seal]');
await click(GENERAL.button('Seal'));
await click(GENERAL.confirmButton);
assert.ok(this.sealSuccess.calledOnce, 'called onSeal action');
assert.dom('[data-test-seal-error]').doesNotExist('Does not show error when successful');
assert.false(this.flashSpy.calledWith(SEAL_WHEN_STANDBY_MSG), 'Does not show error when successful');
});
test('it handles error', async function (assert) {
@ -37,10 +39,10 @@ module('Integration | Component | seal-action', function (hooks) {
await render(hbs`<SealAction @onSeal={{action this.handleSeal}} />`);
// attempt seal
await click('[data-test-seal]');
await click(GENERAL.button('Seal'));
await click(GENERAL.confirmButton);
assert.ok(this.sealError.calledOnce, 'called onSeal action');
assert.dom('[data-test-seal-error]').includesText(SEAL_WHEN_STANDBY_MSG, 'Shows error returned from API');
assert.true(this.flashSpy.calledWith(SEAL_WHEN_STANDBY_MSG), 'Shows error returned from API');
});
});

View file

@ -23,31 +23,11 @@ module('Integration | Component | sync | Secrets::LandingCta', function (hooks)
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
});
test('it should render promotional copy if feature is not activated', async function (assert) {
await render(hbs`<Secrets::LandingCta @isActivated={{false}} /> `, {
owner: this.engine,
});
assert
.dom(cta.summary)
.hasText(
'This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about Secrets Sync'
);
assert.dom(cta.link).hasText('Learn more about Secrets Sync');
assert.dom(cta.button).doesNotExist('does not render create destination button');
});
test('it should render CTA copy and action if feature is activated', async function (assert) {
test('it should render CTA if feature is activated', async function (assert) {
await render(hbs`<Secrets::LandingCta @isActivated={{true}} /> `, {
owner: this.engine,
});
assert
.dom(cta.summary)
.hasText(
'This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about Secrets Sync'
);
assert.dom(cta.link).hasText('Learn more about Secrets Sync');
assert.dom(cta.button).exists('it renders create destination button');
});
});

View file

@ -83,7 +83,6 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
this.setup();
await this.renderComponent();
assert.dom(overview.optInBanner.container).doesNotExist();
assert.dom(cta.summary).exists();
});
test('it should render header, tabs and toolbar for overview state if destinations exist', async function (assert) {
@ -96,7 +95,6 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
await this.renderComponent();
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets sync', 'Page title renders');
assert.dom(cta.summary).doesNotExist('CTA does not render');
assert.dom(tab('Overview')).hasText('Overview', 'Overview tab renders');
assert.dom(tab('Destinations')).hasText('Destinations', 'Destinations tab renders');
assert.dom(overview.createDestination).hasText('Create new destination', 'Toolbar action renders');
@ -118,7 +116,6 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets sync');
assert.dom(GENERAL.badge('Plus feature')).hasText('Plus feature', 'Plus feature badge renders');
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
assert.dom(cta.summary).exists();
});
test('it should show activation error if cluster is not Plus tier', async function (assert) {
@ -160,7 +157,6 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets sync');
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
assert.dom(cta.summary).exists();
});
test('it should show the opt-in banner without permissions to activate', async function (assert) {