[UI][VAULT-41959] Secrets sidebar (#12118) (#12175)

* WIP secrets sidebar

* Remove unwanted text and put some things back..

* Add secrets templates for sidebar

* Fix tests

* Update more Secrets navlinks

* Add copywrite headers

* Creates secrets.hbs so its the parent route

* Update secrets comment

* Update component name

* Update sidebar to use helper

* Secrets sync breadcrumbs

* Address feedback~

* Use enum and add helper test

* Fix links!

Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
Vault Automation 2026-02-04 17:36:06 -05:00 committed by GitHub
parent 3d9a5c5d7d
commit 3842e8df73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 354 additions and 110 deletions

View file

@ -169,10 +169,19 @@ export default class App extends Application {
'api',
'capabilities',
'version',
// services needed for Secrets sidebar component
'current-cluster',
'permissions',
'-portal',
'namespace',
],
externalRoutes: {
kvSecretOverview: 'vault.cluster.secrets.backend.kv.secret.index',
clientCountOverview: 'vault.cluster.clients',
// routes needed for Secrets sidebar component
secrets: 'vault.cluster.secrets',
sync: 'vault.cluster.sync',
vault: 'vault.cluster',
},
},
},

View file

@ -0,0 +1,8 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Sidebar::Nav::Secrets />
{{outlet}}

View file

@ -7,15 +7,7 @@
<Nav.Title data-test-sidebar-nav-heading="Vault">Vault</Nav.Title>
<Nav.Link @route="vault.cluster.dashboard" @text="Dashboard" data-test-sidebar-nav-link="Dashboard" />
<Nav.Link @route="vault.cluster.secrets" @text="Secrets Engines" data-test-sidebar-nav-link="Secrets Engines" />
{{#if this.showSecretsSync}}
<Nav.Link
@route="vault.cluster.sync"
@text="Secrets Sync"
@badge={{if this.flags.isHvdManaged "Plus" ""}}
data-test-sidebar-nav-link="Secrets Sync"
/>
{{/if}}
<Nav.Link @route="vault.cluster.secrets" @text="Secrets" @hasSubItems={{true}} data-test-sidebar-nav-link="Secrets" />
{{#if (display-nav-item this.navSection.resilienceAndRecovery)}}
<Nav.Link

View file

@ -0,0 +1,31 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::AppSideNav::Portal @ariaLabel="Secrets Navigation Links" data-test-sidebar-nav-panel="Secrets" as |Nav|>
<Nav.BackLink
@route={{if @isEngine "vault" "vault.cluster"}}
@isRouteExternal={{@isEngine}}
@current-when={{false}}
@icon="arrow-left"
@text="Back to main navigation"
data-test-sidebar-nav-link="Back to main navigation"
/>
<Nav.Title data-test-sidebar-nav-heading="Secrets">Secrets</Nav.Title>
<Nav.Link
@route={{if @isEngine "secrets" "vault.cluster.secrets"}}
@isRouteExternal={{@isEngine}}
@text="Secrets engines"
data-test-sidebar-nav-link="Secrets engines"
/>
{{#if (display-nav-item this.routeName.secretsSync)}}
<Nav.Link
@route={{if @isEngine "sync" "vault.cluster.sync"}}
@isRouteExternal={{@isEngine}}
@text="Secrets sync"
@badge={{if this.flags.isHvdManaged "Plus" ""}}
data-test-sidebar-nav-link="Secrets sync"
/>
{{/if}}
</Hds::AppSideNav::Portal>

View file

@ -0,0 +1,22 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type FlagsService from 'vault/services/flags';
import { RouteName } from 'core/helpers/display-nav-item';
interface Args {
isEngine?: boolean;
}
export default class SidebarNavSecretsComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
routeName = {
secretsSync: RouteName.SECRETS_SYNC,
};
}

View file

@ -15,6 +15,7 @@ import type PermissionsService from 'vault/services/permissions';
import type FlagsService from 'vault/services/flags';
export enum RouteName {
SECRETS_SYNC = 'secrets-sync',
SECRETS_RECOVERY = 'secrets-recovery',
SEAL = 'seal',
REPLICATION = 'replication',
@ -36,10 +37,13 @@ export default class NavBar extends Helper {
@service declare readonly flags: FlagsService;
compute([navItem]: string[]) {
const { SECRETS_RECOVERY, SEAL, REPLICATION, VAULT_USAGE, LICENSE } = RouteName;
const { SECRETS_RECOVERY, SEAL, REPLICATION, VAULT_USAGE, LICENSE, SECRETS_SYNC } = RouteName;
const { RESILIENCE_AND_RECOVERY, REPORTING, CLIENT_COUNT } = NavSection;
switch (navItem) {
// secrets sync nav items
case SECRETS_SYNC:
return this.supportsSecretsSync;
// client count nav items
case CLIENT_COUNT:
return this.supportsClientCount;
@ -131,6 +135,21 @@ export default class NavBar extends Helper {
!this.version.hasPKIOnly
);
}
get supportsSecretsSync() {
// always show for HVD managed clusters
if (this.flags.isHvdManaged) return true;
if (this.flags.secretsSyncIsActivated) {
// activating the feature requires different permissions than using the feature.
// we want to show the link to allow activation regardless of permissions to sys/sync
// and only check permissions if the feature has been activated
return this.permissions.hasNavPermission('sync');
}
// otherwise we show the link depending on whether or not the feature exists
return this.version.hasSecretsSync;
}
}
export function computeNavBar(context: object, navItem: string): boolean {

View file

@ -0,0 +1,6 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/sidebar/nav/secrets';

View file

@ -7,7 +7,8 @@
@icon={{get (find-by "type" @destination.type (sync-destinations)) "icon"}}
@title={{@destination.name}}
@breadcrumbs={{array
(hash label="Secrets Sync" route="secrets.overview")
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
(hash label="Secrets sync" route="secrets.overview")
(hash label="Destinations" route="secrets.destinations")
(hash label="Destination")
}}

View file

@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Page::Header @title="Secrets Sync">
<Page::Header @title="Secrets sync">
<:breadcrumbs>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</:breadcrumbs>
<:badges>
{{#if this.flags.isHvdManaged}}
<Hds::Badge @text="Plus feature" @color="highlight" @size="large" data-test-badge="Plus feature" />

View file

@ -14,4 +14,16 @@ interface Args {
export default class LandingCtaComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
breadcrumbs = [
{
label: 'Vault',
route: 'vault',
icon: 'vault',
linkExternal: true,
},
{
label: 'Secrets sync',
},
];
}

View file

@ -55,22 +55,24 @@ export default class DestinationsCreateForm extends Component<Args> {
? {
title: `Create Destination for ${typeDisplayName}`,
breadcrumbs: [
{ label: 'Secrets Sync', route: 'secrets.overview' },
{ label: 'Select Destination', route: 'secrets.destinations.create' },
{ label: 'Create Destination' },
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
{ label: 'Secrets sync', route: 'secrets.overview' },
{ label: 'Select destination', route: 'secrets.destinations.create' },
{ label: 'Create destination' },
],
}
: {
title: `Edit ${name}`,
breadcrumbs: [
{ label: 'Secrets Sync', route: 'secrets.overview' },
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
{ label: 'Secrets sync', route: 'secrets.overview' },
{ label: 'Destinations', route: 'secrets.destinations' },
{
label: 'Destination',
route: 'secrets.destinations.destination.secrets',
model: { name, type },
},
{ label: 'Edit Destination' },
{ label: 'Edit destination' },
],
};
}

View file

@ -5,7 +5,11 @@
<SyncHeader
@title="Select a Destination"
@breadcrumbs={{array (hash label="Secrets Sync" route="secrets.overview") (hash label="Select Destination")}}
@breadcrumbs={{array
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
(hash label="Secrets sync" route="secrets.overview")
(hash label="Select Destination")
}}
/>
{{#each (array "cloud" "dev-tools") as |category|}}

View file

@ -41,7 +41,7 @@
{{/if}}
{{#if @destinations}}
<SyncHeader @title="Secrets Sync" />
<SyncHeader @title="Secrets sync" @breadcrumbs={{this.breadcrumbs}} />
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
<nav class="tabs" aria-label="destination tabs">

View file

@ -48,6 +48,18 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
}
}
breadcrumbs = [
{
label: 'Vault',
route: 'vault',
icon: 'vault',
linkExternal: true,
},
{
label: 'Secrets sync',
},
];
fetchAssociationsForDestinations = task(this, {}, async (page = 1) => {
try {
const total = page * this.pageSize;

View file

@ -14,8 +14,20 @@ export default class SyncEngine extends Engine {
modulePrefix = modulePrefix;
Resolver = Resolver;
dependencies = {
services: ['flash-messages', 'flags', 'app-router', 'store', 'api', 'capabilities', 'version'],
externalRoutes: ['kvSecretOverview', 'clientCountOverview'],
services: [
'flash-messages',
'flags',
'app-router',
'store',
'api',
'capabilities',
'version',
'-portal',
'permissions',
'current-cluster',
'namespace',
],
externalRoutes: ['kvSecretOverview', 'clientCountOverview', 'vault', 'secrets', 'sync'],
};
}

View file

@ -0,0 +1,8 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Sidebar::Nav::Secrets @isEngine={{true}} />
{{outlet}}

View file

@ -32,7 +32,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
test('root-only nav items are unavailable', async function (assert) {
await login();
['Dashboard', 'Secrets Engines', 'Access', 'Operational tools'].forEach((nav) => {
['Dashboard', 'Secrets', 'Access', 'Operational tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item in chroot listener`);
});
// Client count is not root-only, but it is hidden for chroot
@ -54,7 +54,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
const userDefault = await runCmd(createTokenCmd());
await loginNs(namespace, userDefault);
[('Dashboard', 'Secrets Engines', 'Access', 'Operational tools')].forEach((nav) => {
[('Dashboard', 'Secrets', 'Access', 'Operational tools')].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item for user with default policy`);
});
['Client count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
@ -84,7 +84,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
);
await loginNs(namespace, reader);
['Dashboard', 'Secrets Engines', 'Access', 'Operational tools'].forEach((nav) => {
['Dashboard', 'Secrets', 'Access', 'Operational tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item for user with read access policy`);
});
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client count'].forEach((nav) => {
@ -116,7 +116,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
await runCmd(`write sys/namespaces/child -f`, false);
await loginNs(namespace, childReader);
['Dashboard', 'Secrets Engines', 'Access', 'Operational tools'].forEach((nav) => {
['Dashboard', 'Secrets', 'Access', 'Operational tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item`);
});
['Client count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
@ -125,7 +125,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
await loginNs(`${namespace}/child`, childReader);
['Dashboard', 'Secrets Engines', 'Access', 'Operational tools'].forEach((nav) => {
['Dashboard', 'Secrets', 'Access', 'Operational tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item within child namespace`);
});
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client count'].forEach((nav) => {

View file

@ -24,8 +24,10 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
test(`it should render enterprise only navigation links`, async function (assert) {
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
await click(GENERAL.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
assert.strictEqual(currentURL(), '/vault/sync/secrets/overview', 'Sync route renders');
await click(GENERAL.navLink('Back to main navigation'));
await click(GENERAL.navLink('Client count'));
assert.dom(panel('Client count')).exists('Client count nav panel renders');

View file

@ -535,7 +535,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert.dom('[data-test-edit-link]').hasText('Edit configuration', 'Edit button exists with correct text');
// Check with restricted permissions
await login(token);
await click('[data-test-sidebar-nav-link="Secrets Engines"]');
await click(GENERAL.navLink('Secrets'));
assert.dom(GENERAL.tableData(`${backend}/`, 'path')).exists('Shows backend on secret list page');
await navToConnection(backend, connection);
assert.strictEqual(

View file

@ -573,7 +573,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks)
setupApplicationTest(hooks);
const navToEngine = async (backend) => {
await click(GENERAL.navLink('Secrets Engines'));
await click(GENERAL.navLink('Secrets'));
return await click(`${GENERAL.tableData(`${backend}/`, 'path')} a`);
};

View file

@ -59,7 +59,7 @@ module('Acceptance | sidebar navigation', function (hooks) {
const links = [
{ label: 'Raft Storage', route: '/vault/storage/raft' },
{ label: 'Secrets Engines', route: '/vault/secrets-engines' },
{ label: 'Secrets', route: '/vault/secrets-engines' },
{ label: 'Dashboard', route: '/vault/dashboard' },
];

View file

@ -30,7 +30,7 @@ module('Acceptance | sync | destination (singular)', function (hooks) {
test('it should transition to overview route via breadcrumb', async function (assert) {
await visit('vault/sync/secrets/destinations/aws-sm/destination-aws/secrets');
await click(ts.breadcrumbAtIdx(0));
await click(ts.breadcrumbAtIdx(1));
assert.strictEqual(
currentURL(),
'/vault/sync/secrets/overview',
@ -39,7 +39,8 @@ module('Acceptance | sync | destination (singular)', function (hooks) {
});
test('it should transition to correct routes when performing actions', async function (assert) {
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(GENERAL.tab('Destinations'));
await click(GENERAL.listItemLink);
assert.dom(GENERAL.tab('Secrets')).hasClass('active', 'Secrets hdsTab is active');

View file

@ -41,7 +41,8 @@ module('Acceptance | sync | destinations (plural)', function (hooks) {
},
};
});
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(ts.cta.button);
await click(ts.selectType('aws-sm'));
await fillIn(ts.inputByAttr('name'), 'foo');
@ -71,7 +72,8 @@ module('Acceptance | sync | destinations (plural)', function (hooks) {
};
});
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(ts.cta.button);
await click(ts.selectType(type));

View file

@ -68,7 +68,8 @@ module('Acceptance | sync | overview', function (hooks) {
test('it should transition to correct routes when performing actions', async function (assert) {
syncScenario(this.server);
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(ts.destinations.list.create);
await click(ts.createCancel);
await click(ts.overviewCard.actionText('Create new'));
@ -77,7 +78,7 @@ module('Acceptance | sync | overview', function (hooks) {
await click(ts.overview.table.actionToggle(0));
await click(ts.overview.table.action('sync'));
await click(GENERAL.cancelButton);
await click(ts.breadcrumbLink('Secrets Sync'));
await click(ts.breadcrumbLink('Secrets sync'));
await waitFor(ts.overview.table.actionToggle(0));
await click(ts.overview.table.actionToggle(0));
await click(ts.overview.table.action('details'));
@ -196,7 +197,8 @@ module('Acceptance | sync | overview', function (hooks) {
// confirm we're in admin/foo
assert.dom('[data-test-badge-namespace]').hasText('foo');
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(ts.overview.optInBanner.enable);
await click(ts.overview.activationModal.checkbox);
await click(ts.overview.activationModal.confirm);
@ -247,7 +249,8 @@ module('Acceptance | sync | overview', function (hooks) {
// confirm we're in admin/foo
assert.dom('[data-test-badge-namespace]').hasText('foo');
await click(ts.navLink('Secrets Sync'));
await click(GENERAL.navLink('Secrets'));
await click(GENERAL.navLink('Secrets sync'));
await click(ts.overview.optInBanner.enable);
await click(ts.overview.activationModal.checkbox);
await click(ts.overview.activationModal.confirm);

View file

@ -61,8 +61,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
test('it should render nav links', async function (assert) {
const links = [
'Dashboard',
'Secrets Engines',
'Secrets Sync',
'Secrets',
'Access',
'Operational tools',
'Resilience and recovery',
@ -130,70 +129,6 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
assert.dom(GENERAL.navHeading('Client Counts')).doesNotExist('Client count link is hidden.');
});
test('it should render badge for promotional links on managed clusters', async function (assert) {
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
const promotionalLinks = ['Secrets Sync'];
stubFeaturesAndPermissions(this.owner, true, true);
await renderComponent();
promotionalLinks.forEach((link) => {
assert.dom(GENERAL.navLink(link)).hasText(`${link} Plus`, `${link} link renders Plus badge`);
});
});
// Secrets Sync side nav link has multiple combinations of three variables to test:
// 1. cluster type: enterprise (on and off license), HVD managed or community
// 2. activation status: activated or not
// 3. permissions: policy access to sys/sync routes or not
test('community: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, false, false);
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).doesNotExist();
});
test('ent but feature is not on license: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, []);
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).doesNotExist();
});
test('ent (on license), activated and permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync']);
this.flags.activatedFlags = ['secrets-sync'];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).exists();
});
test('ent (on license), activated and no permissions: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync'], false);
this.flags.activatedFlags = ['secrets-sync'];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).doesNotExist();
});
test('ent (on license), not activated and permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync']);
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).exists();
});
test('ent (on license), not activated and no permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync'], false);
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).exists();
});
test('hvd managed: it shows Secrets Sync nav link regardless of activation status or permissions', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, [], false);
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets Sync')).exists();
});
test('it does NOT show Secrets Recovery when user is in HVD admin namespace', async function (assert) {
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];

View file

@ -0,0 +1,120 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { stubFeaturesAndPermissions } from 'vault/tests/helpers/components/sidebar-nav';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { allFeatures } from 'core/utils/all-features';
const renderComponent = () => {
return render(hbs`
<Sidebar::Frame @isVisible={{true}}>
<Sidebar::Nav::Secrets />
</Sidebar::Frame>
`);
};
module('Integration | Component | sidebar-nav-secrets', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.flags = this.owner.lookup('service:flags');
setRunOptions({
rules: {
// This is an issue with Hds::AppHeader::HomeLink
'aria-prohibited-attr': { enabled: false },
// TODO: fix use Dropdown on user-menu
'nested-interactive': { enabled: false },
},
});
});
test('it should hide links and headings user does not have access to', async function (assert) {
await renderComponent();
assert.dom(GENERAL.navLink()).exists({ count: 2 }, 'Nav links are hidden other than secrets engines');
assert.dom(GENERAL.navHeading()).exists({ count: 1 }, 'Headings are hidden other than Secrets engines');
});
test('it should render nav links', async function (assert) {
const links = ['Secrets engines', 'Secrets sync'];
// do not add PKI-only Secrets feature as it hides Client count nav link
const features = allFeatures().filter((feat) => feat !== 'PKI-only Secrets');
stubFeaturesAndPermissions(this.owner, true, true, features);
await renderComponent();
assert.dom(GENERAL.navLink()).exists({ count: links.length + 1 }, 'Correct number of links render');
links.forEach((link) => {
assert.dom(GENERAL.navLink(link)).hasText(link, `${link} link renders`);
});
});
test('it should render badge for promotional links on managed clusters', async function (assert) {
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
const promotionalLinks = ['Secrets sync'];
stubFeaturesAndPermissions(this.owner, true, true);
await renderComponent();
promotionalLinks.forEach((link) => {
assert.dom(GENERAL.navLink(link)).hasText(`${link} Plus`, `${link} link renders Plus badge`);
});
});
// Secrets Sync side nav link has multiple combinations of three variables to test:
// 1. cluster type: enterprise (on and off license), HVD managed or community
// 2. activation status: activated or not
// 3. permissions: policy access to sys/sync routes or not
test('community: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, false, false);
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).doesNotExist();
});
test('ent but feature is not on license: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, []);
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).doesNotExist();
});
test('ent (on license), activated and permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync']);
this.flags.activatedFlags = ['secrets-sync'];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).exists();
});
test('ent (on license), activated and no permissions: it hides Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync'], false);
this.flags.activatedFlags = ['secrets-sync'];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).doesNotExist();
});
test('ent (on license), not activated and permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync']);
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).exists();
});
test('ent (on license), not activated and no permissions: it shows Secrets Sync nav link', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Secrets Sync'], false);
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).exists();
});
test('hvd managed: it shows Secrets Sync nav link regardless of activation status or permissions', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, [], false);
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
this.flags.activatedFlags = [];
await renderComponent();
assert.dom(GENERAL.navLink('Secrets sync')).exists();
});
});

View file

@ -62,7 +62,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
assert.expect(2);
await this.renderComponent();
assert.dom(PAGE.breadcrumbs).hasText('Secrets Sync Select Destination Create Destination');
assert.dom(GENERAL.breadcrumbs).hasText('Vault Secrets sync Select destination Create destination');
await click(PAGE.cancelButton);
const transition = this.transitionStub.calledWith('vault.cluster.sync.secrets.destinations.create');
assert.true(transition, 'transitions to vault.cluster.sync.secrets.destinations.create on cancel');
@ -92,7 +92,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE
assert.expect(2);
await this.renderComponent();
assert.dom(PAGE.breadcrumbs).hasText('Secrets Sync Destinations Destination Edit Destination');
assert.dom(GENERAL.breadcrumbs).hasText('Vault Secrets sync Destinations Destination Edit destination');
await click(PAGE.cancelButton);
const transition = this.transitionStub.calledWith('vault.cluster.sync.secrets.destinations.destination');

View file

@ -95,7 +95,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
});
await this.renderComponent();
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets Sync', 'Page title renders');
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');
@ -115,7 +115,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
await this.renderComponent();
assert.dom(overview.optInBanner.container).doesNotExist('Opt-in banner is not shown');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets Sync');
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();
@ -158,7 +158,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
this.isActivated = true;
await this.renderComponent();
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets Sync');
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Secrets sync');
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
assert.dom(cta.summary).exists();
});

