mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
* VAULT-41960 resiliance and recovery sidebar * Add breadcrumbs and fix failing tests * Update link to be external * Update mode title * Fix tests * More tests * Passing tests! * Fix sidebar highlight issue * Update remaining breadcrumbs and fix tests * Fix recovery tests * Add resilience and recovery tests * Sidebar clients rearrangement * Dasherize and address feedback * Add copyright headers * Move to snapshots component for integration test * Create recovery route file in cluster * Update ts file and create resilience route * Remove unused comment * Add display-nav-item helper * remove extra nav instantiation * Add copywrite header * Address feedback! * Add more tests! * Remaining helper tests * last resilience recovery test Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
parent
c6170d36a8
commit
0079d343d8
54 changed files with 1014 additions and 316 deletions
|
|
@ -55,11 +55,19 @@ export default class App extends Application {
|
|||
{ 'app-router': 'router' },
|
||||
'store',
|
||||
'version',
|
||||
// services needed for tools sidebar component
|
||||
'permissions',
|
||||
'current-cluster',
|
||||
'flags',
|
||||
'-portal',
|
||||
'control-group',
|
||||
],
|
||||
externalRoutes: {
|
||||
replication: 'vault.cluster.replication.index',
|
||||
vault: 'vault.cluster',
|
||||
recoverySnapshots: 'vault.cluster.recovery.snapshots',
|
||||
settingsSeal: 'vault.cluster.settings.seal',
|
||||
replicationMode: 'vault.cluster.replication.mode',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,46 +4,64 @@
|
|||
}}
|
||||
|
||||
<Recovery::Page::Header
|
||||
@title="Secrets Recovery"
|
||||
@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}}
|
||||
/>
|
||||
|
||||
{{#if @model.showCommunityMessage}}
|
||||
<EmptyState
|
||||
@title="Secrets Recovery is an enterprise feature"
|
||||
@icon="sync-reverse"
|
||||
@message="Secrets Recovery allows you to restore accidentally deleted or lost secrets from a snapshot. The snapshots can be provided via upload or loaded from external storage."
|
||||
>
|
||||
<Hds::Button
|
||||
@text="Learn more about upgrading"
|
||||
@color="tertiary"
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@href={{doc-link "/vault/docs/enterprise"}}
|
||||
@isHrefExternal={{true}}
|
||||
/>
|
||||
</EmptyState>
|
||||
{{else if (not @model.snapshots)}}
|
||||
{{! Currently, only a single snapshot is supported and the UI automatically redirects users to "recovery.snapshots.snapshot.manage" if one exists.
|
||||
In the future, this may change to support multiple loaded snapshots and a LIST view will be built then. }}
|
||||
{{#let (get this.emptyStateDetails this.state) as |d|}}
|
||||
<EmptyState @title={{d.title}} @icon={{d.icon}} @message={{d.message}}>
|
||||
{{#if @model.snapshots.showRaftStorageMessage}}
|
||||
<hr class="has-background-gray-300" />
|
||||
|
||||
{{#if (eq this.state this.viewState.ALLOW_UPLOAD)}}
|
||||
<Hds::Button @text={{d.buttonText}} @color={{d.buttonColor}} @route="vault.cluster.recovery.snapshots.load" />
|
||||
{{else}}
|
||||
<Hds::Button
|
||||
@text={{d.buttonText}}
|
||||
@color={{d.buttonColor}}
|
||||
@icon={{d.buttonIcon}}
|
||||
@iconPosition="trailing"
|
||||
@route={{d.buttonRoute}}
|
||||
@query={{if d.buttonRoute (hash namespace="")}}
|
||||
@href={{doc-link d.buttonHref}}
|
||||
@isHrefExternal={{if d.buttonHref true}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::ApplicationState as |A|>
|
||||
<A.Header @title="Raft storage required" @icon="info" data-test-empty-state-title />
|
||||
<A.Body @text="Raft storage must be used in order to recover data from a snapshot." data-test-empty-state-message />
|
||||
<A.Footer data-test-empty-state-actions as |F|>
|
||||
<F.LinkStandalone
|
||||
@text="Snapshot management"
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@href={{doc-link "/vault/docs/sysadmin/snapshots"}}
|
||||
@isHrefExternal={{true}}
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
{{else}}
|
||||
{{#if @model.showCommunityMessage}}
|
||||
<EmptyState
|
||||
@title="Secrets Recovery is an enterprise feature"
|
||||
@icon="sync-reverse"
|
||||
@message="Secrets Recovery allows you to restore accidentally deleted or lost secrets from a snapshot. The snapshots can be provided via upload or loaded from external storage."
|
||||
>
|
||||
<Hds::Button
|
||||
@text="Learn more about upgrading"
|
||||
@color="tertiary"
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@href={{doc-link "/vault/docs/enterprise"}}
|
||||
@isHrefExternal={{true}}
|
||||
/>
|
||||
</EmptyState>
|
||||
{{/let}}
|
||||
{{else if (not @model.snapshots)}}
|
||||
{{! Currently, only a single snapshot is supported and the UI automatically redirects users to "recovery.snapshots.snapshot.manage" if one exists.
|
||||
In the future, this may change to support multiple loaded snapshots and a LIST view will be built then. }}
|
||||
{{#let (get this.emptyStateDetails this.state) as |d|}}
|
||||
<EmptyState @title={{d.title}} @icon={{d.icon}} @message={{d.message}}>
|
||||
|
||||
{{#if (eq this.state this.viewState.ALLOW_UPLOAD)}}
|
||||
<Hds::Button @text={{d.buttonText}} @color={{d.buttonColor}} @route="vault.cluster.recovery.snapshots.load" />
|
||||
{{else}}
|
||||
<Hds::Button
|
||||
@text={{d.buttonText}}
|
||||
@color={{d.buttonColor}}
|
||||
@icon={{d.buttonIcon}}
|
||||
@iconPosition="trailing"
|
||||
@route={{d.buttonRoute}}
|
||||
@query={{if d.buttonRoute (hash namespace="")}}
|
||||
@href={{doc-link d.buttonHref}}
|
||||
@isHrefExternal={{if d.buttonHref true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Recovery::Page::Header @title="Upload Snapshot" @breadcrumbs={{@breadcrumbs}} />
|
||||
<Recovery::Page::Header @title="Upload snapshot" @breadcrumbs={{@breadcrumbs}} />
|
||||
|
||||
{{#if this.bannerError}}
|
||||
<div class="has-top-padding-m has-bottom-padding-m">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
}}
|
||||
|
||||
<Recovery::Page::Header
|
||||
@title="Secrets Recovery"
|
||||
@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"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
}}
|
||||
|
||||
<Recovery::Page::Header
|
||||
@title="Secrets Recovery"
|
||||
@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}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export default class ClusterModel extends Model {
|
|||
return this.rm.mode;
|
||||
}
|
||||
get replicationModeForDisplay() {
|
||||
return this.replicationMode === 'dr' ? 'Disaster Recovery' : 'Performance';
|
||||
return this.replicationMode === 'dr' ? 'Disaster recovery' : 'Performance';
|
||||
}
|
||||
get replicationIsInitializing() {
|
||||
// a mode of null only happens when a cluster is being initialized
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ Router.map(function () {
|
|||
this.route('show', { path: '/:policy_name' });
|
||||
this.route('edit', { path: '/:policy_name/edit' });
|
||||
});
|
||||
this.route('resilience-recovery');
|
||||
this.route('replication-dr-promote', function () {
|
||||
this.route('details');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class RecoverySnapshotsRoute extends Route {
|
|||
}
|
||||
|
||||
redirect(model: SnapshotsRouteModel) {
|
||||
if (model.snapshots.length === 1) {
|
||||
if (Array.isArray(model.snapshots) && model.snapshots.length === 1) {
|
||||
const snapshot_id = model.snapshots[0];
|
||||
this.router.transitionTo('vault.cluster.recovery.snapshots.snapshot.manage', snapshot_id);
|
||||
}
|
||||
|
|
@ -59,6 +59,13 @@ export default class RecoverySnapshotsRoute extends Route {
|
|||
if (error.status === 404) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (error.message === 'raft storage is not in use') {
|
||||
return {
|
||||
showRaftStorageMessage: true,
|
||||
};
|
||||
}
|
||||
|
||||
throw {
|
||||
httpStatus: error.status,
|
||||
...error,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default class RecoverySnapshotsIndexRoute extends Route {
|
|||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard', icon: 'vault' },
|
||||
{ label: 'Secrets Recovery' },
|
||||
{ label: 'Secrets recovery' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ export default class RecoverySnapshotsLoadRoute extends Route {
|
|||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets Recovery', route: 'vault.cluster.recovery.snapshots' },
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard' },
|
||||
{ label: 'Secrets recovery', route: 'vault.cluster.recovery.snapshots' },
|
||||
{ label: 'Upload' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export default class RecoverySnapshotsSnapshotDetailsRoute extends Route {
|
|||
super.setupController(controller, resolvedModel);
|
||||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets Recovery', route: 'vault.cluster.recovery.snapshots' },
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard' },
|
||||
{ label: 'Secrets recovery', route: 'vault.cluster.recovery.snapshots' },
|
||||
{ label: 'Details' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default class RecoverySnapshotsSnapshotManageRoute extends Route {
|
|||
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard', icon: 'vault' },
|
||||
{ label: 'Secrets Recovery' },
|
||||
{ label: 'Secrets recovery' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
ui/app/routes/vault/cluster/resilience-recovery.ts
Normal file
28
ui/app/routes/vault/cluster/resilience-recovery.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import Route from '@ember/routing/route';
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import { service } from '@ember/service';
|
||||
import { computeNavBar, RouteName } from 'core/helpers/display-nav-item';
|
||||
|
||||
import type CurrentClusterService from 'vault/services/current-cluster';
|
||||
import type ClusterModel from 'vault/models/cluster';
|
||||
|
||||
export default class ResilienceRecoveryRoute extends Route {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly currentCluster: CurrentClusterService;
|
||||
|
||||
beforeModel() {
|
||||
const cluster = this.currentCluster.cluster as ClusterModel | null;
|
||||
|
||||
if (computeNavBar(this, RouteName.SECRETS_RECOVERY)) {
|
||||
this.router.replaceWith('vault.cluster.recovery.snapshots');
|
||||
} else if (computeNavBar(this, RouteName.SEAL)) {
|
||||
this.router.replaceWith('vault.cluster.settings.seal', cluster?.name);
|
||||
} else if (computeNavBar(this, RouteName.REPLICATION)) {
|
||||
this.router.replaceWith('vault.cluster.replication.index');
|
||||
}
|
||||
}
|
||||
}
|
||||
8
ui/app/templates/vault/cluster/recovery.hbs
Normal file
8
ui/app/templates/vault/cluster/recovery.hbs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Sidebar::Nav::ResilienceAndRecovery />
|
||||
|
||||
{{outlet}}
|
||||
|
|
@ -3,32 +3,4 @@
|
|||
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={{array
|
||||
(hash label="Vault" route="vault.cluster.dashboard" icon="vault")
|
||||
(hash label="Secrets Recovery" current="true")
|
||||
}}
|
||||
/>
|
||||
|
||||
<hr class="has-background-gray-300" />
|
||||
|
||||
{{#if (eq this.model.message "raft storage is not in use")}}
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32" as |A|>
|
||||
<A.Header @title="Raft storage required" @icon="info" data-test-empty-state-title />
|
||||
<A.Body @text="Raft storage must be used in order to recover data from a snapshot." data-test-empty-state-message />
|
||||
<A.Footer data-test-empty-state-actions as |F|>
|
||||
<F.LinkStandalone
|
||||
@text="Snapshot management"
|
||||
@icon="docs-link"
|
||||
@iconPosition="trailing"
|
||||
@href={{doc-link "/vault/docs/sysadmin/snapshots"}}
|
||||
@isHrefExternal={{true}}
|
||||
/>
|
||||
</A.Footer>
|
||||
|
||||
</Hds::ApplicationState>
|
||||
{{else}}
|
||||
<Page::Error @error={{this.model}} />
|
||||
{{/if}}
|
||||
<Page::Error @error={{this.model}} />
|
||||
|
|
@ -2,8 +2,15 @@
|
|||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Sidebar::Nav::ResilienceAndRecovery />
|
||||
|
||||
<Page::Header @title="Seal this Vault" />
|
||||
<Page::Header @title="Seal Vault">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Seal Vault")}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
{{#if this.model.seal.canUpdate}}
|
||||
<SealAction @onSeal={{action "seal"}} />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</h3>
|
||||
<p class="has-top-padding-s" data-test-promote-description>
|
||||
Promote this cluster to a
|
||||
{{this.model.replicationModeForDisplay}}
|
||||
{{lowercase this.model.replicationModeForDisplay}}
|
||||
primary
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
<Page::Header @title={{@title}}>
|
||||
<:breadcrumbs>
|
||||
{{#if (not (or @isSummaryDashboard @isSecondary))}}
|
||||
{{#unless @isSecondary}}
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Replication" route="vault.cluster.replication.index") (hash label=@title)}}
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault" icon="vault" linkExternal=true) (hash label=@title)}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</:breadcrumbs>
|
||||
<:badges>
|
||||
{{#if @data.anyReplicationEnabled}}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { getOwner } from '@ember/owner';
|
|||
*/
|
||||
|
||||
const MODE = {
|
||||
dr: 'Disaster Recovery',
|
||||
dr: 'Disaster recovery',
|
||||
performance: 'Performance',
|
||||
};
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ export default class ReplicationPage extends Component {
|
|||
get formattedReplicationMode() {
|
||||
// dr or performance 🤯
|
||||
if (this.isSummaryDashboard) {
|
||||
return 'Disaster Recovery & Performance';
|
||||
return 'Disaster recovery and performance';
|
||||
}
|
||||
const mode = this.args.model.replicationMode;
|
||||
return MODE[mode];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Hds::AppSideNav::Portal @ariaLabel="Client Count Navigation Links" data-test-sidebar-nav-panel="Client Count" as |Nav|>
|
||||
<Hds::AppSideNav::Portal @ariaLabel="Client count Navigation Links" data-test-sidebar-nav-panel="Client count" as |Nav|>
|
||||
<Nav.BackLink
|
||||
@route="vault.cluster"
|
||||
@current-when={{false}}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
data-test-sidebar-nav-link="Back to main navigation"
|
||||
/>
|
||||
|
||||
<Nav.Title data-test-sidebar-nav-heading="Client Count">Client Count</Nav.Title>
|
||||
<Nav.Title data-test-sidebar-nav-heading="Client count">Client count</Nav.Title>
|
||||
|
||||
<Nav.Link
|
||||
@route="vault.cluster.clients.counts.overview"
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@
|
|||
data-test-sidebar-nav-link="Secrets Sync"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#unless (or this.cluster.dr.isSecondary this.flags.isHvdManaged)}}
|
||||
|
||||
{{#if (display-nav-item this.navSection.resilienceAndRecovery)}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.recovery.snapshots"
|
||||
@text="Secrets Recovery"
|
||||
@badge={{if this.version.isCommunity "Enterprise" ""}}
|
||||
data-test-sidebar-nav-link="Secrets Recovery"
|
||||
@route="vault.cluster.resilience-recovery"
|
||||
@text="Resilience and recovery"
|
||||
@hasSubItems={{true}}
|
||||
data-test-sidebar-nav-link="Resilience and recovery"
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (has-permission "access") (has-permission "policies"))}}
|
||||
<Nav.Link
|
||||
@route={{this.accessRoute}}
|
||||
|
|
@ -44,27 +46,12 @@
|
|||
{{/if}}
|
||||
{{#if
|
||||
(or
|
||||
(and this.isRootNamespace (has-permission "status" routeParams=(array "replication" "raft" "license" "seal")))
|
||||
(and this.isRootNamespace (has-permission "status" routeParams="raft"))
|
||||
(and (has-permission "clients" routeParams="activity") (not this.hasChrootNamespace))
|
||||
)
|
||||
}}
|
||||
<Nav.Title data-test-sidebar-nav-heading="Monitoring">Monitoring</Nav.Title>
|
||||
{{/if}}
|
||||
{{#if
|
||||
(and
|
||||
this.version.isEnterprise
|
||||
this.isRootNamespace
|
||||
(not this.cluster.replicationRedacted)
|
||||
(has-permission "status" routeParams="replication")
|
||||
)
|
||||
}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.replication.index"
|
||||
@text="Replication"
|
||||
data-test-sidebar-nav-link="Replication"
|
||||
@hasSubItems={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (and this.cluster.usingRaft this.isRootNamespace (has-permission "status" routeParams="raft"))}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.storage"
|
||||
|
|
@ -73,32 +60,7 @@
|
|||
data-test-sidebar-nav-link="Raft Storage"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if
|
||||
(and
|
||||
(has-permission "clients" routeParams="activity")
|
||||
(not this.cluster.dr.isSecondary)
|
||||
(not this.hasChrootNamespace)
|
||||
(not this.version.hasPKIOnly)
|
||||
)
|
||||
}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.clients"
|
||||
@text="Client Count"
|
||||
@hasSubItems={{true}}
|
||||
data-test-sidebar-nav-link="Client Count"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if
|
||||
(or
|
||||
this.canAccessVaultUsageDashboard
|
||||
(and
|
||||
this.version.features
|
||||
this.isRootNamespace
|
||||
(has-permission "status" routeParams="license")
|
||||
(not this.cluster.dr.isSecondary)
|
||||
)
|
||||
)
|
||||
}}
|
||||
{{#if (display-nav-item this.navSection.reporting)}}
|
||||
<Nav.Link
|
||||
@route={{if this.canAccessVaultUsageDashboard "vault.cluster.usage-reporting" "vault.cluster.license"}}
|
||||
@text="Reporting"
|
||||
|
|
@ -106,12 +68,12 @@
|
|||
@hasSubItems={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (and this.isRootNamespace (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}}
|
||||
{{#if (display-nav-item this.navSection.clientCount)}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.settings.seal"
|
||||
@model={{this.cluster.name}}
|
||||
@text="Seal Vault"
|
||||
data-test-sidebar-nav-link="Seal Vault"
|
||||
@route="vault.cluster.clients"
|
||||
@text="Client count"
|
||||
@hasSubItems={{true}}
|
||||
data-test-sidebar-nav-link="Client count"
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::AppSideNav::Portal>
|
||||
|
|
@ -5,15 +5,21 @@
|
|||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { NavSection } from 'core/helpers/display-nav-item';
|
||||
|
||||
export default class SidebarNavClusterComponent extends Component {
|
||||
@service currentCluster;
|
||||
@service flags;
|
||||
@service version;
|
||||
@service auth;
|
||||
@service namespace;
|
||||
@service permissions;
|
||||
|
||||
navSection = {
|
||||
resilienceAndRecovery: NavSection.RESILIENCE_AND_RECOVERY,
|
||||
reporting: NavSection.REPORTING,
|
||||
clientCount: NavSection.CLIENT_COUNT,
|
||||
};
|
||||
|
||||
get cluster() {
|
||||
return this.currentCluster.cluster;
|
||||
}
|
||||
|
|
@ -27,22 +33,6 @@ export default class SidebarNavClusterComponent extends Component {
|
|||
return this.namespace.inRootNamespace && !this.hasChrootNamespace;
|
||||
}
|
||||
|
||||
get canAccessVaultUsageDashboard() {
|
||||
/*
|
||||
A user can access Vault Usage if they satisfy the following conditions:
|
||||
1) They have access to sys/v1/utilization-report endpoint
|
||||
2) They are either
|
||||
a) enterprise cluster and root namespace
|
||||
b) hvd cluster and /admin namespace
|
||||
*/
|
||||
|
||||
const hasPermission = this.permissions.hasNavPermission('monitoring');
|
||||
const isEnterprise = this.version.isEnterprise;
|
||||
const isCorrectNamespace = this.isRootNamespace || this.namespace.inHvdAdminNamespace;
|
||||
|
||||
return hasPermission && isEnterprise && isCorrectNamespace;
|
||||
}
|
||||
|
||||
get showSecretsSync() {
|
||||
// always show for HVD managed clusters
|
||||
if (this.flags.isHvdManaged) return true;
|
||||
|
|
|
|||
|
|
@ -13,17 +13,10 @@
|
|||
/>
|
||||
|
||||
<Nav.Title data-test-sidebar-nav-heading="Reporting">Reporting</Nav.Title>
|
||||
{{#if this.canAccessVaultUsageDashboard}}
|
||||
{{#if (display-nav-item this.routeName.vaultUsage)}}
|
||||
<Nav.Link @route="vault.cluster.usage-reporting" @text="Vault usage" data-test-sidebar-nav-link="Vault usage" />
|
||||
{{/if}}
|
||||
{{#if
|
||||
(and
|
||||
this.version.features
|
||||
this.isRootNamespace
|
||||
(has-permission "status" routeParams="license")
|
||||
(not this.cluster.dr.isSecondary)
|
||||
)
|
||||
}}
|
||||
{{#if (display-nav-item this.routeName.license)}}
|
||||
<Nav.Link
|
||||
@route="vault.cluster.license"
|
||||
@model={{this.cluster.name}}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@
|
|||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { RouteName } from 'core/helpers/display-nav-item';
|
||||
|
||||
import type CurrentClusterService from 'vault/services/current-cluster';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type ClusterModel from 'vault/models/cluster';
|
||||
import type PermissionsService from 'vault/services/permissions';
|
||||
|
||||
interface Args {
|
||||
isEngine?: boolean;
|
||||
|
|
@ -18,36 +16,13 @@ interface Args {
|
|||
|
||||
export default class SidebarNavReportingComponent extends Component<Args> {
|
||||
@service declare readonly currentCluster: CurrentClusterService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly permissions: PermissionsService;
|
||||
|
||||
routeName = {
|
||||
vaultUsage: RouteName.VAULT_USAGE,
|
||||
license: RouteName.LICENSE,
|
||||
};
|
||||
|
||||
get cluster() {
|
||||
return this.currentCluster.cluster as ClusterModel | null;
|
||||
}
|
||||
|
||||
get hasChrootNamespace() {
|
||||
return this.cluster?.hasChrootNamespace;
|
||||
}
|
||||
|
||||
get isRootNamespace() {
|
||||
// should only return true if we're in the true root namespace
|
||||
return this.namespace.inRootNamespace && !this.hasChrootNamespace;
|
||||
}
|
||||
|
||||
get canAccessVaultUsageDashboard() {
|
||||
/*
|
||||
A user can access Vault Usage if they satisfy the following conditions:
|
||||
1) They have access to sys/v1/utilization-report endpoint
|
||||
2) They are either
|
||||
a) enterprise cluster and root namespace
|
||||
b) hvd cluster and /admin namespace
|
||||
*/
|
||||
|
||||
const hasPermission = this.permissions.hasNavPermission('monitoring');
|
||||
const isEnterprise = this.version.isEnterprise;
|
||||
const isCorrectNamespace = this.isRootNamespace || this.namespace.inHvdAdminNamespace;
|
||||
|
||||
return hasPermission && isEnterprise && isCorrectNamespace;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Hds::AppSideNav::Portal
|
||||
@ariaLabel="Resilience and recovery Navigation Links"
|
||||
data-test-sidebar-nav-panel="Resilience and recovery"
|
||||
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"
|
||||
/>
|
||||
{{#if (or (display-nav-item this.routeName.secretsRecovery) (display-nav-item this.routeName.seal))}}
|
||||
<Nav.Title data-test-sidebar-nav-heading="Resilience and recovery">Resilience and recovery</Nav.Title>
|
||||
{{/if}}
|
||||
{{#if (display-nav-item this.routeName.secretsRecovery)}}
|
||||
<Nav.Link
|
||||
@route={{if @isEngine "recoverySnapshots" "vault.cluster.recovery.snapshots"}}
|
||||
@isRouteExternal={{@isEngine}}
|
||||
@text="Secrets recovery"
|
||||
@badge={{if this.version.isCommunity "Enterprise" ""}}
|
||||
data-test-sidebar-nav-link="Secrets recovery"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (display-nav-item this.routeName.seal)}}
|
||||
<Nav.Link
|
||||
@route={{if @isEngine "settingsSeal" "vault.cluster.settings.seal"}}
|
||||
@isRouteExternal={{@isEngine}}
|
||||
@model={{this.cluster.name}}
|
||||
@text="Seal Vault"
|
||||
data-test-sidebar-nav-link="Seal Vault"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (display-nav-item this.routeName.replication)}}
|
||||
<Nav.Title data-test-sidebar-nav-heading="Replication">Replication</Nav.Title>
|
||||
<Nav.Link
|
||||
@route={{if @isEngine "replication" "vault.cluster.replication.index"}}
|
||||
@isRouteExternal={{@isEngine}}
|
||||
@text="Overview"
|
||||
data-test-sidebar-nav-link="Overview"
|
||||
/>
|
||||
{{#if (not-eq this.cluster.mode "unsupported")}}
|
||||
{{#if (has-feature "DR Replication")}}
|
||||
<Nav.Link
|
||||
@route={{if @isEngine "replicationMode" "vault.cluster.replication.mode"}}
|
||||
@isRouteExternal={{@isEngine}}
|
||||
@model="dr"
|
||||
@text="Disaster recovery"
|
||||
data-test-sidebar-nav-link="Disaster recovery"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<Nav.Link
|
||||
@route={{if @isEngine "replicationMode" "vault.cluster.replication.mode"}}
|
||||
@isRouteExternal={{@isEngine}}
|
||||
@model="performance"
|
||||
@text="Performance replication"
|
||||
data-test-sidebar-nav-link="Performance replication"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::AppSideNav::Portal>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { RouteName } from 'core/helpers/display-nav-item';
|
||||
|
||||
import type CurrentClusterService from 'vault/services/current-cluster';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type ClusterModel from 'vault/models/cluster';
|
||||
|
||||
interface Args {
|
||||
isEngine?: boolean;
|
||||
}
|
||||
|
||||
export default class SidebarNavResilienceAndRecoveryComponent extends Component<Args> {
|
||||
@service declare readonly currentCluster: CurrentClusterService;
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
routeName = {
|
||||
secretsRecovery: RouteName.SECRETS_RECOVERY,
|
||||
seal: RouteName.SEAL,
|
||||
replication: RouteName.REPLICATION,
|
||||
};
|
||||
|
||||
get cluster() {
|
||||
return this.currentCluster.cluster as ClusterModel | null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,13 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title={{this.title}} />
|
||||
<Page::Header @title={{this.title}}>
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label=this.title)}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<EmptyState
|
||||
@title="Upgrade to use {{this.featureName}}"
|
||||
|
|
|
|||
145
ui/lib/core/addon/helpers/display-nav-item.ts
Normal file
145
ui/lib/core/addon/helpers/display-nav-item.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/owner';
|
||||
|
||||
import type CurrentClusterService from 'vault/services/current-cluster';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type ClusterModel from 'vault/models/cluster';
|
||||
import type PermissionsService from 'vault/services/permissions';
|
||||
import type FlagsService from 'vault/services/flags';
|
||||
|
||||
export enum RouteName {
|
||||
SECRETS_RECOVERY = 'secrets-recovery',
|
||||
SEAL = 'seal',
|
||||
REPLICATION = 'replication',
|
||||
VAULT_USAGE = 'vault-usage',
|
||||
LICENSE = 'license',
|
||||
}
|
||||
|
||||
export enum NavSection {
|
||||
RESILIENCE_AND_RECOVERY = 'resilience-and-recovery',
|
||||
REPORTING = 'reporting',
|
||||
CLIENT_COUNT = 'client-count',
|
||||
}
|
||||
|
||||
export default class NavBar extends Helper {
|
||||
@service declare readonly currentCluster: CurrentClusterService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly namespace: NamespaceService;
|
||||
@service declare readonly permissions: PermissionsService;
|
||||
@service declare readonly flags: FlagsService;
|
||||
|
||||
compute([navItem]: string[]) {
|
||||
const { SECRETS_RECOVERY, SEAL, REPLICATION, VAULT_USAGE, LICENSE } = RouteName;
|
||||
const { RESILIENCE_AND_RECOVERY, REPORTING, CLIENT_COUNT } = NavSection;
|
||||
|
||||
switch (navItem) {
|
||||
// client count nav items
|
||||
case CLIENT_COUNT:
|
||||
return this.supportsClientCount;
|
||||
// reporting nav items
|
||||
case VAULT_USAGE:
|
||||
return this.canAccessVaultUsageDashboard;
|
||||
case LICENSE:
|
||||
return this.supportsLicense;
|
||||
case REPORTING:
|
||||
return this.canAccessVaultUsageDashboard || this.supportsLicense;
|
||||
// resilience and recovery nav items
|
||||
case SECRETS_RECOVERY:
|
||||
return this.supportsSnapshots;
|
||||
case SEAL:
|
||||
return this.canSeal;
|
||||
case REPLICATION:
|
||||
return this.supportsReplication;
|
||||
case RESILIENCE_AND_RECOVERY:
|
||||
return this.supportsSnapshots || this.canSeal || this.supportsReplication;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get cluster() {
|
||||
return this.currentCluster.cluster as ClusterModel | null;
|
||||
}
|
||||
|
||||
get hasChrootNamespace() {
|
||||
return this.cluster?.hasChrootNamespace;
|
||||
}
|
||||
|
||||
get isRootNamespace() {
|
||||
// should only return true if we're in the true root namespace
|
||||
return this.namespace.inRootNamespace && !this.hasChrootNamespace;
|
||||
}
|
||||
|
||||
get supportsReplication() {
|
||||
return (
|
||||
this.version.isEnterprise &&
|
||||
this.isRootNamespace &&
|
||||
!this.cluster?.replicationRedacted &&
|
||||
this.permissions.hasNavPermission('status', 'replication')
|
||||
);
|
||||
}
|
||||
|
||||
get canSeal() {
|
||||
return (
|
||||
this.isRootNamespace &&
|
||||
this.permissions.hasNavPermission('status', 'seal') &&
|
||||
!this.cluster?.dr?.isSecondary
|
||||
);
|
||||
}
|
||||
|
||||
get supportsSnapshots() {
|
||||
return (this.cluster && !this.cluster?.dr?.isSecondary) || !this.flags.isHvdManaged;
|
||||
}
|
||||
|
||||
get canAccessVaultUsageDashboard() {
|
||||
/*
|
||||
A user can access Vault Usage if they satisfy the following conditions:
|
||||
1) They have access to sys/v1/utilization-report endpoint
|
||||
2) They are either
|
||||
a) enterprise cluster and root namespace
|
||||
b) hvd cluster and /admin namespace
|
||||
*/
|
||||
|
||||
const hasPermission = this.permissions.hasNavPermission('monitoring');
|
||||
const isEnterprise = this.version.isEnterprise;
|
||||
const isCorrectNamespace = this.isRootNamespace || this.namespace.inHvdAdminNamespace;
|
||||
|
||||
return hasPermission && isEnterprise && isCorrectNamespace;
|
||||
}
|
||||
|
||||
get supportsLicense() {
|
||||
return (
|
||||
this?.version?.features &&
|
||||
this.isRootNamespace &&
|
||||
this.permissions.hasNavPermission('status', 'license') &&
|
||||
!this.cluster?.dr?.isSecondary
|
||||
);
|
||||
}
|
||||
|
||||
get supportsClientCount() {
|
||||
return (
|
||||
this.permissions.hasNavPermission('clients', 'activity') &&
|
||||
!this.cluster?.dr?.isSecondary &&
|
||||
!this.hasChrootNamespace &&
|
||||
!this.version.hasPKIOnly
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function computeNavBar(context: object, navItem: string): boolean {
|
||||
const navBar = new NavBar();
|
||||
const owner = getOwner(context);
|
||||
|
||||
if (owner) {
|
||||
setOwner(navBar, owner);
|
||||
return navBar.compute([navItem]);
|
||||
}
|
||||
return true; // when in doubt
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/components/sidebar/nav/resilience-and-recovery';
|
||||
6
ui/lib/core/app/helpers/display-nav-item.js
Normal file
6
ui/lib/core/app/helpers/display-nav-item.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/display-nav-item';
|
||||
|
|
@ -4,13 +4,19 @@
|
|||
}}
|
||||
|
||||
{{#if @replicationDisabled}}
|
||||
<Page::Header @title={{this.title}} />
|
||||
<Page::Header @title={{this.title}}>
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault" icon="vault" linkExternal=true) (hash label=this.title)}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</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
|
||||
Disaster recovery (DR) replication
|
||||
</h2>
|
||||
<p class="help has-text-grey-dark">
|
||||
{{replication-mode-description "dr"}}
|
||||
|
|
@ -18,7 +24,7 @@
|
|||
{{else if (eq @replicationMode "performance")}}
|
||||
<h2 class="title is-flex-center is-5 is-marginless">
|
||||
<Icon @size="24" @name="replication-perf" />
|
||||
Performance Replication
|
||||
Performance replication
|
||||
</h2>
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<p class="help has-text-grey-dark">
|
||||
|
|
@ -26,7 +32,7 @@
|
|||
</p>
|
||||
{{else}}
|
||||
<p class="help has-text-grey-dark">
|
||||
Performance Replication is a feature of Vault Enterprise Premium
|
||||
Performance replication is a feature of Vault Enterprise Premium
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ import Component from '@glimmer/component';
|
|||
export default class PageModeIndex extends Component {
|
||||
get title() {
|
||||
if (this.args.replicationMode === 'dr') {
|
||||
return 'Enable Disaster Recovery Replication';
|
||||
return 'Enable disaster recovery replication';
|
||||
}
|
||||
if (this.args.replicationMode === 'performance') {
|
||||
return 'Enable Performance Replication';
|
||||
return 'Enable performance replication';
|
||||
}
|
||||
// should never get here, but have safe fallback just in case
|
||||
return 'Enable Replication';
|
||||
return 'Enable replication';
|
||||
}
|
||||
|
||||
canEnable = (type) => {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,14 @@ const Eng = Engine.extend({
|
|||
'app-router',
|
||||
'store',
|
||||
'version',
|
||||
// services needed for tools sidebar component
|
||||
'permissions',
|
||||
'current-cluster',
|
||||
'flags',
|
||||
'-portal',
|
||||
'control-group',
|
||||
],
|
||||
externalRoutes: ['replication', 'vault'],
|
||||
externalRoutes: ['replication', 'vault', 'recoverySnapshots', 'settingsSeal', 'replicationMode'],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,32 +3,6 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Hds::AppSideNav::Portal @ariaLabel="Replication Navigation Links" data-test-sidebar-nav-panel="Replication" as |Nav|>
|
||||
<Nav.BackLink
|
||||
@route="vault"
|
||||
@current-when={{false}}
|
||||
@isRouteExternal={{true}}
|
||||
@icon="arrow-left"
|
||||
@text="Back to main navigation"
|
||||
data-test-sidebar-nav-link="Back to main navigation"
|
||||
/>
|
||||
|
||||
<Nav.Title data-test-sidebar-nav-heading="Replication">Replication</Nav.Title>
|
||||
<Nav.Link
|
||||
@route="replication"
|
||||
@current-when="replication"
|
||||
@isRouteExternal={{true}}
|
||||
@text="Overview"
|
||||
data-test-sidebar-nav-link="Overview"
|
||||
/>
|
||||
{{#if (not-eq this.model.mode "unsupported")}}
|
||||
{{#if (has-feature "DR Replication")}}
|
||||
<Nav.Link @route="mode" @model="dr" @text="Disaster Recovery" data-test-sidebar-nav-link="Disaster Recovery" />
|
||||
{{/if}}
|
||||
{{#if (has-feature "Performance Replication")}}
|
||||
<Nav.Link @route="mode" @model="performance" @text="Performance" data-test-sidebar-nav-link="Performance" />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::AppSideNav::Portal>
|
||||
<Sidebar::Nav::ResilienceAndRecovery @isEngine={{true}} />
|
||||
|
||||
{{outlet}}
|
||||
|
|
@ -7,13 +7,31 @@
|
|||
<div class="container is-widescreen">
|
||||
{{#if (eq this.model.mode "unsupported")}}
|
||||
{{! Replication is unsupported in non-enterprise or when using non-transactional storage (eg inmem) }}
|
||||
<Page::Header @title="Replication unsupported" />
|
||||
<Page::Header @title="Replication unsupported">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
|
||||
(hash label="Replication unsupported")
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<EmptyState @title="The current cluster configuration does not support replication" />
|
||||
{{else if this.model.replicationIsInitializing}}
|
||||
<LayoutLoading />
|
||||
{{else if this.model.allReplicationDisabled}}
|
||||
<Page::Header @title="Enable Replication" />
|
||||
<Page::Header @title="Enable replication">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
|
||||
(hash label="Enable replication")
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<p class="has-text-grey-dark box is-shadowless is-fullwidth has-slim-padding">
|
||||
|
|
@ -102,7 +120,16 @@
|
|||
</ReplicationPage>
|
||||
{{else}}
|
||||
{{! Renders when at least one mode is not enabled }}
|
||||
<Page::Header @title="Replication" />
|
||||
<Page::Header @title="Replication">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
|
||||
(hash label="Replication")
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-marginless flex-col">
|
||||
<ReplicationOverviewMode
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="Vault" route="vault" icon="vault" linkExternal=true)
|
||||
(hash label="Replication" route="index")
|
||||
(hash label=this.model.replicationModeForDisplay)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
|
|||
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item in chroot listener`);
|
||||
});
|
||||
// Client count is not root-only, but it is hidden for chroot
|
||||
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client Count'].forEach((nav) => {
|
||||
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client count'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item in chroot listener`);
|
||||
});
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
|
|||
[('Dashboard', 'Secrets Engines', '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) => {
|
||||
['Client count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item for user with default policy`);
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
|
|||
['Dashboard', 'Secrets Engines', '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) => {
|
||||
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client count'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item for user with read access policy`);
|
||||
});
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
|
|||
['Dashboard', 'Secrets Engines', 'Access', 'Operational tools'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item`);
|
||||
});
|
||||
['Client Count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
|
||||
['Client count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item`);
|
||||
});
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
|
|||
['Dashboard', 'Secrets Engines', '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) => {
|
||||
['Replication', 'Raft Storage', 'License', 'Seal Vault', 'Client count'].forEach((nav) => {
|
||||
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item within child namespace`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ module('Acceptance | clients | counts | client list', function (hooks) {
|
|||
|
||||
test('it navigates to client list tab', async function (assert) {
|
||||
assert.expect(3);
|
||||
await click(GENERAL.navLink('Client Count'));
|
||||
await click(GENERAL.navLink('Client count'));
|
||||
await click(GENERAL.tab('client list'));
|
||||
assert.strictEqual(currentURL(), '/vault/clients/counts/client-list', 'it navigates to client list tab');
|
||||
assert.dom(GENERAL.tab('client list')).hasClass('active');
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ module('Acceptance | Enterprise | replication-secondaries', function (hooks) {
|
|||
assert
|
||||
.dom('[data-test-promote-description]')
|
||||
.hasText(
|
||||
'Promote this cluster to a Disaster Recovery primary',
|
||||
'Promote this cluster to a disaster recovery primary',
|
||||
'shows the correct description for a DR secondary'
|
||||
);
|
||||
assert.dom(GENERAL.badge('secondary')).includesText('secondary', 'shows the DR secondary mode badge');
|
||||
|
|
|
|||
|
|
@ -60,20 +60,21 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
|
|||
await this.setupMocks(STATUS_DISABLED_RESPONSE);
|
||||
await visit('/vault/replication');
|
||||
|
||||
await assertTitle(assert, 'Enable Replication');
|
||||
await assertTitle(assert, 'Enable replication');
|
||||
|
||||
// Nav links
|
||||
assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
|
||||
assert.dom(GENERAL.navHeading('Replication')).exists('shows replication nav heading');
|
||||
assert.dom(GENERAL.navLink('Performance replication')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster recovery')).exists('shows dr link');
|
||||
|
||||
await click(GENERAL.navLink('Performance'));
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication/performance', 'it navigates to the correct page');
|
||||
await settled();
|
||||
assert.dom(s.enableForm).exists();
|
||||
|
||||
await click(GENERAL.navLink('Disaster Recovery'));
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
|
||||
await assertTitle(assert, 'Enable Disaster Recovery Replication', 'dr');
|
||||
await assertTitle(assert, 'Enable disaster recovery replication', 'dr');
|
||||
});
|
||||
|
||||
['primary', 'secondary'].forEach((mode) => {
|
||||
|
|
@ -89,16 +90,16 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
|
|||
assert.dom(s.detailLink('dr')).hasText('Enable', 'CTA to enable dr');
|
||||
|
||||
// Nav links
|
||||
assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
|
||||
assert.dom(GENERAL.navLink('Performance replication')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster recovery')).exists('shows dr link');
|
||||
|
||||
await click(GENERAL.navLink('Performance'));
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
assert.strictEqual(currentURL(), `/vault/replication/performance`, `goes to correct URL`);
|
||||
await waitFor(s.dashboard);
|
||||
assert.dom(s.dashboard).exists(`it shows the replication dashboard`);
|
||||
|
||||
await click(GENERAL.navLink('Disaster Recovery'));
|
||||
await assertTitle(assert, 'Enable Disaster Recovery Replication', 'dr');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
await assertTitle(assert, 'Enable disaster recovery replication', 'dr');
|
||||
assert.dom(s.enableForm).exists('it shows the enable view for dr');
|
||||
});
|
||||
});
|
||||
|
|
@ -114,16 +115,16 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
|
|||
assert.dom(s.detailLink('dr')).hasText('Details', 'CTA to see dr details');
|
||||
|
||||
// Nav links
|
||||
assert.dom(GENERAL.navLink('Performance')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster Recovery')).exists('shows dr link');
|
||||
assert.dom(GENERAL.navLink('Performance replication')).exists('shows performance link');
|
||||
assert.dom(GENERAL.navLink('Disaster recovery')).exists('shows dr link');
|
||||
|
||||
await click(GENERAL.navLink('Performance'));
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
assert.strictEqual(currentURL(), `/vault/replication/performance`, `goes to correct URL`);
|
||||
await waitFor(s.enableForm);
|
||||
assert.dom(s.enableForm).exists('it shows the enable view for performance');
|
||||
|
||||
await click(GENERAL.navLink('Disaster Recovery'));
|
||||
await assertTitle(assert, 'Disaster Recovery', 'Disaster Recovery');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
await assertTitle(assert, 'Disaster recovery', 'Disaster recovery');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
assert.dom(s.dashboard).exists(`it shows the replication dashboard`);
|
||||
});
|
||||
|
|
@ -134,17 +135,17 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
|
|||
performance: mockReplicationBlock('primary'),
|
||||
});
|
||||
await visit('/vault/replication');
|
||||
await assertTitle(assert, 'Disaster Recovery & Performance', 'Disaster Recovery & Performance');
|
||||
await assertTitle(assert, 'Disaster recovery and performance', 'Disaster recovery and performance');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
assert.dom(s.summaryCard).exists({ count: 2 }, 'shows 2 summary cards');
|
||||
|
||||
await click(GENERAL.navLink('Performance'));
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
await assertTitle(assert, 'Performance', 'Performance');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
assert.dom(s.enableForm).doesNotExist();
|
||||
|
||||
await click(GENERAL.navLink('Disaster Recovery'));
|
||||
await assertTitle(assert, 'Disaster Recovery', 'Disaster Recovery');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
await assertTitle(assert, 'Disaster recovery', 'Disaster recovery');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
assert.dom(s.enableForm).doesNotExist();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||
await settled();
|
||||
assert
|
||||
.dom(GENERAL.hdsPageHeaderTitle)
|
||||
.includesText('Disaster Recovery', 'it displays the replication type correctly');
|
||||
.includesText('Disaster recovery', 'it displays the replication type correctly');
|
||||
assert.dom(GENERAL.badge('primary')).includesText('primary', 'it displays the cluster mode correctly');
|
||||
|
||||
await addSecondary(secondaryName);
|
||||
|
|
@ -256,7 +256,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
|
|||
.doesNotExist(`does not render replication summary card when both modes are not enabled as primary`);
|
||||
|
||||
// enable DR primary replication
|
||||
await click('[data-test-sidebar-nav-link="Disaster Recovery"]');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
// let the controller set replicationMode in afterModel
|
||||
await waitFor('[data-test-replication-enable-form]');
|
||||
await click(GENERAL.submitButton);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { click, currentURL } from '@ember/test-helpers';
|
||||
import { click, currentURL, visit } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from '../helpers/general-selectors';
|
||||
|
|
@ -27,26 +27,8 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
|
|||
await click(GENERAL.navLink('Secrets Sync'));
|
||||
assert.strictEqual(currentURL(), '/vault/sync/secrets/overview', 'Sync route renders');
|
||||
|
||||
await click(GENERAL.navLink('Replication'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication', 'Replication route renders');
|
||||
assert.dom(panel('Replication')).exists(`Replication nav panel renders`);
|
||||
assert.dom(GENERAL.navLink('Overview')).hasClass('active', 'Overview link is active');
|
||||
assert.dom(GENERAL.navLink('Performance')).exists('Performance link exists');
|
||||
assert.dom(GENERAL.navLink('Disaster Recovery')).exists('DR link exists');
|
||||
|
||||
await click(GENERAL.navLink('Performance'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
'/vault/replication/performance',
|
||||
'Replication performance route renders'
|
||||
);
|
||||
|
||||
await click(GENERAL.navLink('Disaster Recovery'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication DR 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');
|
||||
await click(GENERAL.navLink('Client count'));
|
||||
assert.dom(panel('Client count')).exists('Client count nav panel renders');
|
||||
assert.dom(GENERAL.navLink('Client Usage')).hasClass('active', 'Client Usage link is active');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Client counts route renders');
|
||||
await click(GENERAL.navLink('Back to main navigation'));
|
||||
|
|
@ -109,4 +91,16 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
|
|||
await click(GENERAL.navLink('License'));
|
||||
assert.strictEqual(currentURL(), '/vault/license', 'License route renders');
|
||||
});
|
||||
|
||||
test('it should navigate to Resilience and recovery level > Replication (enterprise)', async function (assert) {
|
||||
await visit('/vault/replication');
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
assert.dom(GENERAL.navLink('Performance replication')).exists('Performance link exists');
|
||||
await click(GENERAL.navLink('Overview'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication', 'replication route renders');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
assert.strictEqual(currentURL(), '/vault/replication/dr', 'Replication DR route renders');
|
||||
await click(GENERAL.navLink('Secrets recovery'));
|
||||
assert.strictEqual(currentURL(), '/vault/recovery/snapshots', 'snapshots route renders');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ module('Acceptance | recovery | snapshots', function (hooks) {
|
|||
|
||||
await visit('/vault/recovery/snapshots');
|
||||
assert
|
||||
.dom(`${GENERAL.navLink('Secrets Recovery')} .hds-badge`)
|
||||
.dom(`${GENERAL.navLink('Secrets recovery')} .hds-badge`)
|
||||
.hasText('Enterprise', 'side nav link renders "Enterprise" badge');
|
||||
assert.strictEqual(currentURL(), '/vault/recovery/snapshots');
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('Secrets Recovery is an enterprise feature');
|
||||
|
|
@ -94,7 +94,7 @@ module('Acceptance | recovery | snapshots', function (hooks) {
|
|||
};
|
||||
});
|
||||
|
||||
await click(GENERAL.navLink('Secrets Recovery'));
|
||||
await click(GENERAL.navLink('Resilience and recovery'));
|
||||
assert.strictEqual(currentURL(), '/vault/recovery/snapshots/1234/manage');
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.recovery.snapshots.snapshot.manage');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,13 +8,10 @@ import { setupApplicationTest } from 'ember-qunit';
|
|||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import replicationHandlers from 'vault/mirage/handlers/replication';
|
||||
import { click } from '@ember/test-helpers';
|
||||
import { click, visit } from '@ember/test-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const SELECTORS = {
|
||||
navReplication: '[data-test-sidebar-nav-link="Replication"]',
|
||||
navPerformance: '[data-test-sidebar-nav-link="Performance"]',
|
||||
navDR: '[data-test-sidebar-nav-link="Disaster Recovery"]',
|
||||
title: '[data-test-replication-title]',
|
||||
primaryCluster: `${GENERAL.infoRowValue('primary_cluster_addr')}`,
|
||||
replicationSet: `${GENERAL.infoRowValue('Replication set')}`,
|
||||
|
|
@ -30,10 +27,11 @@ module('Acceptance | Enterprise | replication navigation', function (hooks) {
|
|||
});
|
||||
|
||||
test('navigate between replication types updates page', async function (assert) {
|
||||
await click(SELECTORS.navReplication);
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster Recovery & Performance');
|
||||
await visit('/vault/replication');
|
||||
await click(GENERAL.navLink('Overview'));
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster recovery and performance');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
await click(SELECTORS.navPerformance);
|
||||
await click(GENERAL.navLink('Performance replication'));
|
||||
|
||||
// Ensure data is expected for performance
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Performance');
|
||||
|
|
@ -43,8 +41,8 @@ module('Acceptance | Enterprise | replication navigation', function (hooks) {
|
|||
assert.dom(SELECTORS.knownSecondariesTitle).hasText('0 Known secondaries');
|
||||
|
||||
// Nav to DR and see updated data
|
||||
await click(SELECTORS.navDR);
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster Recovery');
|
||||
await click(GENERAL.navLink('Disaster recovery'));
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster recovery');
|
||||
assert.dom(GENERAL.badge('primary')).exists('shows primary badge for dr');
|
||||
assert.dom(SELECTORS.primaryCluster).hasText('dr-foobar');
|
||||
assert.dom(SELECTORS.replicationSet).hasText('dr-cluster-id');
|
||||
|
|
|
|||
77
ui/tests/acceptance/resilience-recovery-test.js
Normal file
77
ui/tests/acceptance/resilience-recovery-test.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { click, currentRouteName, currentURL } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from '../helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
import * as displayNavItem from 'core/helpers/display-nav-item';
|
||||
|
||||
module('Acceptance | Enterprise | resilience-recovery', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.permissionsStub = sinon.stub(displayNavItem, 'computeNavBar');
|
||||
|
||||
await login();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.permissionsStub.restore();
|
||||
});
|
||||
|
||||
test('it should redirect to recovery snapshots route when replication is disabled', async function (assert) {
|
||||
this.permissionsStub.callsFake((context, routeName) => {
|
||||
if (routeName === displayNavItem.RouteName.SECRETS_RECOVERY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
await click(GENERAL.navLink('Resilience and recovery'));
|
||||
|
||||
assert.strictEqual(currentURL(), '/vault/recovery/snapshots', 'snapshots url renders');
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
'vault.cluster.recovery.snapshots.index',
|
||||
'snapshots route renders'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should redirect to seal route when user has permission and secrets recovery is not supported', async function (assert) {
|
||||
this.permissionsStub.callsFake((context, routeName) => {
|
||||
if (routeName === displayNavItem.RouteName.SEAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
await click(GENERAL.navLink('Resilience and recovery'));
|
||||
|
||||
assert.strictEqual(currentURL(), '/vault/settings/seal', 'seal route renders');
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.seal', 'seal route renders');
|
||||
});
|
||||
|
||||
test('it should redirect to replication route when secrets recovery or seal is not supported', async function (assert) {
|
||||
this.permissionsStub.callsFake((context, routeName) => {
|
||||
if (routeName === displayNavItem.RouteName.REPLICATION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
await click(GENERAL.navLink('Resilience and recovery'));
|
||||
|
||||
assert.strictEqual(currentURL(), '/vault/replication', 'replication route renders');
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.replication.index', 'replication route renders');
|
||||
});
|
||||
});
|
||||
|
|
@ -40,7 +40,7 @@ module('Acceptance | sidebar navigation', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should link to correct routes at the cluster level', async function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(8);
|
||||
|
||||
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
|
||||
|
||||
|
|
@ -59,7 +59,6 @@ module('Acceptance | sidebar navigation', function (hooks) {
|
|||
|
||||
const links = [
|
||||
{ label: 'Raft Storage', route: '/vault/storage/raft' },
|
||||
{ label: 'Seal Vault', route: '/vault/settings/seal' },
|
||||
{ label: 'Secrets Engines', route: '/vault/secrets-engines' },
|
||||
{ label: 'Dashboard', route: '/vault/dashboard' },
|
||||
];
|
||||
|
|
@ -115,8 +114,8 @@ module('Acceptance | sidebar navigation', function (hooks) {
|
|||
|
||||
test('it should link to correct routes at the client counts level', async function (assert) {
|
||||
assert.expect(7);
|
||||
await click(link('Client Count'));
|
||||
assert.dom(panel('Client Count')).exists('Client counts nav panel renders');
|
||||
await click(link('Client count'));
|
||||
assert.dom(panel('Client count')).exists('Client counts nav panel renders');
|
||||
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'Top level nav link renders overview');
|
||||
assert.dom(link('Client Usage')).hasClass('active');
|
||||
await click(link('Configuration'));
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const disableReplication = async (type, assert) => {
|
|||
|
||||
await click('[data-test-disable-replication] button');
|
||||
|
||||
const typeDisplay = type === 'dr' ? 'Disaster Recovery' : 'Performance';
|
||||
const typeDisplay = type === 'dr' ? 'Disaster recovery' : 'Performance';
|
||||
await fillIn('[data-test-confirmation-modal-input="Disable Replication?"]', typeDisplay);
|
||||
await click(GENERAL.confirmButton);
|
||||
await settled(); // eslint-disable-line
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ module('Integration | Component | replication page/mode-index', function (hooks)
|
|||
test('it renders correctly when replication disabled', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(S.title).hasText('Enable Disaster Recovery Replication');
|
||||
assert.dom(S.title).hasText('Enable disaster recovery replication');
|
||||
assert.dom(S.enableForm).exists();
|
||||
assert.dom(S.notAllowed).doesNotExist();
|
||||
assert.dom(GENERAL.submitButton).exists('Enable button shows by default if no permissions available');
|
||||
|
|
@ -82,7 +82,7 @@ module('Integration | Component | replication page/mode-index', function (hooks)
|
|||
test('it renders correctly when replication disabled', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(S.title).hasText('Enable Performance Replication');
|
||||
assert.dom(S.title).hasText('Enable performance replication');
|
||||
assert.dom(S.enableForm).exists();
|
||||
assert.dom(S.notAllowed).doesNotExist();
|
||||
assert.dom(GENERAL.submitButton).exists('Enable button shows by default if no permissions available');
|
||||
|
|
|
|||
|
|
@ -24,6 +24,23 @@ module('Integration | Component | recovery/snapshots', function (hooks) {
|
|||
this.renderComponent = () => render(hbs`<Recovery::Page::Snapshots @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}}/>`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.emptyStateTitle)
|
||||
.hasText('Raft storage required ', ' Raft storage required empty state title renders');
|
||||
assert
|
||||
.dom(GENERAL.emptyStateMessage)
|
||||
.hasText(
|
||||
'Raft storage must be used in order to recover data from a snapshot.',
|
||||
'Raft storage empty state message'
|
||||
);
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Snapshot management', 'Raft storage empty state action');
|
||||
});
|
||||
|
||||
test('it displays empty state in CE', async function (assert) {
|
||||
this.model = { snapshots: [], showCommunityMessage: true };
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ module('Integration | Component | replication-page', function (hooks) {
|
|||
await render(
|
||||
hbs`<ReplicationPage @model={{this.model}} as |Page|><Page.header @showTabs={{false}} /></ReplicationPage>`
|
||||
);
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster Recovery');
|
||||
assert.dom(GENERAL.hdsPageHeaderTitle).hasText('Disaster recovery');
|
||||
|
||||
this.model.replicationMode = 'performance';
|
||||
await render(
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
|
|||
|
||||
assert
|
||||
.dom(GENERAL.navLink())
|
||||
.exists({ count: 3 }, 'Nav links are hidden other than secrets, recovery and dashboard');
|
||||
.exists(
|
||||
{ count: 3 },
|
||||
'Nav links are hidden other than secrets, recovery (nested in Resilience and recovery nav link) and dashboard'
|
||||
);
|
||||
assert.dom(GENERAL.navHeading()).exists({ count: 1 }, 'Headings are hidden other than Vault');
|
||||
});
|
||||
|
||||
|
|
@ -60,16 +63,14 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
|
|||
'Dashboard',
|
||||
'Secrets Engines',
|
||||
'Secrets Sync',
|
||||
'Secrets Recovery',
|
||||
'Access',
|
||||
'Operational tools',
|
||||
'Replication',
|
||||
'Resilience and recovery',
|
||||
'Reporting',
|
||||
'Raft Storage',
|
||||
'Client Count',
|
||||
'Seal Vault',
|
||||
'Client count',
|
||||
];
|
||||
// do not add PKI-only Secrets feature as it hides Client Count nav link
|
||||
// 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();
|
||||
|
|
@ -81,14 +82,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
|
|||
});
|
||||
|
||||
test('it should hide enterprise related links in child namespace', async function (assert) {
|
||||
const links = [
|
||||
'Disaster Recovery',
|
||||
'Performance',
|
||||
'Replication',
|
||||
'Raft Storage',
|
||||
'License',
|
||||
'Seal Vault',
|
||||
];
|
||||
const links = ['Raft Storage', 'License'];
|
||||
this.owner.lookup('service:namespace').set('path', 'foo');
|
||||
const stubs = stubFeaturesAndPermissions(this.owner, true, true);
|
||||
stubs.hasNavPermission.callsFake((route) => route !== 'clients');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* 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 { capitalize } from '@ember/string';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const renderComponent = () => {
|
||||
return render(hbs`
|
||||
<Sidebar::Frame @isVisible={{true}}>
|
||||
<Sidebar::Nav::ResilienceAndRecovery />
|
||||
</Sidebar::Frame>
|
||||
`);
|
||||
};
|
||||
|
||||
module('Integration | Component | sidebar-nav-resilience-and-recovery', 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 user does not have access to other than secrets recovery', async function (assert) {
|
||||
await renderComponent();
|
||||
stubFeaturesAndPermissions(this.owner);
|
||||
assert
|
||||
.dom(GENERAL.navLink())
|
||||
.exists({ count: 2 }, 'Nav links are hidden other than back and recovery link');
|
||||
});
|
||||
|
||||
test('it should render nav headings and links', async function (assert) {
|
||||
const links = [
|
||||
'Back to main navigation',
|
||||
'Secrets recovery',
|
||||
'Seal Vault',
|
||||
'Overview',
|
||||
'Performance replication',
|
||||
'Disaster recovery',
|
||||
];
|
||||
stubFeaturesAndPermissions(this.owner, true);
|
||||
await renderComponent();
|
||||
|
||||
assert.dom(GENERAL.navHeading()).exists({ count: 2 }, 'Correct number of headings render');
|
||||
assert
|
||||
.dom(GENERAL.navHeading('Resilience and recovery'))
|
||||
.hasText('Resilience and recovery', 'Resilience and recovery heading renders');
|
||||
assert.dom(GENERAL.navHeading('Replication')).hasText('Replication', 'Replication heading renders');
|
||||
|
||||
assert.dom(GENERAL.navLink()).exists({ count: links.length }, 'Correct number of links render');
|
||||
links.forEach((link) => {
|
||||
const name = capitalize(link);
|
||||
assert.dom(GENERAL.navLink(name)).hasText(name, `${name} link renders`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it shows Seal Vault when user is enterprise and in root namespace and has nav permissions', async function (assert) {
|
||||
stubFeaturesAndPermissions(this.owner, true, true, [], true);
|
||||
await renderComponent();
|
||||
assert.dom(GENERAL.navLink('Seal Vault')).exists();
|
||||
});
|
||||
|
||||
test('it does NOT show snapshots when user is in HVD admin namespace', async function (assert) {
|
||||
const stubs = stubFeaturesAndPermissions(this.owner, true, false, [], false);
|
||||
stubs.hasNavPermission.callsFake((route) => route === 'snapshots');
|
||||
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
const namespace = this.owner.lookup('service:namespace');
|
||||
namespace.setNamespace('admin');
|
||||
|
||||
await renderComponent();
|
||||
|
||||
assert.dom(GENERAL.navLink('Secrets recovery')).doesNotExist();
|
||||
});
|
||||
});
|
||||
279
ui/tests/integration/helpers/display-nav-item-test.js
Normal file
279
ui/tests/integration/helpers/display-nav-item-test.js
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { computeNavBar, NavSection, RouteName } from 'core/helpers/display-nav-item';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import sinon from 'sinon';
|
||||
import { ROOT_NAMESPACE } from 'vault/services/namespace';
|
||||
|
||||
class PermissionsService extends Service {
|
||||
@tracked globPaths = null;
|
||||
@tracked exactPaths = null;
|
||||
@tracked canViewAll = false;
|
||||
@tracked chrootNamespace = null;
|
||||
|
||||
hasNavPermission(...args) {
|
||||
const [route, routeParams, requireAll] = args;
|
||||
void (typeof route === 'string');
|
||||
void (routeParams === undefined || Array.isArray(routeParams));
|
||||
void (requireAll === undefined || typeof requireAll === 'boolean');
|
||||
|
||||
if (this.canViewAll) return true;
|
||||
if (this.globPaths || this.exactPaths) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module('Unit | Helper | displayNavItem', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.register('service:permissions', PermissionsService);
|
||||
this.permissions = this.owner.lookup('service:permissions');
|
||||
this.namespace = this.owner.lookup('service:namespace');
|
||||
this.currentCluster = this.owner.lookup('service:current-cluster');
|
||||
this.flags = this.owner.lookup('service:flags');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
|
||||
this.permissionsStub = sinon.stub(this.permissions, 'hasNavPermission');
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.permissionsStub.restore();
|
||||
});
|
||||
|
||||
module('client count', function () {
|
||||
test('it returns true when there are permissions and cluster is not secondary', function (assert) {
|
||||
this.permissionsStub.returns(true);
|
||||
this.currentCluster.setCluster({
|
||||
name: 'cluster-0',
|
||||
hasChrootNamespace: false,
|
||||
dr: { isSecondary: false },
|
||||
});
|
||||
|
||||
this.version.features = [];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, NavSection.CLIENT_COUNT);
|
||||
|
||||
assert.true(supportsClientCount);
|
||||
});
|
||||
|
||||
test('it returns false when there are no permissions and cluster is secondary', function (assert) {
|
||||
this.permissionsStub.returns(false);
|
||||
|
||||
this.currentCluster.setCluster({
|
||||
hasChrootNamespace: true,
|
||||
dr: { isSecondary: true },
|
||||
});
|
||||
|
||||
this.version.features = ['PKI-only Secrets'];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, NavSection.CLIENT_COUNT);
|
||||
|
||||
assert.false(supportsClientCount);
|
||||
});
|
||||
});
|
||||
|
||||
module('vault usage', function () {
|
||||
test('it returns true when there are permissions and is enterprise', function (assert) {
|
||||
this.permissionsStub.returns(true);
|
||||
this.version.type = 'enterprise';
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.VAULT_USAGE);
|
||||
|
||||
assert.true(supportsClientCount);
|
||||
});
|
||||
|
||||
test('it returns false when there are no permissions', function (assert) {
|
||||
this.permissionsStub.returns(false);
|
||||
|
||||
this.version.type = 'community';
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.VAULT_USAGE);
|
||||
|
||||
assert.false(supportsClientCount);
|
||||
});
|
||||
});
|
||||
|
||||
module('license', function () {
|
||||
test('it returns true when there are permissions and is enterprise', function (assert) {
|
||||
this.permissionsStub.returns(true);
|
||||
this.version.features = ['Secrets sync'];
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({
|
||||
hasChrootNamespace: false,
|
||||
dr: { isSecondary: false },
|
||||
});
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.LICENSE);
|
||||
|
||||
assert.true(supportsClientCount);
|
||||
});
|
||||
|
||||
test('it returns false when there are no permissions', function (assert) {
|
||||
this.permissionsStub.returns(false);
|
||||
this.version.features = ['Secrets sync'];
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.LICENSE);
|
||||
|
||||
assert.false(supportsClientCount);
|
||||
});
|
||||
});
|
||||
module('reporting', function () {
|
||||
test('it returns true when user can access vault usage', function (assert) {
|
||||
this.permissionsStub.returns(true);
|
||||
this.version.type = 'enterprise';
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.REPORTING);
|
||||
|
||||
assert.true(supportsClientCount);
|
||||
});
|
||||
|
||||
test('it returns true when can support license but not vault usage', function (assert) {
|
||||
this.permissionsStub.returns(true);
|
||||
this.version.features = ['Secrets sync'];
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({
|
||||
hasChrootNamespace: false,
|
||||
dr: { isSecondary: false },
|
||||
});
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.REPORTING);
|
||||
|
||||
assert.true(supportsClientCount);
|
||||
});
|
||||
|
||||
test('it returns false when user cannot access vault usage or cannot support license', function (assert) {
|
||||
this.permissionsStub.returns(false);
|
||||
this.version.features = [];
|
||||
this.version.type = 'community';
|
||||
|
||||
const supportsClientCount = computeNavBar(this, RouteName.LICENSE);
|
||||
|
||||
assert.false(supportsClientCount);
|
||||
});
|
||||
});
|
||||
|
||||
module('secrets recovery', function () {
|
||||
test('it returns true when dr is not secondary', function (assert) {
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: false } });
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.SECRETS_RECOVERY);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns true when flags is not hvd managed and dr is secondary', function (assert) {
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: true } });
|
||||
this.flags.featureFlags = [];
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.SECRETS_RECOVERY);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns false when flags is hvd managed and dr is secondary', function (assert) {
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: false } });
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.SECRETS_RECOVERY);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
});
|
||||
|
||||
module('seal', function () {
|
||||
test('it returns true when in root namespace and has permissions', function (assert) {
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.permissionsStub.returns(true);
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: false } });
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.SEAL);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns false when flags is hvd managed and dr is secondary', function (assert) {
|
||||
this.namespace.path = 'child-namespace';
|
||||
this.permissionsStub.returns(false);
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: true } });
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.SEAL);
|
||||
|
||||
assert.false(supportsSecretsRecovery);
|
||||
});
|
||||
});
|
||||
|
||||
module('replication', function () {
|
||||
test('it returns true when in root namespace, on enterprise, and has permissions', function (assert) {
|
||||
this.version.type = 'enterprise';
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({
|
||||
replicationRedacted: false,
|
||||
hasChrootNamespace: false,
|
||||
});
|
||||
this.permissionsStub.returns(true);
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.REPLICATION);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns false when hasChrootNamespace is true and type is community', function (assert) {
|
||||
this.version.type = 'community';
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({
|
||||
replicationRedacted: false,
|
||||
hasChrootNamespace: true,
|
||||
});
|
||||
this.permissionsStub.returns(false);
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, RouteName.REPLICATION);
|
||||
|
||||
assert.false(supportsSecretsRecovery);
|
||||
});
|
||||
});
|
||||
|
||||
module('resilience and recovery', function () {
|
||||
test('it returns true when supports secrets recovery', function (assert) {
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: false } });
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, NavSection.RESILIENCE_AND_RECOVERY);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns true when supports replication but does not support secrets recovery or cannot seal', function (assert) {
|
||||
this.version.type = 'enterprise';
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: true }, replicationRedacted: false });
|
||||
|
||||
this.permissionsStub.returns(true);
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, NavSection.RESILIENCE_AND_RECOVERY);
|
||||
|
||||
assert.true(supportsSecretsRecovery);
|
||||
});
|
||||
|
||||
test('it returns false when does not supports replication, support secrets recovery and cannot seal', function (assert) {
|
||||
this.version.type = 'community';
|
||||
this.namespace.path = ROOT_NAMESPACE;
|
||||
this.currentCluster.setCluster({ dr: { isSecondary: true }, replicationRedacted: true });
|
||||
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
|
||||
|
||||
this.permissionsStub.returns(false);
|
||||
|
||||
const supportsSecretsRecovery = computeNavBar(this, NavSection.RESILIENCE_AND_RECOVERY);
|
||||
|
||||
assert.false(supportsSecretsRecovery);
|
||||
});
|
||||
});
|
||||
});
|
||||
2
ui/types/vault/services/permissions.d.ts
vendored
2
ui/types/vault/services/permissions.d.ts
vendored
|
|
@ -16,5 +16,5 @@ export default class PermissionsService extends Service {
|
|||
canViewAll: boolean | null;
|
||||
permissionsBanner: string | null;
|
||||
chrootNamespace: string | null | undefined;
|
||||
hasNavPermission: (string) => boolean;
|
||||
hasNavPermission: (navItem: string, routeParams?: string | string[], requireAll?: boolean) => boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue