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'); + }); +});