diff --git a/ui/app/controllers/vault/cluster/dashboard.js b/ui/app/controllers/vault/cluster/dashboard.js
new file mode 100644
index 000000000000..d60df0d03df2
--- /dev/null
+++ b/ui/app/controllers/vault/cluster/dashboard.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Controller from '@ember/controller';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import timestamp from 'core/utils/timestamp';
+
+export default class DashboardController extends Controller {
+ @tracked replicationUpdatedAt = timestamp.now().toISOString();
+
+ @action
+ refreshModel() {
+ this.replicationUpdatedAt = timestamp.now().toISOString();
+ this.send('refreshRoute');
+ }
+}
diff --git a/ui/app/routes/vault/cluster/dashboard.js b/ui/app/routes/vault/cluster/dashboard.js
index 52d487fe5fe3..37e84b36f4fe 100644
--- a/ui/app/routes/vault/cluster/dashboard.js
+++ b/ui/app/routes/vault/cluster/dashboard.js
@@ -8,9 +8,11 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
// eslint-disable-next-line ember/no-mixins
import ClusterRoute from 'vault/mixins/cluster-route';
+import { action } from '@ember/object';
export default class VaultClusterDashboardRoute extends Route.extend(ClusterRoute) {
@service store;
+ @service namespace;
@service version;
async getVaultConfiguration() {
@@ -33,12 +35,24 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout
model() {
const vaultConfiguration = this.getVaultConfiguration();
+ const clusterModel = this.modelFor('vault.cluster');
+ const replication = {
+ dr: clusterModel.dr,
+ performance: clusterModel.performance,
+ };
return hash({
vaultConfiguration,
+ replication,
secretsEngines: this.store.query('secret-engine', {}),
+ isRootNamespace: this.namespace.inRootNamespace,
version: this.version,
license: this.getLicense(),
});
}
+
+ @action
+ refreshRoute() {
+ this.refresh();
+ }
}
diff --git a/ui/app/styles/core/title.scss b/ui/app/styles/core/title.scss
index f1fecec940e4..5d632b0491f5 100644
--- a/ui/app/styles/core/title.scss
+++ b/ui/app/styles/core/title.scss
@@ -47,6 +47,10 @@
&.is-7 {
font-size: $size-7;
}
+
+ &.is-8 {
+ font-size: $size-8;
+ }
}
.form-section .title {
diff --git a/ui/app/templates/components/dashboard/replication-card.hbs b/ui/app/templates/components/dashboard/replication-card.hbs
new file mode 100644
index 000000000000..b5e8b0dadba2
--- /dev/null
+++ b/ui/app/templates/components/dashboard/replication-card.hbs
@@ -0,0 +1,92 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+
+ Replication
+
+
+
+ Details
+
+
+
+{{! check if dr replication and performance replication exists }}
+{{#if (or @replication.dr.clusterId @replication.performance.clusterId)}}
+
+ {{! check if user has access to both perf replication and dr replication }}
+ {{#if (and @version.hasPerfReplication @version.hasDRReplication)}}
+
+
+
+
+ {{! if user only has access to dr replication }}
+ {{else if @version.hasDRReplication}}
+
+ DR Primary
+
+
+
+
+
+
+ {{/if}}
+
+
+
+ Updated
+ {{date-format @updatedAt "MMM dd, yyyy HH:mm:SS"}}
+
+
+{{else}}
+
+
+ Enable replication
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/dashboard/replication-state-text.hbs b/ui/app/templates/components/dashboard/replication-state-text.hbs
new file mode 100644
index 000000000000..2540ed9f4bb2
--- /dev/null
+++ b/ui/app/templates/components/dashboard/replication-state-text.hbs
@@ -0,0 +1,48 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ {{#if @name}}
+
+ {{@title}}
+
+ {{else}}
+
+ {{@title}}
+
+ {{/if}}
+
+ {{#if @subText}}
+
+ {{@subText}}
+
+ {{/if}}
+
+
+
+ {{or @state "not set up"}}
+
+
+
+
+
+ The cluster's current operating state
+
+
+
+
\ No newline at end of file
diff --git a/ui/app/templates/vault/cluster/dashboard.hbs b/ui/app/templates/vault/cluster/dashboard.hbs
index 6526ad9f2064..31f93c4747fa 100644
--- a/ui/app/templates/vault/cluster/dashboard.hbs
+++ b/ui/app/templates/vault/cluster/dashboard.hbs
@@ -6,6 +6,18 @@
{{/if}}
+
+ {{#if (and @model.version.isEnterprise @model.isRootNamespace)}}
+
+
+
+ {{/if}}
+
diff --git a/ui/tests/helpers/components/dashboard/replication-card.js b/ui/tests/helpers/components/dashboard/replication-card.js
new file mode 100644
index 000000000000..c44a13583bca
--- /dev/null
+++ b/ui/tests/helpers/components/dashboard/replication-card.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+const SELECTORS = {
+ getReplicationTitle: (type, name) => `[data-test-${type}-replication] [data-test-title="${name}"]`,
+ getStateTooltipTitle: (type, name) => `[data-test-${type}-replication] [data-test-tooltip-title="${name}"]`,
+ getStateTooltipIcon: (type, name, icon) =>
+ `[data-test-${type}-replication] [data-test-tooltip-title="${name}"] [data-test-icon="${icon}"]`,
+ drOnlyStateSubText: '[data-test-dr-replication] [data-test-subtext="state"]',
+ knownSecondariesLabel: '[data-test-stat-text="known secondaries"] .stat-label',
+ knownSecondariesSubtext: '[data-test-stat-text="known secondaries"] .stat-text',
+ knownSecondariesValue: '[data-test-stat-text="known secondaries"] .stat-value',
+ replicationEmptyState: '[data-test-component="empty-state"]',
+ replicationEmptyStateTitle: '[data-test-component="empty-state"] .empty-state-title',
+ replicationEmptyStateMessage: '[data-test-component="empty-state"] .empty-state-message',
+ replicationEmptyStateActions: '[data-test-component="empty-state"] .empty-state-actions',
+};
+
+export default SELECTORS;
diff --git a/ui/tests/integration/components/dashboard/replication-card-test.js b/ui/tests/integration/components/dashboard/replication-card-test.js
new file mode 100644
index 000000000000..84fb89bad257
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/replication-card-test.js
@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import timestamp from 'core/utils/timestamp';
+import SELECTORS from 'vault/tests/helpers/components/dashboard/replication-card';
+
+module('Integration | Component | dashboard/replication-card', function (hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.replication = {
+ dr: {
+ clusterId: '123',
+ state: 'running',
+ },
+ performance: {
+ clusterId: 'abc-1',
+ state: 'running',
+ },
+ };
+ this.version = {
+ hasPerfReplication: true,
+ hasDRReplication: true,
+ };
+ this.updatedAt = timestamp.now().toISOString();
+ this.refresh = () => {};
+ });
+
+ test('it should display replication information if both dr and performance replication are enabled as features', async function (assert) {
+ await render(
+ hbs`
+
+ `
+ );
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'DR primary')).hasText('DR primary');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary')).hasText('running');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'check-circle')).exists();
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'Perf primary')).hasText('Perf primary');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'Perf primary')).hasText('running');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'check-circle')).exists();
+ });
+ test('it should display replication information if both dr and performance replication are enabled as features and only dr is setup', async function (assert) {
+ this.replication = {
+ dr: {
+ clusterId: '123',
+ state: 'running',
+ },
+ performance: {
+ clusterId: '',
+ },
+ };
+ await render(
+ hbs`
+
+ `
+ );
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'DR primary')).hasText('DR primary');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary')).hasText('running');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'check-circle')).exists();
+ assert
+ .dom(SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'check-circle'))
+ .hasClass('has-text-success');
+
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'Perf primary')).hasText('Perf primary');
+
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'Perf primary')).hasText('not set up');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'x-circle')).exists();
+ assert
+ .dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'x-circle'))
+ .hasClass('has-text-danger');
+ });
+
+ test('it should display only dr replication information if vault version only has hasDRReplication', async function (assert) {
+ this.version = {
+ hasPerfReplication: false,
+ hasDRReplication: true,
+ };
+ this.replication = {
+ dr: {
+ clusterId: '123',
+ state: 'running',
+ knownSecondaries: [{ id: 1 }],
+ },
+ };
+ await render(
+ hbs`
+
+ `
+ );
+ assert.dom(SELECTORS.getReplicationTitle('dr', 'state')).hasText('state');
+ assert.dom(SELECTORS.drOnlyStateSubText).hasText('The current operating state of the cluster.');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr', 'state')).hasText('running');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr', 'state', 'check-circle')).exists();
+ assert.dom(SELECTORS.getStateTooltipIcon('dr', 'state', 'check-circle')).hasClass('has-text-success');
+ assert.dom(SELECTORS.knownSecondariesLabel).hasText('known secondaries');
+ assert.dom(SELECTORS.knownSecondariesSubtext).hasText('Number of secondaries connected to this primary.');
+ assert.dom(SELECTORS.knownSecondariesValue).hasText('1');
+ });
+
+ test('it should show correct icons if dr and performance replication is idle or shutdown states', async function (assert) {
+ this.replication = {
+ dr: {
+ clusterId: 'abc',
+ state: 'idle',
+ },
+ performance: {
+ clusterId: 'def',
+ state: 'shutdown',
+ },
+ };
+ await render(
+ hbs`
+
+ `
+ );
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'DR primary')).hasText('DR primary');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary')).hasText('idle');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'x-square')).exists();
+ assert
+ .dom(SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'x-square'))
+ .hasClass('has-text-danger');
+
+ assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'Perf primary')).hasText('Perf primary');
+ assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'Perf primary')).hasText('shutdown');
+ assert.dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'x-circle')).exists();
+ assert
+ .dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'x-circle'))
+ .hasClass('has-text-danger');
+ });
+
+ test('it should show empty state', async function (assert) {
+ this.replication = {
+ dr: {
+ clusterId: '',
+ },
+ performance: {
+ clusterId: '',
+ },
+ };
+ await render(
+ hbs`
+
+ `
+ );
+ assert.dom(SELECTORS.replicationEmptyState).exists();
+ assert.dom(SELECTORS.replicationEmptyStateTitle).hasText('Replication not set up');
+ assert
+ .dom(SELECTORS.replicationEmptyStateMessage)
+ .hasText('Data will be listed here. Enable a primary replication cluster to get started.');
+ assert.dom(SELECTORS.replicationEmptyStateActions).hasText('Enable replication');
+ });
+});