mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
* Add secret engines intro page * add test coverage * hide header actions when showing wizard * add changelog entry * update copy and variable naming * fix tests Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
parent
21d95fb9fe
commit
8f6253cc0b
9 changed files with 374 additions and 171 deletions
3
changelog/_12343.txt
Normal file
3
changelog/_12343.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**UI Secret engines intro**: Onboarding intro which provides feature context to users.
|
||||
```
|
||||
|
|
@ -12,189 +12,212 @@
|
|||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
<:actions>
|
||||
<Hds::Button @text="Enable new engine" @icon="plus" @route="vault.cluster.secrets.enable" data-test-enable-engine />
|
||||
{{#unless this.showWizard}}
|
||||
{{#if this.hasOnlyDefaultEngines}}
|
||||
<Hds::Button
|
||||
@color="secondary"
|
||||
@icon="bulb"
|
||||
@text="New to Secret engines?"
|
||||
{{on "click" this.showIntroPage}}
|
||||
data-test-button="intro"
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Button @text="Enable new engine" @icon="plus" @route="vault.cluster.secrets.enable" data-test-enable-engine />
|
||||
{{/unless}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
||||
{{! Filters section }}
|
||||
<Hds::SegmentedGroup class="has-top-margin-m" as |SG|>
|
||||
<SG.TextInput
|
||||
@width="300px"
|
||||
@type="search"
|
||||
placeholder="Search by path"
|
||||
aria-label="Search"
|
||||
data-test-input-search="secret-engine-path"
|
||||
{{on "input" (fn this.setSearchText "path")}}
|
||||
{{#if this.showWizard}}
|
||||
<Wizard::SecretEngines::SecretEnginesWizard
|
||||
@isIntroModal={{this.shouldRenderIntroModal}}
|
||||
@onRefresh={{this.refreshSecretEngineList}}
|
||||
/>
|
||||
<SG.Dropdown @width="175px" as |D|>
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput @type="search" placeholder="Search" aria-label="Search" {{on "input" (fn this.setSearchText "type")}} />
|
||||
</D.Header>
|
||||
<D.ToggleButton @color="secondary" @text="Engine type" data-test-toggle-input="filter-by-engine-type" />
|
||||
{{#each this.secretEngineArrayByType as |type|}}
|
||||
<D.Checkbox
|
||||
value={{type.name}}
|
||||
checked={{includes type.name this.engineTypeFilters}}
|
||||
{{on "click" (fn this.filterByEngineType type.name)}}
|
||||
data-test-checkbox={{type.name}}
|
||||
><Hds::Icon @name={{type.icon}} @isInline={{true}} /> {{type.name}}</D.Checkbox>
|
||||
{{/each}}
|
||||
</SG.Dropdown>
|
||||
<SG.Dropdown @width="250px" as |D|>
|
||||
<D.ToggleButton @color="secondary" @text="Version" data-test-toggle-input="filter-by-engine-version" />
|
||||
{{#if this.engineTypeFilters.length}}
|
||||
{{else}}
|
||||
{{! Filters section }}
|
||||
<Hds::SegmentedGroup class="has-top-margin-m" as |SG|>
|
||||
<SG.TextInput
|
||||
@width="300px"
|
||||
@type="search"
|
||||
placeholder="Search by path"
|
||||
aria-label="Search"
|
||||
data-test-input-search="secret-engine-path"
|
||||
{{on "input" (fn this.setSearchText "path")}}
|
||||
/>
|
||||
<SG.Dropdown @width="175px" as |D|>
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput
|
||||
@type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
{{on "input" (fn this.setSearchText "version")}}
|
||||
{{on "input" (fn this.setSearchText "type")}}
|
||||
/>
|
||||
</D.Header>
|
||||
{{#each this.secretEngineArrayByVersions as |backend|}}
|
||||
<D.ToggleButton @color="secondary" @text="Engine type" data-test-toggle-input="filter-by-engine-type" />
|
||||
{{#each this.secretEngineArrayByType as |type|}}
|
||||
<D.Checkbox
|
||||
value={{backend.version}}
|
||||
checked={{includes backend.version this.engineVersionFilters}}
|
||||
{{on "click" (fn this.filterByEngineVersion backend.version)}}
|
||||
data-test-checkbox={{backend.version}}
|
||||
>
|
||||
{{backend.version}}
|
||||
</D.Checkbox>
|
||||
value={{type.name}}
|
||||
checked={{includes type.name this.engineTypeFilters}}
|
||||
{{on "click" (fn this.filterByEngineType type.name)}}
|
||||
data-test-checkbox={{type.name}}
|
||||
><Hds::Icon @name={{type.icon}} @isInline={{true}} /> {{type.name}}</D.Checkbox>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<D.Description class="has-top-padding-s" @text="Select an engine type first to filter by versions." />
|
||||
{{/if}}
|
||||
</SG.Dropdown>
|
||||
</Hds::SegmentedGroup>
|
||||
</SG.Dropdown>
|
||||
<SG.Dropdown @width="250px" as |D|>
|
||||
<D.ToggleButton @color="secondary" @text="Version" data-test-toggle-input="filter-by-engine-version" />
|
||||
{{#if this.engineTypeFilters.length}}
|
||||
<D.Header @hasDivider={{true}}>
|
||||
<SG.TextInput
|
||||
@type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
{{on "input" (fn this.setSearchText "version")}}
|
||||
/>
|
||||
</D.Header>
|
||||
{{#each this.secretEngineArrayByVersions as |backend|}}
|
||||
<D.Checkbox
|
||||
value={{backend.version}}
|
||||
checked={{includes backend.version this.engineVersionFilters}}
|
||||
{{on "click" (fn this.filterByEngineVersion backend.version)}}
|
||||
data-test-checkbox={{backend.version}}
|
||||
>
|
||||
{{backend.version}}
|
||||
</D.Checkbox>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<D.Description class="has-top-padding-s" @text="Select an engine type first to filter by versions." />
|
||||
{{/if}}
|
||||
</SG.Dropdown>
|
||||
</Hds::SegmentedGroup>
|
||||
|
||||
<Hds::Layout::Flex @gap="8" class="has-top-margin-xs has-bottom-margin-xs" @align="center">
|
||||
{{#if (and (not this.engineTypeFilters) (not this.engineVersionFilters))}}
|
||||
<Hds::Text::Body class="has-top-padding-xs">No filters applied.</Hds::Text::Body>
|
||||
<Hds::Layout::Flex @gap="8" class="has-top-margin-xs has-bottom-margin-xs" @align="center">
|
||||
{{#if (and (not this.engineTypeFilters) (not this.engineVersionFilters))}}
|
||||
<Hds::Text::Body class="has-top-padding-xs">No filters applied.</Hds::Text::Body>
|
||||
{{else}}
|
||||
<Hds::Text::Body>Filters applied:</Hds::Text::Body>
|
||||
{{#each this.engineTypeFilters as |type|}}
|
||||
<Hds::Tag @text={{type}} @onDismiss={{fn this.filterByEngineType type}} data-test-button={{type}} />
|
||||
{{/each}}
|
||||
{{#each this.engineVersionFilters as |version|}}
|
||||
<Hds::Tag @text={{version}} @onDismiss={{fn this.filterByEngineVersion version}} data-test-button={{version}} />
|
||||
{{/each}}
|
||||
<Hds::Button
|
||||
@text="Clear all"
|
||||
@color="tertiary"
|
||||
@icon="x"
|
||||
@size="small"
|
||||
data-test-button="Clear all"
|
||||
{{on "click" this.clearAllFilters}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Layout::Flex>
|
||||
{{! End Filters Section }}
|
||||
|
||||
{{! Table Section }}
|
||||
{{#if this.sortedDisplayableBackends}}
|
||||
<ListTable
|
||||
class="has-top-margin-xs"
|
||||
@columns={{this.tableColumns}}
|
||||
@selectionKey="path"
|
||||
@data={{this.sortedDisplayableBackends}}
|
||||
@onSelectionChange={{this.updateSelectedItems}}
|
||||
>
|
||||
<:selectedItems>
|
||||
{{#if this.selectedItems}}
|
||||
<Hds::Layout::Flex
|
||||
@gap="8"
|
||||
@direction="row"
|
||||
@justify="end"
|
||||
@align="center"
|
||||
class="has-bottom-margin-s has-top-margin-negative-xxl"
|
||||
@isInline="true"
|
||||
>
|
||||
<Hds::Text::Body role="status" @tag="p" @size="200" @color="foreground-primary">
|
||||
{{this.selectedItems.length}}
|
||||
selected out of
|
||||
{{this.sortedDisplayableBackends.length}}
|
||||
</Hds::Text::Body>
|
||||
<Hds::Button
|
||||
@text="Disable engines"
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
|
||||
/>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
</:selectedItems>
|
||||
<:customTableItem as |itemData|>
|
||||
{{#let (this.getEngineResourceData itemData.path) as |backendData|}}
|
||||
<Hds::TooltipButton
|
||||
aria-label="Type of backend"
|
||||
@text={{this.generateToolTipText backendData}}
|
||||
data-test-tooltip="Backend type"
|
||||
isInline={{true}}
|
||||
class="is-v-centered"
|
||||
>
|
||||
<Hds::Icon @name={{if backendData.icon backendData.icon "lock"}} />
|
||||
</Hds::TooltipButton>
|
||||
{{#if backendData.isSupportedBackend}}
|
||||
<Hds::Link::Inline
|
||||
@route={{backendData.backendLink}}
|
||||
@model={{backendData.id}}
|
||||
@color="secondary"
|
||||
class="has-text-weight-semibold"
|
||||
>{{backendData.path}}</Hds::Link::Inline>
|
||||
{{else}}
|
||||
{{backendData.path}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</:customTableItem>
|
||||
|
||||
<:popupMenu as |rowData|>
|
||||
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backendData.backendConfigurationLink}}
|
||||
@model={{backendData.id}}
|
||||
data-test-popup-menu="View configuration"
|
||||
@icon="settings"
|
||||
>View configuration</dd.Interactive>
|
||||
{{#if (not-eq backendData.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backendData)}}
|
||||
data-test-popup-menu="Delete"
|
||||
@icon="trash"
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/let}}
|
||||
</:popupMenu>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<Hds::Text::Body>Filters applied:</Hds::Text::Body>
|
||||
{{#each this.engineTypeFilters as |type|}}
|
||||
<Hds::Tag @text={{type}} @onDismiss={{fn this.filterByEngineType type}} data-test-button={{type}} />
|
||||
{{/each}}
|
||||
{{#each this.engineVersionFilters as |version|}}
|
||||
<Hds::Tag @text={{version}} @onDismiss={{fn this.filterByEngineVersion version}} data-test-button={{version}} />
|
||||
{{/each}}
|
||||
<Hds::Button
|
||||
@text="Clear all"
|
||||
@color="tertiary"
|
||||
@icon="x"
|
||||
@size="small"
|
||||
data-test-button="Clear all"
|
||||
{{on "click" this.clearAllFilters}}
|
||||
<EmptyState @title="No Secrets engines found" />
|
||||
{{/if}}
|
||||
{{! End Table Section }}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Layout::Flex>
|
||||
{{! End Filters Section }}
|
||||
|
||||
{{! Table Section }}
|
||||
{{#if this.sortedDisplayableBackends}}
|
||||
<ListTable
|
||||
class="has-top-margin-xs"
|
||||
@columns={{this.tableColumns}}
|
||||
@selectionKey="path"
|
||||
@data={{this.sortedDisplayableBackends}}
|
||||
@onSelectionChange={{this.updateSelectedItems}}
|
||||
>
|
||||
<:selectedItems>
|
||||
{{#if this.selectedItems}}
|
||||
<Hds::Layout::Flex
|
||||
@gap="8"
|
||||
@direction="row"
|
||||
@justify="end"
|
||||
@align="center"
|
||||
class="has-bottom-margin-s has-top-margin-negative-xxl"
|
||||
@isInline="true"
|
||||
>
|
||||
<Hds::Text::Body role="status" @tag="p" @size="200" @color="foreground-primary">
|
||||
{{this.selectedItems.length}}
|
||||
selected out of
|
||||
{{this.sortedDisplayableBackends.length}}
|
||||
</Hds::Text::Body>
|
||||
<Hds::Button
|
||||
@text="Disable engines"
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
|
||||
/>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
</:selectedItems>
|
||||
<:customTableItem as |itemData|>
|
||||
{{#let (this.getEngineResourceData itemData.path) as |backendData|}}
|
||||
<Hds::TooltipButton
|
||||
aria-label="Type of backend"
|
||||
@text={{this.generateToolTipText backendData}}
|
||||
data-test-tooltip="Backend type"
|
||||
isInline={{true}}
|
||||
class="is-v-centered"
|
||||
>
|
||||
<Hds::Icon @name={{if backendData.icon backendData.icon "lock"}} />
|
||||
</Hds::TooltipButton>
|
||||
{{#if backendData.isSupportedBackend}}
|
||||
<Hds::Link::Inline
|
||||
@route={{backendData.backendLink}}
|
||||
@model={{backendData.id}}
|
||||
@color="secondary"
|
||||
class="has-text-weight-semibold"
|
||||
>{{backendData.path}}</Hds::Link::Inline>
|
||||
{{else}}
|
||||
{{backendData.path}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</:customTableItem>
|
||||
|
||||
<:popupMenu as |rowData|>
|
||||
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backendData.backendConfigurationLink}}
|
||||
@model={{backendData.id}}
|
||||
data-test-popup-menu="View configuration"
|
||||
@icon="settings"
|
||||
>View configuration</dd.Interactive>
|
||||
{{#if (not-eq backendData.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backendData)}}
|
||||
data-test-popup-menu="Delete"
|
||||
@icon="trash"
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/let}}
|
||||
</:popupMenu>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<EmptyState @title="No Secrets engines found" />
|
||||
{{/if}}
|
||||
{{! End Table Section }}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.enginesToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in these engines will be permanently deleted."
|
||||
@confirmTitle="Disable engines?"
|
||||
@onClose={{fn (mut this.enginesToDisable) null}}
|
||||
@onConfirm={{perform this.disableMultipleEngines this.enginesToDisable}}
|
||||
/>
|
||||
{{#if this.enginesToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in these engines will be permanently deleted."
|
||||
@confirmTitle="Disable engines?"
|
||||
@onClose={{fn (mut this.enginesToDisable) null}}
|
||||
@onConfirm={{perform this.disableMultipleEngines this.enginesToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
@ -18,6 +18,8 @@ import type NamespaceService from 'vault/services/namespace';
|
|||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard';
|
||||
|
||||
/**
|
||||
* @module SecretEngineList handles the display of the list of secret engines, including the filtering.
|
||||
|
|
@ -35,11 +37,12 @@ interface Args {
|
|||
}
|
||||
|
||||
export default class SecretEngineList extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@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 namespace: NamespaceService;
|
||||
@service declare readonly wizard: WizardService;
|
||||
|
||||
@tracked secretEngineOptions: Array<string> | [] = [];
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
|
@ -55,6 +58,9 @@ export default class SecretEngineList extends Component<Args> {
|
|||
|
||||
@tracked selectedItems = Array<string>();
|
||||
|
||||
@tracked shouldRenderIntroModal = false;
|
||||
wizardId = WIZARD_ID;
|
||||
|
||||
tableColumns = [
|
||||
{
|
||||
key: 'path',
|
||||
|
|
@ -189,6 +195,32 @@ export default class SecretEngineList extends Component<Args> {
|
|||
}));
|
||||
}
|
||||
|
||||
// The backend does not directly indicate which engines were mounted by default and which have been mounted by the user
|
||||
// Currently the cubbyhole/, sys/, identity/ engines are mounted by default. (secret/ is mounted in dev mode as well)
|
||||
// The sys/ and identity/ engines are non-displayable engines.
|
||||
// While not ideal, we can check whether there are other engines than the default cubbyhole/ engine
|
||||
// to determine whether we should show the intro page
|
||||
get hasOnlyDefaultEngines() {
|
||||
const listedEngines = this.sortedDisplayableBackends;
|
||||
return !listedEngines.length || (listedEngines.length === 1 && listedEngines[0]?.path === 'cubbyhole/');
|
||||
}
|
||||
|
||||
get showWizard() {
|
||||
return !this.wizard.isDismissed(this.wizardId) && this.hasOnlyDefaultEngines;
|
||||
}
|
||||
|
||||
@action
|
||||
showIntroPage() {
|
||||
// Reset the wizard dismissal state to allow re-entering the wizard
|
||||
this.wizard.reset(this.wizardId);
|
||||
this.shouldRenderIntroModal = true;
|
||||
}
|
||||
|
||||
@action
|
||||
refreshSecretEngineList() {
|
||||
this.router.refresh('vault.cluster.secrets.backends');
|
||||
}
|
||||
|
||||
// Returns engine resource data for a given engine path, needed to get icon and other metadata from SecretEnginesResource
|
||||
getEngineResourceData = (enginePath: string) => {
|
||||
return this.displayableBackends.find((backend) => backend.path === enginePath);
|
||||
|
|
|
|||
30
ui/app/components/wizard/secret-engines/intro.hbs
Normal file
30
ui/app/components/wizard/secret-engines/intro.hbs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Wizard::IntroContent
|
||||
@setupTime="15 min"
|
||||
@description="Secrets engines in Vault securely manage, store, and dynamically generate secrets, certificates, and encryption keys."
|
||||
@imageAlt="Flow of secret engine access control by policy"
|
||||
@imageCaption="Access to secrets within an engine is governed by policies. Users must be granted explicit access to the engine's unique path."
|
||||
@imageSrc={{img-path "~/secret-engines-intro.png"}}
|
||||
>
|
||||
<:features>
|
||||
<Wizard::IntroContent::Feature @icon="service">
|
||||
Store
|
||||
<strong>static</strong>
|
||||
application passwords (KV), generate dynamic, time-limited credentials for databases and cloud platforms.
|
||||
</Wizard::IntroContent::Feature>
|
||||
<Wizard::IntroContent::Feature @icon="replication-direct">
|
||||
One cluster can support multiple instances of the same engine type, with a
|
||||
<strong>unique path</strong>
|
||||
and granular access control.
|
||||
</Wizard::IntroContent::Feature>
|
||||
<Wizard::IntroContent::Feature @icon="lock">
|
||||
You’ll need a
|
||||
<strong>privileged account</strong>
|
||||
on the target system (root DB user or AWS Admin) that Vault can use to generate dynamic credentials, and appropriate
|
||||
ACL policies to control which users can interact with those secrets.
|
||||
</Wizard::IntroContent::Feature>
|
||||
</:features>
|
||||
</Wizard::IntroContent>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Wizard @wizardId={{this.wizardId}} @isModal={{@isIntroModal}} @title="Secrets engines" @onDismiss={{this.onDismiss}}>
|
||||
<:intro>
|
||||
<Wizard::SecretEngines::Intro />
|
||||
</:intro>
|
||||
<:introActions>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@icon="plus"
|
||||
@text="Enable a Secret engine"
|
||||
{{on "click" (fn this.onIntroChange false)}}
|
||||
data-test-button="intro"
|
||||
/>
|
||||
<Hds::Button
|
||||
@color="secondary"
|
||||
@text={{if @isIntroModal "Close" "Skip"}}
|
||||
{{on "click" this.onDismiss}}
|
||||
data-test-button="Skip"
|
||||
/>
|
||||
<Hds::Link::Standalone
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@text="View documentation"
|
||||
@href={{doc-link "/vault/tutorials/get-started/understand-static-dynamic-secrets"}}
|
||||
class="has-left-margin-m"
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</:introActions>
|
||||
</Wizard>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
|
||||
interface Args {
|
||||
isIntroModal: boolean;
|
||||
onRefresh: CallableFunction;
|
||||
}
|
||||
|
||||
export const WIZARD_ID = 'secret-engines';
|
||||
|
||||
export default class WizardSecretEnginesWizardComponent extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly wizard: WizardService;
|
||||
|
||||
wizardId = WIZARD_ID;
|
||||
|
||||
@action
|
||||
onDismiss() {
|
||||
this.wizard.dismiss(this.wizardId);
|
||||
this.args.onRefresh();
|
||||
}
|
||||
|
||||
@action
|
||||
onIntroChange(visible: boolean) {
|
||||
this.wizard.setIntroVisible(this.wizardId, visible);
|
||||
}
|
||||
}
|
||||
BIN
ui/public/images/secret-engines-intro.png
Normal file
BIN
ui/public/images/secret-engines-intro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from 'vault/tests/helpers/commands';
|
||||
import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import localStorage from 'vault/lib/local-storage';
|
||||
|
||||
module('Acceptance | secret-engine list view', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
|
@ -32,9 +33,11 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
await click(SES.crumb(enginePath));
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
hooks.beforeEach(async function () {
|
||||
this.uid = uuidv4();
|
||||
return login();
|
||||
await login();
|
||||
// dismiss wizard
|
||||
localStorage.setItem('dismissed-wizards', ['secret-engines']);
|
||||
});
|
||||
|
||||
// the new API service camelizes response keys, so this tests is to assert that does NOT happen when we re-implement it
|
||||
|
|
@ -146,6 +149,7 @@ module('Acceptance | secret-engine list view', function (hooks) {
|
|||
|
||||
await runCmd([`write sys/namespaces/${this.namespace} -force`]);
|
||||
await loginNs(this.namespace);
|
||||
localStorage.setItem('dismissed-wizards', ['secret-engines']);
|
||||
await visit(`/vault/secrets-engines?namespace=${this.namespace}`);
|
||||
await click(`${GENERAL.tableData('cubbyhole/', 'path')} a`);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
this.version = this.owner.lookup('service:version');
|
||||
this.router = this.owner.lookup('service:router');
|
||||
this.router.transitionTo = sinon.stub();
|
||||
this.router.refresh = sinon.stub();
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
|
|
@ -44,6 +45,11 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
];
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
// ensure clean state
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
test('it allows you to disable an engine', async function (assert) {
|
||||
const enginePath = 'kv-test';
|
||||
this.server.delete(`sys/mounts/${enginePath}`, () => {
|
||||
|
|
@ -172,4 +178,36 @@ module('Integration | Component | secret-engine/list', function (hooks) {
|
|||
.dom(GENERAL.tableData('aws-1/', 'path'))
|
||||
.hasClass('text-overflow-ellipsis', 'secret engine name has text overflow class ');
|
||||
});
|
||||
|
||||
test('it shows the intro page when only default engines are enabled', async function (assert) {
|
||||
// Only cubbyhole engine exists (default engine)
|
||||
const defaultEngines = [createSecretsEngine(undefined, 'cubbyhole', 'cubbyhole')];
|
||||
this.secretEngineModels = defaultEngines;
|
||||
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
|
||||
assert.dom('[data-test-intro]').exists('Intro page is shown');
|
||||
assert.dom(GENERAL.button('intro')).exists('Shows intro button');
|
||||
assert.dom(GENERAL.button('Skip')).exists('Shows skip button');
|
||||
});
|
||||
|
||||
test('it does not show the intro page when other engines exist', async function (assert) {
|
||||
// Has engines beyond the default cubbyhole
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
|
||||
assert.dom('[data-test-intro]').doesNotExist('Intro modal is not shown when engines exist');
|
||||
assert.dom(GENERAL.button('intro')).doesNotExist('Intro button is not shown');
|
||||
});
|
||||
|
||||
test('it can show the intro modal after dismissal', async function (assert) {
|
||||
const defaultEngines = [createSecretsEngine(undefined, 'cubbyhole', 'cubbyhole')];
|
||||
this.secretEngineModels = defaultEngines;
|
||||
|
||||
await render(hbs`<SecretEngine::List @secretEngines={{this.secretEngineModels}} />`);
|
||||
await click(GENERAL.button('Skip'));
|
||||
assert.dom('[data-test-intro]').doesNotExist('Intro is dismissed');
|
||||
|
||||
await click(GENERAL.button('intro'));
|
||||
assert.dom('[data-test-intro]').exists('Intro can be shown again after reset');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue