+
{{#if (eq columnAttr "intermediateMount")}}
{{data}}
diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js
new file mode 100644
index 000000000000..d86f0082cef2
--- /dev/null
+++ b/ui/tests/acceptance/dashboard-test.js
@@ -0,0 +1,471 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ settled,
+ fillIn,
+ click,
+ waitUntil,
+ find,
+ currentRouteName,
+} from '@ember/test-helpers';
+import { setupApplicationTest } from 'vault/tests/helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { create } from 'ember-cli-page-object';
+import { selectChoose } from 'ember-power-select/test-support/helpers';
+import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
+import { deleteEngineCmd } from 'vault/tests/helpers/commands';
+import authPage from 'vault/tests/pages/auth';
+import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
+import consoleClass from 'vault/tests/pages/components/console/ui-panel';
+import ENV from 'vault/config/environment';
+import { formatNumber } from 'core/helpers/format-number';
+import { pollCluster } from 'vault/tests/helpers/poll-cluster';
+import { disableReplication } from 'vault/tests/helpers/replication';
+import connectionPage from 'vault/tests/pages/secrets/backend/database/connection';
+
+// selectors
+import SECRETS_ENGINE_SELECTORS from 'vault/tests/helpers/components/dashboard/secrets-engines-card';
+import VAULT_CONFIGURATION_SELECTORS from 'vault/tests/helpers/components/dashboard/vault-configuration-details-card';
+import QUICK_ACTION_SELECTORS from 'vault/tests/helpers/components/dashboard/quick-actions-card';
+import REPLICATION_CARD_SELECTORS from 'vault/tests/helpers/components/dashboard/replication-card';
+
+const consoleComponent = create(consoleClass);
+
+module('Acceptance | landing page dashboard', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ test('navigate to dashboard on login', async function (assert) {
+ await authPage.login();
+ assert.strictEqual(currentURL(), '/vault/dashboard');
+ });
+
+ test('display the version number for the title', async function (assert) {
+ await authPage.login();
+ await visit('/vault/dashboard');
+ const version = this.owner.lookup('service:version');
+ const versionName = version.version;
+ const versionNameEnd = version.isEnterprise ? versionName.indexOf('+') : versionName.length;
+ assert
+ .dom('[data-test-dashboard-version-header]')
+ .hasText(`Vault v${versionName.slice(0, versionNameEnd)} root`);
+ });
+
+ module('secrets engines card', function (hooks) {
+ hooks.beforeEach(async function () {
+ await authPage.login();
+ });
+
+ test('shows a secrets engine card', async function (assert) {
+ await mountSecrets.enable('pki', 'pki');
+ await settled();
+ await visit('/vault/dashboard');
+ assert.dom(SECRETS_ENGINE_SELECTORS.cardTitle).hasText('Secrets engines');
+ assert.dom('[data-test-secrets-engines-card-show-all]').doesNotExist();
+ // cleanup engine mount
+ await consoleComponent.runCommands(deleteEngineCmd('pki'));
+ });
+
+ test('it adds disabled css styling to unsupported secret engines', async function (assert) {
+ await mountSecrets.enable('nomad', 'nomad');
+ await settled();
+ await visit('/vault/dashboard');
+ assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist();
+ assert.dom('[data-test-secrets-engines-card-show-all]').doesNotExist();
+ // cleanup engine mount
+ await consoleComponent.runCommands(deleteEngineCmd('nomad'));
+ });
+ });
+
+ module('learn more card', function (hooks) {
+ hooks.beforeEach(function () {
+ return authPage.login();
+ });
+ test('shows the learn more card on community', async function (assert) {
+ await visit('/vault/dashboard');
+ assert.dom('[data-test-learn-more-title]').hasText('Learn more');
+ assert
+ .dom('[data-test-learn-more-subtext]')
+ .hasText(
+ 'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
+ );
+ assert.dom('[data-test-learn-more-links] a').exists({ count: 3 });
+ assert
+ .dom('[data-test-feedback-form]')
+ .hasText("Don't see what you're looking for on this page? Let us know via our feedback form .");
+ });
+ test('shows the learn more card on enterprise', async function (assert) {
+ await visit('/vault/dashboard');
+ assert.dom('[data-test-learn-more-title]').hasText('Learn more');
+ assert
+ .dom('[data-test-learn-more-subtext]')
+ .hasText(
+ 'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
+ );
+ assert.dom('[data-test-learn-more-links] a').exists({ count: 4 });
+ assert
+ .dom('[data-test-feedback-form]')
+ .hasText("Don't see what you're looking for on this page? Let us know via our feedback form .");
+ });
+ });
+
+ module('configuration details card', function (hooks) {
+ hooks.beforeEach(async function () {
+ this.data = {
+ api_addr: 'http://127.0.0.1:8200',
+ cache_size: 0,
+ cluster_addr: 'https://127.0.0.1:8201',
+ cluster_cipher_suites: '',
+ cluster_name: '',
+ default_lease_ttl: 0,
+ default_max_request_duration: 0,
+ detect_deadlocks: '',
+ disable_cache: false,
+ disable_clustering: false,
+ disable_indexing: false,
+ disable_mlock: true,
+ disable_performance_standby: false,
+ disable_printable_check: false,
+ disable_sealwrap: false,
+ disable_sentinel_trace: false,
+ enable_response_header_hostname: false,
+ enable_response_header_raft_node_id: false,
+ enable_ui: true,
+ experiments: null,
+ introspection_endpoint: false,
+ listeners: [
+ {
+ config: {
+ address: '0.0.0.0:8200',
+ cluster_address: '0.0.0.0:8201',
+ tls_disable: true,
+ },
+ type: 'tcp',
+ },
+ ],
+ log_format: '',
+ log_level: 'debug',
+ log_requests_level: '',
+ max_lease_ttl: '48h',
+ pid_file: '',
+ plugin_directory: '',
+ plugin_file_permissions: 0,
+ plugin_file_uid: 0,
+ raw_storage_endpoint: true,
+ seals: [
+ {
+ disabled: false,
+ type: 'shamir',
+ },
+ ],
+ storage: {
+ cluster_addr: 'https://127.0.0.1:8201',
+ disable_clustering: false,
+ raft: {
+ max_entry_size: '',
+ },
+ redirect_addr: 'http://127.0.0.1:8200',
+ type: 'raft',
+ },
+ telemetry: {
+ add_lease_metrics_namespace_labels: false,
+ circonus_api_app: '',
+ circonus_api_token: '',
+ circonus_api_url: '',
+ circonus_broker_id: '',
+ circonus_broker_select_tag: '',
+ circonus_check_display_name: '',
+ circonus_check_force_metric_activation: '',
+ circonus_check_id: '',
+ circonus_check_instance_id: '',
+ circonus_check_search_tag: '',
+ circonus_check_tags: '',
+ circonus_submission_interval: '',
+ circonus_submission_url: '',
+ disable_hostname: true,
+ dogstatsd_addr: '',
+ dogstatsd_tags: null,
+ lease_metrics_epsilon: 3600000000000,
+ maximum_gauge_cardinality: 500,
+ metrics_prefix: '',
+ num_lease_metrics_buckets: 168,
+ prometheus_retention_time: 86400000000000,
+ stackdriver_debug_logs: false,
+ stackdriver_location: '',
+ stackdriver_namespace: '',
+ stackdriver_project_id: '',
+ statsd_address: '',
+ statsite_address: '',
+ usage_gauge_period: 5000000000,
+ },
+ };
+ await authPage.login();
+ });
+
+ test('shows the configuration details card', async function (assert) {
+ this.server.get('sys/config/state/sanitized', () => ({
+ data: this.data,
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ }));
+ await authPage.login();
+ await visit('/vault/dashboard');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.cardTitle).hasText('Configuration details');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.apiAddr).hasText('http://127.0.0.1:8200');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.defaultLeaseTtl).hasText('0');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.maxLeaseTtl).hasText('2 days');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.tlsDisable).hasText('Enabled');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.logFormat).hasText('None');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.logLevel).hasText('debug');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.storageType).hasText('raft');
+ });
+ test('shows the tls disabled if it is disabled', async function (assert) {
+ this.server.get('sys/config/state/sanitized', () => {
+ this.data.listeners[0].config.tls_disable = false;
+ return {
+ data: this.data,
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ };
+ });
+ await authPage.login();
+ await visit('/vault/dashboard');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.tlsDisable).hasText('Disabled');
+ });
+ test('shows the tls disabled if there is no tlsDisabled returned from server', async function (assert) {
+ this.server.get('sys/config/state/sanitized', () => {
+ this.data.listeners = [];
+
+ return {
+ data: this.data,
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ };
+ });
+ await authPage.login();
+ await visit('/vault/dashboard');
+ assert.dom(VAULT_CONFIGURATION_SELECTORS.tlsDisable).hasText('Disabled');
+ });
+ });
+
+ module('quick actions card', function (hooks) {
+ hooks.beforeEach(async function () {
+ await authPage.login();
+ });
+
+ test('shows the default state of the quick actions card', async function (assert) {
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).exists();
+ });
+
+ test('shows the correct actions and links associated with pki', async function (assert) {
+ await mountSecrets.enable('pki', 'pki');
+ await runCommands([
+ `write pki/roles/some-role \
+ issuer_ref="default" \
+ allowed_domains="example.com" \
+ allow_subdomains=true \
+ max_ttl="720h"`,
+ ]);
+ await runCommands([`write pki/root/generate/internal issuer_name="Hashicorp" common_name="Hello"`]);
+ await settled();
+ await visit('/vault/dashboard');
+ await selectChoose(QUICK_ACTION_SELECTORS.secretsEnginesSelect, 'pki');
+ await fillIn(QUICK_ACTION_SELECTORS.actionSelect, 'Issue certificate');
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).doesNotExist();
+ assert.dom(QUICK_ACTION_SELECTORS.paramsTitle).hasText('Role to use');
+
+ await selectChoose(QUICK_ACTION_SELECTORS.paramSelect, 'some-role');
+ assert.dom(QUICK_ACTION_SELECTORS.getActionButton('Issue leaf certificate')).exists({ count: 1 });
+ await click(QUICK_ACTION_SELECTORS.getActionButton('Issue leaf certificate'));
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.roles.role.generate');
+
+ await visit('/vault/dashboard');
+
+ await selectChoose(QUICK_ACTION_SELECTORS.secretsEnginesSelect, 'pki');
+ await fillIn(QUICK_ACTION_SELECTORS.actionSelect, 'View certificate');
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).doesNotExist();
+ assert.dom(QUICK_ACTION_SELECTORS.paramsTitle).hasText('Certificate serial number');
+ assert.dom(QUICK_ACTION_SELECTORS.getActionButton('View certificate')).exists({ count: 1 });
+ await selectChoose(QUICK_ACTION_SELECTORS.paramSelect, '.ember-power-select-option', 0);
+ await click(QUICK_ACTION_SELECTORS.getActionButton('View certificate'));
+ assert.strictEqual(
+ currentRouteName(),
+ 'vault.cluster.secrets.backend.pki.certificates.certificate.details'
+ );
+
+ await visit('/vault/dashboard');
+
+ await selectChoose(QUICK_ACTION_SELECTORS.secretsEnginesSelect, 'pki');
+ await fillIn(QUICK_ACTION_SELECTORS.actionSelect, 'View issuer');
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).doesNotExist();
+ assert.dom(QUICK_ACTION_SELECTORS.paramsTitle).hasText('Issuer');
+ assert.dom(QUICK_ACTION_SELECTORS.getActionButton('View issuer')).exists({ count: 1 });
+ await selectChoose(QUICK_ACTION_SELECTORS.paramSelect, '.ember-power-select-option', 0);
+ await click(QUICK_ACTION_SELECTORS.getActionButton('View issuer'));
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details');
+
+ // cleanup engine mount
+ await consoleComponent.runCommands(deleteEngineCmd('pki'));
+ });
+
+ const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
+ const name = `connection-${Date.now()}`;
+ await connectionPage.visitCreate({ backend });
+ await connectionPage.dbPlugin(plugin);
+ await connectionPage.name(name);
+ await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`);
+ await connectionPage.toggleVerify();
+ await connectionPage.save();
+ await connectionPage.enable();
+ return name;
+ };
+
+ test('shows the correct actions and links associated with database', async function (assert) {
+ await mountSecrets.enable('database', 'database');
+ await newConnection('database');
+ await runCommands([
+ `write database/roles/my-role \
+ db_name=mongodb-database-plugin \
+ creation_statements='{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }' \
+ default_ttl="1h" \
+ max_ttl="24h`,
+ ]);
+ await settled();
+ await visit('/vault/dashboard');
+ await selectChoose(QUICK_ACTION_SELECTORS.secretsEnginesSelect, 'database');
+ await fillIn(QUICK_ACTION_SELECTORS.actionSelect, 'Generate credentials for database');
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).doesNotExist();
+ assert.dom(QUICK_ACTION_SELECTORS.paramsTitle).hasText('Role to use');
+ assert.dom(QUICK_ACTION_SELECTORS.getActionButton('Generate credentials')).exists({ count: 1 });
+ await selectChoose(QUICK_ACTION_SELECTORS.paramSelect, '.ember-power-select-option', 0);
+ await click(QUICK_ACTION_SELECTORS.getActionButton('Generate credentials'));
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.credentials');
+ await consoleComponent.runCommands(deleteEngineCmd('database'));
+ });
+
+ test('shows the correct actions and links associated with kv v1', async function (assert) {
+ await runCommands(['write sys/mounts/kv type=kv', 'write kv/foo bar=baz']);
+ await settled();
+ await visit('/vault/dashboard');
+ await selectChoose(QUICK_ACTION_SELECTORS.secretsEnginesSelect, 'kv');
+ await fillIn(QUICK_ACTION_SELECTORS.actionSelect, 'Find KV secrets');
+ assert.dom(QUICK_ACTION_SELECTORS.emptyState).doesNotExist();
+ assert.dom(QUICK_ACTION_SELECTORS.paramsTitle).hasText('Secret path');
+ assert.dom(QUICK_ACTION_SELECTORS.getActionButton('Read secrets')).exists({ count: 1 });
+ await consoleComponent.runCommands(deleteEngineCmd('kv'));
+ });
+ });
+
+ module('replication and client count card community version', function () {
+ test('hides replication card for community version', async function (assert) {
+ await visit('/vault/dashboard');
+ assert.dom('[data-test-replication-card]').doesNotExist();
+ });
+
+ test('hides the client count card in community version', async function (assert) {
+ assert.dom('[data-test-client-count-card]').doesNotExist();
+ });
+ });
+
+ module('client counts card enterprise', function (hooks) {
+ hooks.before(async function () {
+ ENV['ember-cli-mirage'].handler = 'clients';
+ });
+
+ hooks.beforeEach(async function () {
+ this.store = this.owner.lookup('service:store');
+
+ await authPage.login();
+ });
+
+ hooks.after(function () {
+ ENV['ember-cli-mirage'].handler = null;
+ });
+
+ test('shows the client count card for enterprise', async function (assert) {
+ const version = this.owner.lookup('service:version');
+ assert.true(version.isEnterprise, 'version is enterprise');
+ assert.strictEqual(currentURL(), '/vault/dashboard');
+ assert.dom('[data-test-client-count-card]').exists();
+ const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
+ assert.dom('[data-test-client-count-title]').hasText('Client count');
+ assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
+ assert
+ .dom('[data-test-stat-text="total-clients"] .stat-value')
+ .hasText(formatNumber([response.total.clients]));
+ assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
+ assert
+ .dom('[data-test-stat-text="new-clients"] .stat-text')
+ .hasText('The number of clients new to Vault in the current month.');
+ assert
+ .dom('[data-test-stat-text="new-clients"] .stat-value')
+ .hasText(formatNumber([response.byMonth.lastObject.new_clients.clients]));
+ });
+ });
+
+ module('replication card enterprise', function (hooks) {
+ hooks.beforeEach(async function () {
+ await authPage.login();
+ await settled();
+ await disableReplication('dr');
+ await settled();
+ await disableReplication('performance');
+ await settled();
+ });
+
+ test('shows the replication card empty state in enterprise version', async function (assert) {
+ await visit('/vault/dashboard');
+ const version = this.owner.lookup('service:version');
+ assert.true(version.isEnterprise, 'vault is enterprise');
+ assert.dom(REPLICATION_CARD_SELECTORS.replicationEmptyState).exists();
+ assert.dom(REPLICATION_CARD_SELECTORS.replicationEmptyStateTitle).hasText('Replication not set up');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.replicationEmptyStateMessage)
+ .hasText('Data will be listed here. Enable a primary replication cluster to get started.');
+ assert.dom(REPLICATION_CARD_SELECTORS.replicationEmptyStateActions).hasText('Enable replication');
+ });
+
+ test('it should show replication status if both dr and performance replication are enabled as features in enterprise', async function (assert) {
+ const version = this.owner.lookup('service:version');
+ assert.true(version.isEnterprise, 'vault is enterprise');
+ await visit('/vault/replication');
+ assert.strictEqual(currentURL(), '/vault/replication');
+ await click('[data-test-replication-type-select="performance"]');
+ await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
+ await click('[data-test-replication-enable]');
+ await pollCluster(this.owner);
+ assert.ok(
+ await waitUntil(() => find('[data-test-replication-dashboard]')),
+ 'details dashboard is shown'
+ );
+ await visit('/vault/dashboard');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getReplicationTitle('dr-perf', 'DR primary'))
+ .hasText('DR primary');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary'))
+ .hasText('not set up');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getStateTooltipIcon('dr-perf', 'DR primary', 'x-circle'))
+ .exists();
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getReplicationTitle('dr-perf', 'Perf primary'))
+ .hasText('Perf primary');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getStateTooltipTitle('dr-perf', 'Perf primary'))
+ .hasText('running');
+ assert
+ .dom(REPLICATION_CARD_SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'check-circle'))
+ .exists();
+ });
+ });
+});
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
index 3115290ca9b0..abf2f826fbc5 100644
--- a/ui/tests/acceptance/enterprise-replication-test.js
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -12,40 +12,10 @@ import { pollCluster } from 'vault/tests/helpers/poll-cluster';
import { create } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
import ss from 'vault/tests/pages/components/search-select';
-
+import { disableReplication } from 'vault/tests/helpers/replication';
const searchSelect = create(ss);
const flash = create(flashMessage);
-const disableReplication = async (type, assert) => {
- // disable performance replication
- await visit(`/vault/replication/${type}`);
-
- if (findAll('[data-test-replication-link="manage"]').length) {
- await click('[data-test-replication-link="manage"]');
-
- await click('[data-test-disable-replication] button');
-
- const typeDisplay = type === 'dr' ? 'Disaster Recovery' : 'Performance';
- await fillIn('[data-test-confirmation-modal-input="Disable Replication?"]', typeDisplay);
- await click('[data-test-confirm-button]');
- await settled(); // eslint-disable-line
-
- if (assert) {
- // bypassing for now -- remove if tests pass reliably
- // assert.strictEqual(
- // flash.latestMessage,
- // 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.',
- // 'renders info flash when disabled'
- // );
- assert.ok(
- await waitUntil(() => currentURL() === '/vault/replication'),
- 'redirects to the replication page'
- );
- }
- await settled();
- }
-};
-
module('Acceptance | Enterprise | replication', function (hooks) {
setupApplicationTest(hooks);
diff --git a/ui/tests/acceptance/mfa-login-test.js b/ui/tests/acceptance/mfa-login-test.js
index 1adb112d6146..e05d5dfb7074 100644
--- a/ui/tests/acceptance/mfa-login-test.js
+++ b/ui/tests/acceptance/mfa-login-test.js
@@ -42,7 +42,7 @@ module('Acceptance | mfa-login', function (hooks) {
await click('[data-test-auth-submit]');
};
const didLogin = (assert) => {
- assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backends', 'Route transitions after login');
+ assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'Route transitions after login');
};
const validate = async (multi) => {
await fillIn('[data-test-mfa-passcode="0"]', 'test');
diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js
index 2bceb8400855..5276c9d02019 100644
--- a/ui/tests/acceptance/secrets/backend/database/secret-test.js
+++ b/ui/tests/acceptance/secrets/backend/database/secret-test.js
@@ -453,6 +453,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
await authPage.logout();
// Check with restricted permissions
await authPage.login(token);
+ await click('[data-test-sidebar-nav-link="Secrets engines"]');
assert.dom(`[data-test-auth-backend-link="${backend}"]`).exists('Shows backend on secret list page');
await navToConnection(backend, connection);
assert.strictEqual(
diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
index 9e123006db40..fbf49af4351a 100644
--- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js
@@ -709,6 +709,8 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
await settled();
await authPage.login(userToken);
await settled();
+ // on login users are directed to dashboard, so we would need to visit the vault secrets page to click on an engine
+ await visit('vault/secrets');
// test if metadata tab there with no read access message and no ability to edit.
await click(`[data-test-auth-backend-link=${enginePath}]`);
assert
diff --git a/ui/tests/acceptance/sidebar-nav-test.js b/ui/tests/acceptance/sidebar-nav-test.js
index 3587563b08d6..c264ef56826a 100644
--- a/ui/tests/acceptance/sidebar-nav-test.js
+++ b/ui/tests/acceptance/sidebar-nav-test.js
@@ -27,8 +27,13 @@ module('Acceptance | sidebar navigation', function (hooks) {
return authPage.login();
});
+ test('it should navigate back to the dashboard when logo is clicked', async function (assert) {
+ await click('[data-test-sidebar-logo]');
+ assert.strictEqual(currentURL(), '/vault/dashboard', 'dashboard route renders');
+ });
+
test('it should link to correct routes at the cluster level', async function (assert) {
- assert.expect(10);
+ assert.expect(11);
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
@@ -50,6 +55,7 @@ module('Acceptance | sidebar navigation', function (hooks) {
{ label: 'Raft Storage', route: '/vault/storage/raft' },
{ label: 'Seal Vault', route: '/vault/settings/seal' },
{ label: 'Secrets engines', route: '/vault/secrets' },
+ { label: 'Dashboard', route: '/vault/dashboard' },
];
for (const l of links) {
diff --git a/ui/tests/acceptance/wrapped-token-test.js b/ui/tests/acceptance/wrapped-token-test.js
index 8242e4a0d4fd..6afeaefd4410 100644
--- a/ui/tests/acceptance/wrapped-token-test.js
+++ b/ui/tests/acceptance/wrapped-token-test.js
@@ -39,14 +39,22 @@ module('Acceptance | wrapped_token query param functionality', function (hooks)
const token = await setupWrapping();
await auth.visit({ wrapped_token: token });
await settled();
- assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home');
+ assert.strictEqual(
+ currentURL(),
+ '/vault/dashboard',
+ 'authenticates and redirects to home (dashboard page)'
+ );
});
test('it authenticates when used with the with=token query param', async function (assert) {
const token = await setupWrapping();
await auth.visit({ wrapped_token: token, with: 'token' });
await settled();
- assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home');
+ assert.strictEqual(
+ currentURL(),
+ '/vault/dashboard',
+ 'authenticates and redirects to home (dashboard page)'
+ );
});
test('it should authenticate when hitting logout url with wrapped_token when logged out', async function (assert) {
@@ -55,6 +63,10 @@ module('Acceptance | wrapped_token query param functionality', function (hooks)
});
await visit(`/vault/logout?wrapped_token=1234`);
- assert.strictEqual(currentURL(), '/vault/secrets', 'authenticates and redirects to home');
+ assert.strictEqual(
+ currentURL(),
+ '/vault/dashboard',
+ 'authenticates and redirects to home (dashboard page)'
+ );
});
});
diff --git a/ui/tests/helpers/components/dashboard/quick-actions-card.js b/ui/tests/helpers/components/dashboard/quick-actions-card.js
new file mode 100644
index 000000000000..f49d900d38df
--- /dev/null
+++ b/ui/tests/helpers/components/dashboard/quick-actions-card.js
@@ -0,0 +1,11 @@
+const SELECTORS = {
+ searchSelect: '.search-select',
+ secretsEnginesSelect: '[data-test-secrets-engines-select]',
+ actionSelect: '[data-test-select="action-select"]',
+ emptyState: '[data-test-no-mount-selected-empty]',
+ paramsTitle: '[data-test-search-select-params-title]',
+ paramSelect: '[data-test-param-select]',
+ getActionButton: (action) => `[data-test-button="${action}"]`,
+};
+
+export default SELECTORS;
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..76eb7da354cc
--- /dev/null
+++ b/ui/tests/helpers/components/dashboard/replication-card.js
@@ -0,0 +1,26 @@
+/**
+ * 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-replication-card] [data-test-component="empty-state"]',
+ replicationEmptyStateTitle:
+ '[data-test-replication-card] [data-test-component="empty-state"] .empty-state-title',
+ replicationEmptyStateMessage:
+ '[data-test-replication-card] [data-test-component="empty-state"] .empty-state-message',
+ replicationEmptyStateActions:
+ '[data-test-replication-card] [data-test-component="empty-state"] .empty-state-actions',
+ replicationEmptyStateActionsLink:
+ '[data-test-replication-card] [data-test-component="empty-state"] .empty-state-actions a',
+};
+
+export default SELECTORS;
diff --git a/ui/tests/helpers/components/dashboard/secrets-engines-card.js b/ui/tests/helpers/components/dashboard/secrets-engines-card.js
new file mode 100644
index 000000000000..b03fa8d43987
--- /dev/null
+++ b/ui/tests/helpers/components/dashboard/secrets-engines-card.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+const SELECTORS = {
+ cardTitle: '[data-test-dashboard-secrets-engines-header]',
+ secretEnginesTableRows: '[data-test-dashboard-secrets-engines-table] tr',
+ getSecretEngineAccessor: (engineId) => `[data-test-secrets-engines-row=${engineId}] [data-test-accessor]`,
+ getSecretEngineDescription: (engineId) =>
+ `[data-test-secrets-engines-row=${engineId}] [data-test-description]`,
+};
+
+export default SELECTORS;
diff --git a/ui/tests/helpers/components/dashboard/vault-configuration-details-card.js b/ui/tests/helpers/components/dashboard/vault-configuration-details-card.js
new file mode 100644
index 000000000000..11a05067d5a3
--- /dev/null
+++ b/ui/tests/helpers/components/dashboard/vault-configuration-details-card.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+const SELECTORS = {
+ cardTitle: '[data-test-configuration-details-title]',
+ apiAddr: '[data-test-vault-config-details="api_addr"]',
+ defaultLeaseTtl: '[data-test-vault-config-details="default_lease_ttl"]',
+ maxLeaseTtl: '[data-test-vault-config-details="max_lease_ttl"]',
+ tlsDisable: '[data-test-vault-config-details="tls_disable"]',
+ logFormat: '[data-test-vault-config-details="log_format"]',
+ logLevel: '[data-test-vault-config-details="log_level"]',
+ storageType: '[data-test-vault-config-details="type"]',
+};
+
+export default SELECTORS;
diff --git a/ui/tests/helpers/replication.js b/ui/tests/helpers/replication.js
new file mode 100644
index 000000000000..fca104cf627b
--- /dev/null
+++ b/ui/tests/helpers/replication.js
@@ -0,0 +1,31 @@
+import { click, fillIn, findAll, currentURL, visit, settled, waitUntil } from '@ember/test-helpers';
+
+export const disableReplication = async (type, assert) => {
+ // disable performance replication
+ await visit(`/vault/replication/${type}`);
+
+ if (findAll('[data-test-replication-link="manage"]').length) {
+ await click('[data-test-replication-link="manage"]');
+
+ await click('[data-test-disable-replication] button');
+
+ const typeDisplay = type === 'dr' ? 'Disaster Recovery' : 'Performance';
+ await fillIn('[data-test-confirmation-modal-input="Disable Replication?"]', typeDisplay);
+ await click('[data-test-confirm-button]');
+ await settled(); // eslint-disable-line
+
+ if (assert) {
+ // bypassing for now -- remove if tests pass reliably
+ // assert.strictEqual(
+ // flash.latestMessage,
+ // 'This cluster is having replication disabled. Vault will be unavailable for a brief period and will resume service shortly.',
+ // 'renders info flash when disabled'
+ // );
+ assert.ok(
+ await waitUntil(() => currentURL() === '/vault/replication'),
+ 'redirects to the replication page'
+ );
+ }
+ await settled();
+ }
+};
diff --git a/ui/tests/integration/components/dashboard/client-count-card-test.js b/ui/tests/integration/components/dashboard/client-count-card-test.js
new file mode 100644
index 000000000000..f749c6bcfd36
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/client-count-card-test.js
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { render, click } 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 { parseAPITimestamp } from 'core/utils/date-formatters';
+
+module('Integration | Component | dashboard/client-count-card', function (hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.license = {
+ startTime: '2018-04-03T14:15:30',
+ };
+ });
+
+ test('it should display client count information', async function (assert) {
+ this.server.get('sys/internal/counters/activity', () => {
+ return {
+ request_id: 'some-activity-id',
+ data: {
+ months: [
+ {
+ timestamp: '2023-08-01T00:00:00-07:00',
+ counts: {},
+ namespaces: [
+ {
+ namespace_id: 'root',
+ namespace_path: '',
+ counts: {},
+ mounts: [{ mount_path: 'auth/up2/', counts: {} }],
+ },
+ ],
+ new_clients: {
+ counts: {
+ clients: 12,
+ },
+ namespaces: [
+ {
+ namespace_id: 'root',
+ namespace_path: '',
+ counts: {
+ clients: 12,
+ },
+ mounts: [{ mount_path: 'auth/up2/', counts: {} }],
+ },
+ ],
+ },
+ },
+ ],
+ total: {
+ clients: 300417,
+ entity_clients: 73150,
+ non_entity_clients: 227267,
+ },
+ },
+ };
+ });
+
+ await render(hbs``);
+ assert.dom('[data-test-client-count-title]').hasText('Client count');
+ assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
+ assert
+ .dom('[data-test-stat-text="total-clients"] .stat-text')
+ .hasText(
+ `The number of clients in this billing period (Apr 2018 - ${parseAPITimestamp(
+ timestamp.now().toISOString(),
+ 'MMM yyyy'
+ )}).`
+ );
+ assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('300,417');
+ assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
+ assert
+ .dom('[data-test-stat-text="new-clients"] .stat-text')
+ .hasText('The number of clients new to Vault in the current month.');
+ assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('12');
+ this.server.get('sys/internal/counters/activity', () => {
+ return {
+ request_id: 'some-activity-id',
+ data: {
+ months: [
+ {
+ timestamp: '2023-09-01T00:00:00-07:00',
+ counts: {},
+ namespaces: [
+ {
+ namespace_id: 'root',
+ namespace_path: '',
+ counts: {},
+ mounts: [{ mount_path: 'auth/up2/', counts: {} }],
+ },
+ ],
+ new_clients: {
+ counts: {
+ clients: 5,
+ },
+ namespaces: [
+ {
+ namespace_id: 'root',
+ namespace_path: '',
+ counts: {
+ clients: 12,
+ },
+ mounts: [{ mount_path: 'auth/up2/', counts: {} }],
+ },
+ ],
+ },
+ },
+ ],
+ total: {
+ clients: 120,
+ entity_clients: 100,
+ non_entity_clients: 100,
+ },
+ },
+ };
+ });
+ await click('[data-test-refresh]');
+ assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
+ assert
+ .dom('[data-test-stat-text="total-clients"] .stat-text')
+ .hasText(
+ `The number of clients in this billing period (Apr 2018 - ${parseAPITimestamp(
+ timestamp.now().toISOString(),
+ 'MMM yyyy'
+ )}).`
+ );
+ assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('120');
+ assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
+ assert
+ .dom('[data-test-stat-text="new-clients"] .stat-text')
+ .hasText('The number of clients new to Vault in the current month.');
+ assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('5');
+ });
+});
diff --git a/ui/tests/integration/components/dashboard/quick-actions-card-test.js b/ui/tests/integration/components/dashboard/quick-actions-card-test.js
new file mode 100644
index 000000000000..3d021c7a0b14
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/quick-actions-card-test.js
@@ -0,0 +1,131 @@
+/**
+ * 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 { fillIn } from '@ember/test-helpers';
+import { selectChoose } from 'ember-power-select/test-support/helpers';
+
+import SELECTORS from 'vault/tests/helpers/components/dashboard/quick-actions-card';
+
+module('Integration | Component | dashboard/quick-actions-card', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'kubernetes_f3400dee',
+ path: 'kubernetes-test/',
+ type: 'kubernetes',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'database_f3400dee',
+ path: 'database-test/',
+ type: 'database',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_i1234dd',
+ path: 'apki-test/',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'secrets_j2350ii',
+ path: 'secrets-test/',
+ type: 'kv',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'nomad_123hh',
+ path: 'nomad/',
+ type: 'nomad',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_f3400dee',
+ path: 'pki-0-test/',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_i1234dd',
+ path: 'pki-1-test/',
+ description: 'pki-1-path-description',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'secrets_j2350ii',
+ path: 'secrets-1-test/',
+ type: 'kv',
+ },
+ });
+
+ this.secretsEngines = this.store.peekAll('secret-engine', {});
+
+ this.renderComponent = () => {
+ return render(hbs``);
+ };
+ });
+
+ test('it should show quick action empty state if no engine is selected', async function (assert) {
+ await this.renderComponent();
+ assert.dom('.title').hasText('Quick actions');
+ assert.dom(SELECTORS.secretsEnginesSelect).exists({ count: 1 });
+ assert.dom(SELECTORS.emptyState).exists({ count: 1 });
+ });
+
+ test('it should show correct actions for pki', async function (assert) {
+ await this.renderComponent();
+ await selectChoose(SELECTORS.secretsEnginesSelect, 'pki-0-test');
+ await fillIn(SELECTORS.actionSelect, 'Issue certificate');
+ assert.dom(SELECTORS.emptyState).doesNotExist();
+ await fillIn(SELECTORS.actionSelect, 'Issue certificate');
+ assert.dom(SELECTORS.getActionButton('Issue leaf certificate')).exists({ count: 1 });
+ assert.dom(SELECTORS.paramsTitle).hasText('Role to use');
+ await fillIn(SELECTORS.actionSelect, 'View certificate');
+ assert.dom(SELECTORS.paramsTitle).hasText('Certificate serial number');
+ assert.dom(SELECTORS.getActionButton('View certificate')).exists({ count: 1 });
+ await fillIn(SELECTORS.actionSelect, 'View issuer');
+ assert.dom(SELECTORS.paramsTitle).hasText('Issuer');
+ assert.dom(SELECTORS.getActionButton('View issuer')).exists({ count: 1 });
+ });
+ test('it should show correct actions for database', async function (assert) {
+ await this.renderComponent();
+ await selectChoose(SELECTORS.secretsEnginesSelect, 'database-test');
+ assert.dom(SELECTORS.emptyState).doesNotExist();
+ await fillIn(SELECTORS.actionSelect, 'Generate credentials for database');
+ assert.dom(SELECTORS.paramsTitle).hasText('Role to use');
+ assert.dom(SELECTORS.getActionButton('Generate credentials')).exists({ count: 1 });
+ });
+ test('it should show correct actions for kv', async function (assert) {
+ await this.renderComponent();
+ await selectChoose(SELECTORS.secretsEnginesSelect, 'secrets-1-test');
+ assert.dom(SELECTORS.emptyState).doesNotExist();
+ await fillIn(SELECTORS.actionSelect, 'Find KV secrets');
+ assert.dom(SELECTORS.paramsTitle).hasText('Secret path');
+ assert.dom(SELECTORS.getActionButton('Read secrets')).exists({ count: 1 });
+ });
+});
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');
+ });
+});
diff --git a/ui/tests/integration/components/dashboard/replication-state-text.js b/ui/tests/integration/components/dashboard/replication-state-text.js
new file mode 100644
index 000000000000..956f2f0ea419
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/replication-state-text.js
@@ -0,0 +1,60 @@
+/**
+ * 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 SELECTORS from 'vault/tests/helpers/components/dashboard/replication-card';
+
+module('Integration | Component | dashboard/replication-state-text', function (hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.name = 'DR Primary';
+ this.clusterState = {
+ glyph: 'circle-check',
+ isOk: true,
+ };
+ });
+
+ test('it displays replication states', 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();
+
+ this.name = 'DR Primary';
+ this.clusterState = {
+ glyph: 'x-circle',
+ isOk: false,
+ };
+ await render(
+ hbs`
+
+ `
+ );
+ 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', 'x-circle')).exists();
+ assert
+ .dom(SELECTORS.getStateTooltipIcon('dr-perf', 'Perf primary', 'x-circle'))
+ .hasClass('has-text-danger');
+ });
+});
diff --git a/ui/tests/integration/components/dashboard/secrets-engines-card-test.js b/ui/tests/integration/components/dashboard/secrets-engines-card-test.js
new file mode 100644
index 000000000000..8f4831dee4b9
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/secrets-engines-card-test.js
@@ -0,0 +1,108 @@
+/**
+ * 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 SELECTORS from 'vault/tests/helpers/components/dashboard/secrets-engines-card';
+
+module('Integration | Component | dashboard/secrets-engines-card', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'kubernetes_f3400dee',
+ path: 'kubernetes-test/',
+ type: 'kubernetes',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_i1234dd',
+ path: 'pki-test/',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'secrets_j2350ii',
+ path: 'secrets-test/',
+ type: 'kv',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'nomad_123hh',
+ path: 'nomad/',
+ type: 'nomad',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_f3400dee',
+ path: 'pki-0-test/',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'pki_i1234dd',
+ path: 'pki-1-test/',
+ description: 'pki-1-path-description',
+ type: 'pki',
+ },
+ });
+ this.store.pushPayload('secret-engine', {
+ modelName: 'secret-engine',
+ data: {
+ accessor: 'secrets_j2350ii',
+ path: 'secrets-1-test/',
+ type: 'kv',
+ },
+ });
+
+ this.secretsEngines = this.store.peekAll('secret-engine', {});
+
+ this.renderComponent = () => {
+ return render(hbs``);
+ };
+ });
+
+ test('it should display only five secrets engines', async function (assert) {
+ await this.renderComponent();
+ assert.dom(SELECTORS.cardTitle).hasText('Secrets engines');
+ assert.dom(SELECTORS.secretEnginesTableRows).exists({ count: 5 });
+ });
+
+ test('it should display the secrets engines accessor and path', async function (assert) {
+ await this.renderComponent();
+ assert.dom(SELECTORS.cardTitle).hasText('Secrets engines');
+ assert.dom(SELECTORS.secretEnginesTableRows).exists({ count: 5 });
+
+ this.secretsEngines.slice(0, 5).forEach((engine) => {
+ assert.dom(SELECTORS.getSecretEngineAccessor(engine.id)).hasText(engine.accessor);
+ if (engine.description) {
+ assert.dom(SELECTORS.getSecretEngineDescription(engine.id)).hasText(engine.description);
+ } else {
+ assert.dom(SELECTORS.getSecretEngineDescription(engine.id)).doesNotExist(engine.description);
+ }
+ });
+ });
+
+ test('it adds disabled css styling to unsupported secret engines', async function (assert) {
+ await this.renderComponent();
+ assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist();
+ assert.dom('[data-test-icon="nomad"]').hasClass('has-text-grey');
+ });
+});
diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js
index 15fae626a613..59f8a5370657 100644
--- a/ui/tests/integration/components/sidebar/nav/cluster-test.js
+++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js
@@ -39,7 +39,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
await renderComponent();
assert
.dom('[data-test-sidebar-nav-link]')
- .exists({ count: 1 }, 'Nav links are hidden other than secrets');
+ .exists({ count: 2 }, 'Nav links are hidden other than secrets and dashboard');
assert
.dom('[data-test-sidebar-nav-heading]')
.exists({ count: 1 }, 'Headings are hidden other than Vault');
@@ -47,6 +47,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
test('it should render nav links', async function (assert) {
const links = [
+ 'Dashboard',
'Secrets engines',
'Access',
'Policies',
diff --git a/ui/tests/unit/routes/vault/cluster/dashboard-test.js b/ui/tests/unit/routes/vault/cluster/dashboard-test.js
new file mode 100644
index 000000000000..4fc36dad3e7c
--- /dev/null
+++ b/ui/tests/unit/routes/vault/cluster/dashboard-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'vault/tests/helpers';
+
+module('Unit | Route | vault/cluster/dashboard', function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ const route = this.owner.lookup('route:vault/cluster/dashboard');
+ assert.ok(route);
+ });
+});