View file

@ -47,6 +47,46 @@ module('Unit | Helper | displayNavItem', function (hooks) {
this.permissionsStub.restore();
});
module('secrets sync', function () {
test('it returns true when it is hvd managed', function (assert) {
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
const supportsClientCount = computeNavBar(this, RouteName.SECRETS_SYNC);
assert.true(supportsClientCount);
});
test('it returns true when it is secrets sync activated but not hvd managed', function (assert) {
this.flags.featureFlags = [];
this.flags.activatedFlags = [RouteName.SECRETS_SYNC];
this.permissionsStub.returns(true);
const supportsClientCount = computeNavBar(this, RouteName.SECRETS_SYNC);
assert.true(supportsClientCount);
});
test('it returns false when it is secrets sync activated but not hvd managed and permissions is false', function (assert) {
this.flags.featureFlags = [];
this.flags.activatedFlags = [RouteName.SECRETS_SYNC];
this.permissionsStub.returns(false);
const supportsClientCount = computeNavBar(this, RouteName.SECRETS_SYNC);
assert.false(supportsClientCount);
});
test('it returns false when it is enterprise', function (assert) {
this.flags.featureFlags = [];
this.version.type = 'community';
this.features = [];
const supportsClientCount = computeNavBar(this, RouteName.SECRETS_SYNC);
assert.false(supportsClientCount);
});
});
module('client count', function () {
test('it returns true when there are permissions and cluster is not secondary', function (assert) {
this.permissionsStub.returns(true);

View file

@ -14,7 +14,7 @@ export default create({
maxTTLVal: fillable('[data-test-ttl-value="Max Lease TTL"]'),
maxTTLUnit: fillable('[data-test-ttl-unit="Max Lease TTL"] [data-test-select="ttl-unit"]'),
enableEngine: clickable('[data-test-enable-engine]'),
secretList: clickable('[data-test-sidebar-nav-link="Secrets Engines"]'),
secretList: clickable('[data-test-sidebar-nav-link="Secrets"]'),
defaultTTLVal: fillable('input[data-test-ttl-value="Default Lease TTL"]'),
defaultTTLUnit: fillable('[data-test-ttl-unit="Default Lease TTL"] [data-test-select="ttl-unit"]'),
enable: async function (type, path) {