mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 19:47:54 +00:00
UI: [VAULT-12979] Dashboard Landing Page (#21057)
Co-authored-by: Chelsea Shaw <cshaw@hashicorp.com> Co-authored-by: clairebontempo@gmail.com <clairebontempo@gmail.com> Co-authored-by: Angel Garbarino <angel@hashicorp.com>
This commit is contained in:
3
changelog/21057.txt
Normal file
3
changelog/21057.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:feature
|
||||
**Dashboard UI**: Dashboard is now available in the UI as the new landing page.
|
||||
```
|
||||
63
ui/app/components/dashboard/client-count-card.js
Normal file
63
ui/app/components/dashboard/client-count-card.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import timestamp from 'core/utils/timestamp';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* @module DashboardClientCountCard
|
||||
* DashboardClientCountCard component are used to display total and new client count information
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Dashboard::ClientCountCard @license={{@model.license}} />
|
||||
* ```
|
||||
* @param {object} license - license object passed from the parent
|
||||
*/
|
||||
|
||||
export default class DashboardClientCountCard extends Component {
|
||||
@service store;
|
||||
|
||||
@tracked activityData = null;
|
||||
@tracked clientConfig = null;
|
||||
@tracked updatedAt = timestamp.now().toISOString();
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.fetchClientActivity.perform();
|
||||
this.clientConfig = this.store.queryRecord('clients/config', {}).catch(() => {});
|
||||
}
|
||||
|
||||
get currentMonthActivityTotalCount() {
|
||||
return this.activityData?.byMonth?.lastObject?.new_clients.clients;
|
||||
}
|
||||
|
||||
get licenseStartTime() {
|
||||
return this.args.license.startTime || getStorage().getItem('vault:ui-inputted-start-date') || null;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*fetchClientActivity(e) {
|
||||
if (e) e.preventDefault();
|
||||
this.updatedAt = timestamp.now().toISOString();
|
||||
// only make the network request if we have a start_time
|
||||
if (!this.licenseStartTime) return {};
|
||||
try {
|
||||
this.activityData = yield this.store.queryRecord('clients/activity', {
|
||||
start_time: { timestamp: this.licenseStartTime },
|
||||
end_time: { timestamp: this.updatedAt },
|
||||
});
|
||||
this.noActivityData = this.activityData.activity.id === 'no-data' ? true : false;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
ui/app/components/dashboard/learn-more-card.js
Normal file
44
ui/app/components/dashboard/learn-more-card.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
/**
|
||||
* @module DashboardLearnMoreCard
|
||||
* DashboardLearnMoreCard component are used to display external links
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <DashboardLearnMoreCard />
|
||||
* ```
|
||||
*/
|
||||
|
||||
export default class DashboardLearnMoreCard extends Component {
|
||||
get learnMoreLinks() {
|
||||
return [
|
||||
{
|
||||
link: '/vault/tutorials/secrets-management',
|
||||
icon: 'docs-link',
|
||||
title: 'Secrets Management',
|
||||
},
|
||||
{
|
||||
link: '/vault/tutorials/monitoring',
|
||||
icon: 'docs-link',
|
||||
title: 'Monitor & Troubleshooting',
|
||||
},
|
||||
{
|
||||
link: '/vault/tutorials/adp/transform',
|
||||
icon: 'learn-link',
|
||||
title: 'Advanced Data Protection: Transform engine',
|
||||
requiredFeature: 'Transform Secrets Engine',
|
||||
},
|
||||
{
|
||||
link: '/vault/tutorials/secrets-management/pki-engine',
|
||||
icon: 'learn-link',
|
||||
title: 'Build your own Certificate Authority (CA)',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
147
ui/app/components/dashboard/quick-actions-card.js
Normal file
147
ui/app/components/dashboard/quick-actions-card.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* @module DashboardQuickActionsCard
|
||||
* DashboardQuickActionsCard component allows users to see a list of secrets engines filtered by
|
||||
* kv, pki and database and perform certain actions based on the type of secret engine selected
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Dashboard::QuickActionsCard @secretsEngines={{@model.secretsEngines}} />
|
||||
* ```
|
||||
*/
|
||||
|
||||
const QUICK_ACTION_ENGINES = ['pki', 'kv', 'database'];
|
||||
|
||||
export default class DashboardQuickActionsCard extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked selectedEngine;
|
||||
@tracked selectedAction;
|
||||
@tracked paramValue;
|
||||
|
||||
get actionOptions() {
|
||||
switch (this.selectedEngine.type) {
|
||||
case `kv version ${this.selectedEngine?.version}`:
|
||||
return ['Find KV secrets'];
|
||||
case 'database':
|
||||
return ['Generate credentials for database'];
|
||||
case 'pki':
|
||||
return ['Issue certificate', 'View certificate', 'View issuer'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get searchSelectParams() {
|
||||
switch (this.selectedAction) {
|
||||
case 'Find KV secrets':
|
||||
return {
|
||||
title: 'Secret path',
|
||||
subText: 'Path of the secret you want to read, including the mount. E.g., secret/data/foo.',
|
||||
buttonText: 'Read secrets',
|
||||
// check kv version to figure out which model to use
|
||||
model: this.selectedEngine.version === 2 ? 'secret-v2' : 'secret',
|
||||
route: 'vault.cluster.secrets.backend.show',
|
||||
};
|
||||
case 'Generate credentials for database':
|
||||
return {
|
||||
title: 'Role to use',
|
||||
buttonText: 'Generate credentials',
|
||||
model: 'database/role',
|
||||
route: 'vault.cluster.secrets.backend.credentials',
|
||||
};
|
||||
case 'Issue certificate':
|
||||
return {
|
||||
title: 'Role to use',
|
||||
placeholder: 'Type to find a role',
|
||||
buttonText: 'Issue leaf certificate',
|
||||
model: 'pki/role',
|
||||
route: 'vault.cluster.secrets.backend.pki.roles.role.generate',
|
||||
};
|
||||
case 'View certificate':
|
||||
return {
|
||||
title: 'Certificate serial number',
|
||||
placeholder: '33:a3:...',
|
||||
buttonText: 'View certificate',
|
||||
model: 'pki/certificate/base',
|
||||
route: 'vault.cluster.secrets.backend.pki.certificates.certificate.details',
|
||||
};
|
||||
case 'View issuer':
|
||||
return {
|
||||
title: 'Issuer',
|
||||
placeholder: 'Type issuer name or ID',
|
||||
buttonText: 'View issuer',
|
||||
model: 'pki/issuer',
|
||||
nameKey: 'issuerName',
|
||||
route: 'vault.cluster.secrets.backend.pki.issuers.issuer.details',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
placeholder: 'Please select an action above',
|
||||
buttonText: 'Select an action',
|
||||
model: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get filteredSecretEngines() {
|
||||
return this.args.secretsEngines.filter((engine) => QUICK_ACTION_ENGINES.includes(engine.type));
|
||||
}
|
||||
|
||||
get mountOptions() {
|
||||
return this.filteredSecretEngines.map((engine) => {
|
||||
let { id, type, version } = engine;
|
||||
if (type === 'kv') type = `kv version ${version}`;
|
||||
|
||||
return { name: id, type, id, version };
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
handleSearchEngineSelect([selection]) {
|
||||
this.selectedEngine = selection;
|
||||
// reset tracked properties
|
||||
this.selectedAction = null;
|
||||
this.paramValue = null;
|
||||
}
|
||||
|
||||
@action
|
||||
setSelectedAction(selectedAction) {
|
||||
this.selectedAction = selectedAction;
|
||||
this.paramValue = null;
|
||||
}
|
||||
|
||||
@action
|
||||
handleActionSelect(val) {
|
||||
if (Array.isArray(val)) {
|
||||
this.paramValue = val[0];
|
||||
} else {
|
||||
this.paramValue = val;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
navigateToPage() {
|
||||
let searchSelectParamRoute = this.searchSelectParams.route;
|
||||
|
||||
// kv has a special use case where if the paramValue ends in a '/' you should
|
||||
// link to different route
|
||||
if (this.selectedEngine.type === 'kv') {
|
||||
searchSelectParamRoute =
|
||||
this.paramValue && this.paramValue?.endsWith('/')
|
||||
? 'vault.cluster.secrets.backend.list'
|
||||
: 'vault.cluster.secrets.backend.show';
|
||||
}
|
||||
|
||||
this.router.transitionTo(searchSelectParamRoute, this.selectedEngine.id, this.paramValue);
|
||||
}
|
||||
}
|
||||
27
ui/app/components/dashboard/secrets-engines-card.js
Normal file
27
ui/app/components/dashboard/secrets-engines-card.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
/**
|
||||
* @module DashboardSecretsEnginesCard
|
||||
* DashboardSecretsEnginesCard component are used to display 5 secrets engines to the user.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <DashboardSecretsEnginesCard @secretsEngines={{@model.secretsEngines}} />
|
||||
* ```
|
||||
* @param {array} secretsEngines - list of secrets engines
|
||||
*/
|
||||
|
||||
export default class DashboardSecretsEnginesCard extends Component {
|
||||
get filteredSecretsEngines() {
|
||||
const filteredEngines = this.args.secretsEngines.filter(
|
||||
(secretEngine) => secretEngine.shouldIncludeInList
|
||||
);
|
||||
|
||||
return filteredEngines.slice(0, 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
/**
|
||||
* @module DashboardVaultConfigurationCard
|
||||
* DashboardVaultConfigurationCard component are used to display vault configuration.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <DashboardVaultConfigurationCard @vaultConfiguration={{@model.vaultConfiguration}} />
|
||||
* ```
|
||||
* @param {object} vaultConfiguration - object of vault configuration key/values
|
||||
*/
|
||||
|
||||
export default class DashboardSecretsEnginesCard extends Component {
|
||||
get tlsDisabled() {
|
||||
const tlsDisableConfig = this.args.vaultConfiguration?.listeners.find((listener) => {
|
||||
if (listener.config && listener.config.tls_disable) return listener.config.tls_disable;
|
||||
});
|
||||
|
||||
return tlsDisableConfig?.config.tls_disable ? 'Enabled' : 'Disabled';
|
||||
}
|
||||
}
|
||||
34
ui/app/components/dashboard/vault-version-title.js
Normal file
34
ui/app/components/dashboard/vault-version-title.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* @module DashboardVaultVersionTitle
|
||||
* DashboardVaultVersionTitle component are use to display the vault version title and the badges
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Dashboard::VaultVersionTitle />
|
||||
* ```
|
||||
*/
|
||||
|
||||
export default class DashboardVaultVersionTitle extends Component {
|
||||
@service version;
|
||||
@service namespace;
|
||||
|
||||
get versionHeader() {
|
||||
return this.version.isEnterprise
|
||||
? `Vault v${this.version.version.slice(0, this.version.version.indexOf('+'))}`
|
||||
: `Vault v${this.version.version}`;
|
||||
}
|
||||
|
||||
get namespaceDisplay() {
|
||||
if (this.namespace.inRootNamespace) return 'root';
|
||||
const parts = this.namespace.path?.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export default Component.extend({
|
||||
window.location.hostname +
|
||||
(window.location.port ? ':' + window.location.port : '');
|
||||
|
||||
if (!this.normalizedNamespace) return `${origin}/ui/vault/secrets`;
|
||||
if (!this.normalizedNamespace) return `${origin}/ui/vault/dashboard`;
|
||||
// The full URL/origin is required so that the page is reloaded.
|
||||
return `${origin}/ui/vault/secrets?namespace=${encodeURIComponent(this.normalizedNamespace)}`;
|
||||
return `${origin}/ui/vault/dashboard?namespace=${encodeURIComponent(this.normalizedNamespace)}`;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<:logo>
|
||||
<Hds::SideNav::Header::HomeLink
|
||||
@icon="vault"
|
||||
@route="vault.cluster"
|
||||
@route="vault.cluster.dashboard"
|
||||
@model={{this.currentCluster.cluster.name}}
|
||||
@ariaLabel="home link"
|
||||
data-test-sidebar-logo
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<Hds::SideNav::Portal @ariaLabel="Cluster Navigation Links" data-test-sidebar-nav-panel="Cluster" as |Nav|>
|
||||
<Nav.Title data-test-sidebar-nav-heading="Vault">Vault</Nav.Title>
|
||||
|
||||
<Nav.Link @route="vault.cluster.dashboard" @text="Dashboard" data-test-sidebar-nav-link="Dashboard" />
|
||||
<Nav.Link
|
||||
@route="vault.cluster.secrets"
|
||||
@current-when="vault.cluster.secrets vault.cluster.settings.mount-secret-backend vault.cluster.settings.configure-secret-backend"
|
||||
|
||||
20
ui/app/controllers/vault/cluster/dashboard.js
Normal file
20
ui/app/controllers/vault/cluster/dashboard.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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(e) {
|
||||
if (e) e.preventDefault();
|
||||
this.replicationUpdatedAt = timestamp.now().toISOString();
|
||||
this.send('refreshRoute');
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default class Router extends EmberRouter {
|
||||
Router.map(function () {
|
||||
this.route('vault', { path: '/' }, function () {
|
||||
this.route('cluster', { path: '/:cluster_name' }, function () {
|
||||
this.route('dashboard');
|
||||
this.route('oidc-provider-ns', { path: '/*namespace/identity/oidc/provider/:provider_name/authorize' });
|
||||
this.route('oidc-provider', { path: '/identity/oidc/provider/:provider_name/authorize' });
|
||||
this.route('oidc-callback', { path: '/auth/*auth_path/oidc/callback' });
|
||||
|
||||
49
ui/app/routes/vault/cluster/dashboard.js
Normal file
49
ui/app/routes/vault/cluster/dashboard.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
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() {
|
||||
try {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const configState = await adapter.ajax('/v1/sys/config/state/sanitized', 'GET');
|
||||
return configState.data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
const clusterModel = this.modelFor('vault.cluster');
|
||||
const replication = {
|
||||
dr: clusterModel.dr,
|
||||
performance: clusterModel.performance,
|
||||
};
|
||||
|
||||
return hash({
|
||||
replication,
|
||||
secretsEngines: this.store.query('secret-engine', {}),
|
||||
license: this.store.queryRecord('license', {}).catch(() => null),
|
||||
isRootNamespace: this.namespace.inRootNamespace,
|
||||
version: this.version,
|
||||
vaultConfiguration: this.getVaultConfiguration(),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
refreshRoute() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@ import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
beforeModel() {
|
||||
return this.transitionTo('vault.cluster.secrets');
|
||||
return this.transitionTo('vault.cluster.dashboard');
|
||||
},
|
||||
});
|
||||
|
||||
3
ui/app/styles/components/secrets-engines-card.scss
Normal file
3
ui/app/styles/components/secrets-engines-card.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.secrets-engines-card {
|
||||
min-height: 480px;
|
||||
}
|
||||
@@ -105,6 +105,7 @@
|
||||
@import './components/search-select';
|
||||
@import './components/selectable-card';
|
||||
@import './components/selectable-card-container';
|
||||
@import './components/secrets-engines-card';
|
||||
// action-block extends selectable-card
|
||||
@import './components/action-block';
|
||||
@import './components/shamir-modal-flow';
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
&.is-7 {
|
||||
font-size: $size-7;
|
||||
}
|
||||
|
||||
&.is-8 {
|
||||
font-size: $size-8;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section .title {
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.has-gap-m {
|
||||
gap: $spacing-m;
|
||||
}
|
||||
|
||||
.has-gap-l {
|
||||
gap: $spacing-l;
|
||||
}
|
||||
|
||||
// Alignment of the items
|
||||
.is-flex-v-centered {
|
||||
display: flex;
|
||||
@@ -68,13 +76,17 @@
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.is-flex-1 {
|
||||
.is-flex-grow-1 {
|
||||
flex-grow: 1;
|
||||
&.basis-0 {
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.is-no-flex-grow {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
@@ -97,6 +109,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include until($mobile) {
|
||||
.is-flex-row {
|
||||
flex-flow: column wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS GRID */
|
||||
.is-grid {
|
||||
display: grid;
|
||||
@@ -129,3 +147,7 @@
|
||||
.is-grid-column-span-3 {
|
||||
grid-column-end: span 3;
|
||||
}
|
||||
|
||||
.grid-align-items-start {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.has-border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
// pointer helpers
|
||||
.has-no-pointer {
|
||||
pointer-events: none;
|
||||
@@ -85,6 +89,14 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.truncate-first-line {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// screen reader only
|
||||
.sr-only {
|
||||
border: 0;
|
||||
@@ -103,3 +115,8 @@
|
||||
.border-radius-2 {
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
// border-spacing
|
||||
.is-border-spacing-revert {
|
||||
border-spacing: revert;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,16 @@
|
||||
padding-right: $spacing-s;
|
||||
}
|
||||
|
||||
.has-padding-xxs {
|
||||
padding: $spacing-xxs;
|
||||
}
|
||||
|
||||
.has-padding-m {
|
||||
padding: $spacing-m;
|
||||
}
|
||||
.has-padding-l {
|
||||
padding: $spacing-l;
|
||||
}
|
||||
|
||||
.has-bottom-padding-s {
|
||||
padding-bottom: $spacing-s;
|
||||
@@ -88,6 +95,19 @@
|
||||
margin-bottom: -$spacing-m;
|
||||
}
|
||||
|
||||
.has-top-margin-xxs {
|
||||
margin: $spacing-xxs 0;
|
||||
}
|
||||
.has-right-margin-xxs {
|
||||
margin-right: $spacing-xxs;
|
||||
}
|
||||
.has-left-margin-xxs {
|
||||
margin-left: $spacing-xxs;
|
||||
}
|
||||
.has-bottom-margin-xxs {
|
||||
margin-bottom: $spacing-xxs !important;
|
||||
}
|
||||
|
||||
.has-bottom-margin-xs {
|
||||
margin-bottom: $spacing-xs !important;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="is-flex-1 text">
|
||||
<div class="is-flex-grow-1 text">
|
||||
Sign in with Google
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,25 +27,7 @@
|
||||
{{this.versionText.description}}
|
||||
</p>
|
||||
{{#if this.noActivityData}}
|
||||
{{#if (eq @model.config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received {{if this.dateRangeMessage this.dateRangeMessage}}"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.canEdit}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
<Clients::NoData @config={{@model.config}} @dateRangeMessage={{this.dateRangeMessage}} />
|
||||
{{else if this.errorObject}}
|
||||
<Clients::Error @error={{this.errorObject}} />
|
||||
{{else}}
|
||||
|
||||
19
ui/app/templates/components/clients/no-data.hbs
Normal file
19
ui/app/templates/components/clients/no-data.hbs
Normal file
@@ -0,0 +1,19 @@
|
||||
{{#if (eq @config.enabled "On")}}
|
||||
<EmptyState
|
||||
@title="No data received {{if @dateRangeMessage @dateRangeMessage}}"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @config.canEdit}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.config">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
58
ui/app/templates/components/dashboard/client-count-card.hbs
Normal file
58
ui/app/templates/components/dashboard/client-count-card.hbs
Normal file
@@ -0,0 +1,58 @@
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l has-bottom-padding-m" data-test-client-count-card>
|
||||
<div class="is-flex-between">
|
||||
<h3 class="title is-4 has-bottom-margin-xxs" data-test-client-count-title>
|
||||
Client count
|
||||
</h3>
|
||||
|
||||
<LinkTo @route="vault.cluster.clients.dashboard" class="is-no-underline">Details</LinkTo>
|
||||
</div>
|
||||
|
||||
<hr class="has-background-gray-100" />
|
||||
|
||||
{{#if this.noActivityData}}
|
||||
{{! This will likely not be show since the client activity api was changed to always return data. In the past it
|
||||
would return no activity data. Adding this empty state here to match the current client count behavior }}
|
||||
<Clients::NoData @config={{this.clientConfig}} />
|
||||
{{else}}
|
||||
{{#if this.fetchClientActivity.isRunning}}
|
||||
<VaultLogoSpinner />
|
||||
{{else}}
|
||||
<div class="is-grid grid-2-columns grid-gap-2 has-top-margin-m grid-align-items-start is-flex-v-centered">
|
||||
<StatText
|
||||
@label="Total"
|
||||
@value={{this.activityData.total.clients}}
|
||||
@size="l"
|
||||
@subText="The number of clients in this billing period ({{date-format
|
||||
this.licenseStartTime
|
||||
'MMM yyyy'
|
||||
}} - {{date-format this.updatedAt 'MMM yyyy'}})."
|
||||
data-test-stat-text="total-clients"
|
||||
/>
|
||||
<StatText
|
||||
@label="New"
|
||||
@value={{this.currentMonthActivityTotalCount}}
|
||||
@size="l"
|
||||
@subText="The number of clients new to Vault in the current month."
|
||||
data-test-stat-text="new-clients"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-l is-flex-center">
|
||||
<Hds::Button
|
||||
@text="Refresh"
|
||||
@isIconOnly={{true}}
|
||||
@color="tertiary"
|
||||
@icon="sync"
|
||||
disabled={{this.fetchClientActivity.isRunning}}
|
||||
class="has-padding-xxs"
|
||||
{{on "click" (perform this.fetchClientActivity)}}
|
||||
data-test-refresh
|
||||
/>
|
||||
<small class="has-left-margin-xs has-text-grey">
|
||||
Updated
|
||||
{{date-format this.updatedAt "MMM dd, yyyy HH:mm:SS"}}
|
||||
</small>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
26
ui/app/templates/components/dashboard/learn-more-card.hbs
Normal file
26
ui/app/templates/components/dashboard/learn-more-card.hbs
Normal file
@@ -0,0 +1,26 @@
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l">
|
||||
<h3 class="title is-4 has-bottom-margin-xxs" data-test-learn-more-title>Learn more</h3>
|
||||
<div class="sub-text" data-test-learn-more-subtext>
|
||||
Explore the features of Vault and learn advance practices with the following tutorials and documentation.
|
||||
</div>
|
||||
<div class="has-top-margin-xs" data-test-learn-more-links>
|
||||
{{#each this.learnMoreLinks as |learnMoreLink|}}
|
||||
{{#if
|
||||
(or
|
||||
(and learnMoreLink.requiredFeature @isEnterprise (has-feature learnMoreLink.requiredFeature))
|
||||
(not learnMoreLink.requiredFeature)
|
||||
)
|
||||
}}
|
||||
<DocLink
|
||||
data-test-learn-more-link={{learnMoreLink.title}}
|
||||
@path={{learnMoreLink.link}}
|
||||
class="is-size-8 has-font-weight-normal is-block has-bottom-margin-xxs"
|
||||
>
|
||||
<Icon @name={{learnMoreLink.icon}} />
|
||||
{{learnMoreLink.title}}
|
||||
</DocLink>
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
</div>
|
||||
</Hds::Card::Container>
|
||||
70
ui/app/templates/components/dashboard/quick-actions-card.hbs
Normal file
70
ui/app/templates/components/dashboard/quick-actions-card.hbs
Normal file
@@ -0,0 +1,70 @@
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l">
|
||||
<h3 class="title is-4">Quick actions</h3>
|
||||
|
||||
<div class="has-bottom-margin-m">
|
||||
<h4 class="title is-6">Secrets engines</h4>
|
||||
<SearchSelect
|
||||
@id="secrets-engines-select"
|
||||
@options={{this.mountOptions}}
|
||||
@selectLimit="1"
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="input-search"
|
||||
@onChange={{this.handleSearchEngineSelect}}
|
||||
@placeholder="Type to select a mount"
|
||||
@displayInherit={{true}}
|
||||
@shouldRenderName={{true}}
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "type" "version"}}
|
||||
class="is-marginless"
|
||||
data-test-secrets-engines-select
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.selectedEngine}}
|
||||
<h4 class="title is-6">Action</h4>
|
||||
<Select
|
||||
@name="action-select"
|
||||
@options={{this.actionOptions}}
|
||||
@isFullwidth={{true}}
|
||||
@selectedValue={{this.selectedAction}}
|
||||
@onChange={{this.setSelectedAction}}
|
||||
@noDefault={{true}}
|
||||
/>
|
||||
|
||||
{{#if this.searchSelectParams.model}}
|
||||
<h4 class="title is-6" data-test-search-select-params-title>{{this.searchSelectParams.title}}</h4>
|
||||
|
||||
<SearchSelect
|
||||
class="is-flex-grow-1"
|
||||
@selectLimit="1"
|
||||
@models={{array this.searchSelectParams.model}}
|
||||
@backend={{this.selectedEngine.id}}
|
||||
@placeholder={{this.searchSelectParams.placeholder}}
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{this.handleActionSelect}}
|
||||
@fallbackComponent="input-search"
|
||||
@nameKey={{this.searchSelectParams.nameKey}}
|
||||
@disabled={{not this.searchSelectParams.model}}
|
||||
data-test-param-select
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary has-top-margin-m"
|
||||
disabled={{(not (and this.selectedAction this.selectedEngine this.paramValue))}}
|
||||
{{on "click" this.navigateToPage}}
|
||||
data-test-button={{this.searchSelectParams.buttonText}}
|
||||
>
|
||||
{{this.searchSelectParams.buttonText}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No mount selected"
|
||||
@message="Select a mount above to get started."
|
||||
data-test-no-mount-selected-empty
|
||||
/>
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
94
ui/app/templates/components/dashboard/replication-card.hbs
Normal file
94
ui/app/templates/components/dashboard/replication-card.hbs
Normal file
@@ -0,0 +1,94 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l has-bottom-padding-m" data-test-replication-card>
|
||||
|
||||
<div class="is-flex-between">
|
||||
<h3 class="title is-4 has-bottom-margin-xxs" data-test-client-count-title>
|
||||
Replication
|
||||
</h3>
|
||||
|
||||
<LinkTo class="is-no-underline" @route="vault.cluster.replication.index">
|
||||
Details
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
{{! check if dr replication and performance replication exists }}
|
||||
{{#if (or @replication.dr.clusterId @replication.performance.clusterId)}}
|
||||
<hr class="has-background-gray-100" />
|
||||
{{! check if user has access to both perf replication and dr replication }}
|
||||
{{#if (and @version.hasPerfReplication @version.hasDRReplication)}}
|
||||
<div
|
||||
class="is-grid grid-2-columns grid-gap-2 has-top-margin-m has-bottom-margin-xs grid-align-items-start"
|
||||
data-test-dr-perf-replication
|
||||
>
|
||||
<Dashboard::ReplicationStateText
|
||||
@title="DR primary"
|
||||
@name="dr"
|
||||
@state={{@replication.dr.state}}
|
||||
@clusterStates={{cluster-states @replication.dr.state}}
|
||||
/>
|
||||
<Dashboard::ReplicationStateText
|
||||
@title="Perf primary"
|
||||
@name="performance"
|
||||
@state={{if @replication.performance.clusterId @replication.performance.state "not set up"}}
|
||||
@clusterStates={{if @replication.performance.clusterId (cluster-states @replication.performance.state)}}
|
||||
/>
|
||||
</div>
|
||||
{{! if user only has access to dr replication }}
|
||||
{{else if @version.hasDRReplication}}
|
||||
<LinkTo
|
||||
class="title is-5 has-text-weight-semibold is-marginless"
|
||||
@route="vault.cluster.replication.mode.index"
|
||||
@model="dr"
|
||||
>
|
||||
DR Primary
|
||||
</LinkTo>
|
||||
|
||||
<div
|
||||
class="is-grid grid-2-columns grid-gap-2 has-top-margin-m has-bottom-margin-m grid-align-items-start"
|
||||
data-test-dr-replication
|
||||
>
|
||||
<Dashboard::ReplicationStateText
|
||||
@title="state"
|
||||
@state={{@replication.dr.state}}
|
||||
@clusterStates={{cluster-states @replication.dr.state}}
|
||||
@subText="The current operating state of the cluster."
|
||||
/>
|
||||
<StatText
|
||||
@label="known secondaries"
|
||||
@value={{@replication.dr.knownSecondaries.length}}
|
||||
@size="l"
|
||||
@subText="Number of secondaries connected to this primary."
|
||||
data-test-stat-text="known secondaries"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="has-top-margin-s is-flex-center">
|
||||
<Hds::Button
|
||||
@text="Refresh"
|
||||
@isIconOnly={{true}}
|
||||
@color="tertiary"
|
||||
@icon="sync"
|
||||
class="has-padding-xxs"
|
||||
{{on "click" @refresh}}
|
||||
data-test-refresh
|
||||
/>
|
||||
<small class="has-left-margin-xs has-text-grey">
|
||||
Updated
|
||||
{{date-format @updatedAt "MMM dd, yyyy HH:mm:SS"}}
|
||||
</small>
|
||||
</div>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Replication not set up"
|
||||
@message="Data will be listed here. Enable a primary replication cluster to get started."
|
||||
class="has-top-margin-m"
|
||||
>
|
||||
<div>
|
||||
<LinkTo @route="vault.cluster.replication">Enable replication</LinkTo>
|
||||
</div>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
@@ -0,0 +1,48 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div>
|
||||
{{#if @name}}
|
||||
<LinkTo
|
||||
class="title is-5 has-text-weight-semibold has-bottom-margin-xs"
|
||||
@route="vault.cluster.replication.mode.index"
|
||||
@model={{@name}}
|
||||
data-test-title={{@title}}
|
||||
>
|
||||
{{@title}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<h2 class="title is-5 has-text-weight-semibold has-bottom-margin-xs" data-test-title={{@title}}>
|
||||
{{@title}}
|
||||
</h2>
|
||||
{{/if}}
|
||||
|
||||
{{#if @subText}}
|
||||
<div class="title is-8 has-font-weight-normal has-text-grey-dark" data-test-subtext={{@title}}>
|
||||
{{@subText}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
|
||||
<T.Trigger
|
||||
data-test-tooltip-trigger
|
||||
tabindex="-1"
|
||||
class="title is-3 has-font-weight-normal has-top-margin-xxs has-bottom-margin-xxs"
|
||||
data-test-tooltip-title={{@title}}
|
||||
>
|
||||
{{or @state "not set up"}}
|
||||
|
||||
<FlightIcon
|
||||
@name={{or @clusterStates.glyph "x-circle"}}
|
||||
class={{if @clusterStates.isOk "has-text-success" "has-text-danger"}}
|
||||
/>
|
||||
</T.Trigger>
|
||||
<T.Content @defaultClass="tool-tip smaller-font">
|
||||
<div class="box" data-test-hover-copy-tooltip-text>
|
||||
The cluster's current operating state
|
||||
</div>
|
||||
</T.Content>
|
||||
</ToolTip>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l secrets-engines-card">
|
||||
<h3 class="title is-4 has-left-margin-xxs" data-test-dashboard-secrets-engines-header>Secrets engines</h3>
|
||||
<Hds::Table @caption="Five secrets engines" class="is-border-spacing-revert" data-test-dashboard-secrets-engines-table>
|
||||
<:body as |B|>
|
||||
{{#each this.filteredSecretsEngines as |backend|}}
|
||||
<B.Tr data-test-secrets-engines-row={{backend.id}}>
|
||||
<B.Td class="is-flex-between is-flex-center has-gap-m">
|
||||
<div>
|
||||
<div class="is-flex-center">
|
||||
{{#if backend.icon}}
|
||||
<ToolTip @horizontalPosition="left" as |T|>
|
||||
<T.Trigger>
|
||||
<Icon @name={{backend.icon}} class={{unless backend.isSupportedBackend "has-text-grey"}} />
|
||||
</T.Trigger>
|
||||
<T.Content @defaultClass="tool-tip">
|
||||
<div class="box">
|
||||
{{or backend.engineType backend.path}}
|
||||
</div>
|
||||
</T.Content>
|
||||
</ToolTip>
|
||||
{{/if}}
|
||||
{{#if backend.path}}
|
||||
{{#if backend.isSupportedBackend}}
|
||||
<LinkTo
|
||||
@route={{backend.backendLink}}
|
||||
@model={{backend.id}}
|
||||
class="has-text-black has-text-weight-semibold"
|
||||
data-test-secret-path
|
||||
>
|
||||
{{backend.path}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<span class="has-text-grey" data-test-secret-path>{{backend.path}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if backend.accessor}}
|
||||
<code class="has-text-grey is-size-8" data-test-accessor>
|
||||
{{backend.accessor}}
|
||||
</code>
|
||||
{{/if}}
|
||||
{{#if backend.description}}
|
||||
<div data-test-description class="truncate-first-line">
|
||||
{{backend.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if backend.isSupportedBackend}}
|
||||
<LinkTo
|
||||
@route={{backend.backendLink}}
|
||||
@model={{backend.id}}
|
||||
class="has-text-weight-semibold is-no-underline"
|
||||
data-test-view
|
||||
>
|
||||
View
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/each}}
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{#if (gt this.filteredSecretsEngines.length 4)}}
|
||||
<div class="is-flex-end has-top-margin-s" data-test-secrets-engines-card-show-all>
|
||||
<LinkTo @route="vault.cluster.secrets.backends" class="has-text-weight-semibold is-size-7 is-no-underline">
|
||||
Show all
|
||||
<Icon @name="arrow-right" />
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="has-padding-s has-top-margin-m" data-test-feedback-form>
|
||||
<small>
|
||||
Don't see what you're looking for on this page? Let us know via our
|
||||
<Hds::Link::Inline @icon="external-link" @href="https://hashicorp.sjc1.qualtrics.com/jfe/form/SV_1SNUsZLdWHpfw0e">
|
||||
feedback form
|
||||
</Hds::Link::Inline>
|
||||
.
|
||||
</small>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l">
|
||||
<h3 class="title is-4" data-test-configuration-details-title>Configuration details</h3>
|
||||
|
||||
<Hds::Table @caption="Vault configuration details" class="is-border-spacing-revert">
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
<B.Td>API_ADDR</B.Td>
|
||||
<B.Td data-test-vault-config-details="api_addr">{{or @vaultConfiguration.api_addr "None"}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>Default lease TTL</B.Td>
|
||||
<B.Td data-test-vault-config-details="default_lease_ttl">{{format-duration
|
||||
@vaultConfiguration.default_lease_ttl
|
||||
}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>Max lease TTL</B.Td>
|
||||
<B.Td data-test-vault-config-details="max_lease_ttl">{{format-duration @vaultConfiguration.max_lease_ttl}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>TLS</B.Td>
|
||||
<B.Td data-test-vault-config-details="tls_disable">{{this.tlsDisabled}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>Log format</B.Td>
|
||||
<B.Td data-test-vault-config-details="log_format">{{or @vaultConfiguration.log_format "None"}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>Log level</B.Td>
|
||||
<B.Td data-test-vault-config-details="log_level">{{@vaultConfiguration.log_level}}</B.Td>
|
||||
</B.Tr>
|
||||
<B.Tr>
|
||||
<B.Td>Storage type</B.Td>
|
||||
<B.Td data-test-vault-config-details="type">{{@vaultConfiguration.storage.type}}</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
</Hds::Card::Container>
|
||||
@@ -0,0 +1,11 @@
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-dashboard-version-header>
|
||||
{{this.versionHeader}}
|
||||
{{#if @version.isEnterprise}}
|
||||
<Hds::Badge @text={{this.namespaceDisplay}} @icon="org" data-test-badge-namespace />
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<hr class="has-top-margin-xxs has-bottom-margin-l has-background-gray-200" />
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="is-flex-v-centered is-flex-1">
|
||||
<div class="is-flex-v-centered is-flex-grow-1">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-narrow has-text-centered has-text-grey-dark has-current-color-fill">
|
||||
<LogoEdition />
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
/>
|
||||
{{#each this.targets as |target|}}
|
||||
<div class="is-flex-center has-border-top-light" data-test-mlef-target={{target.label}}>
|
||||
<InfoTableRow @label={{target.label}} class="is-flex-1 has-no-shadow">
|
||||
<InfoTableRow @label={{target.label}} class="is-flex-grow-1 has-no-shadow">
|
||||
{{#if target.value.id}}
|
||||
{{target.value.name}}
|
||||
<span class="tag has-left-margin-s">{{target.value.id}}</span>
|
||||
@@ -85,7 +85,7 @@
|
||||
@onChange={{this.onTargetSelect}}
|
||||
data-test-mlef-select="target-type"
|
||||
/>
|
||||
<div class="has-left-margin-s is-flex-1">
|
||||
<div class="has-left-margin-s is-flex-grow-1">
|
||||
{{#if (eq this.selectedTargetType "accessor")}}
|
||||
<MountAccessorSelect
|
||||
@value={{this.selectedTargetValue}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-grow-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<div class="column is-4-desktop is-6-tablet">
|
||||
<div class="has-text-grey has-bottom-margin-m has-current-color-fill">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{{#if @hasAltContent}}
|
||||
{{yield (hash altContent=(component "splash-page/splash-content"))}}
|
||||
{{else}}
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-grow-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<div class="column is-4-desktop is-6-tablet">
|
||||
<div class="splash-page-header">
|
||||
|
||||
@@ -143,14 +143,14 @@
|
||||
</div>
|
||||
<div class="column is-two-thirds is-flex-column">
|
||||
{{#if (or (and @verification (eq @verification "HMAC")) @hmac)}}
|
||||
<div class="field is-flex-column is-flex-1">
|
||||
<div class="control is-flex-column is-flex-1">
|
||||
<div class="field is-flex-column is-flex-grow-1">
|
||||
<div class="control is-flex-column is-flex-grow-1">
|
||||
<JsonEditor @title="HMAC" @value={{@hmac}} @valueUpdated={{action (mut @hmac)}} @mode="ruby" />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="field is-flex-column is-flex-1">
|
||||
<div class="control is-flex-column is-flex-1">
|
||||
<div class="field is-flex-column is-flex-grow-1">
|
||||
<div class="control is-flex-column is-flex-grow-1">
|
||||
<JsonEditor
|
||||
@title="Signature"
|
||||
@value={{@signature}}
|
||||
|
||||
49
ui/app/templates/vault/cluster/dashboard.hbs
Normal file
49
ui/app/templates/vault/cluster/dashboard.hbs
Normal file
@@ -0,0 +1,49 @@
|
||||
<Dashboard::VaultVersionTitle @version={{@model.version}} />
|
||||
|
||||
<div class="has-bottom-margin-xl">
|
||||
<div class="is-flex-row has-gap-l">
|
||||
{{#if (and @model.version.isEnterprise (or @model.license @model.isRootNamespace))}}
|
||||
|
||||
<div class="is-flex-column is-flex-1 has-gap-l">
|
||||
{{#if @model.license}}
|
||||
<Dashboard::ClientCountCard @license={{@model.license}} />
|
||||
{{/if}}
|
||||
{{#if @model.isRootNamespace}}
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{@model.replication}}
|
||||
@version={{@model.version}}
|
||||
@refresh={{this.refreshModel}}
|
||||
@updatedAt={{this.replicationUpdatedAt}}
|
||||
/>
|
||||
{{/if}}
|
||||
<Dashboard::SecretsEnginesCard @secretsEngines={{@model.secretsEngines}} />
|
||||
</div>
|
||||
|
||||
<div class="is-flex-column is-flex-1 has-gap-l">
|
||||
<Dashboard::QuickActionsCard @secretsEngines={{@model.secretsEngines}} />
|
||||
{{#if @model.vaultConfiguration}}
|
||||
<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{@model.vaultConfiguration}} />
|
||||
{{/if}}
|
||||
<div>
|
||||
<Dashboard::LearnMoreCard @isEnterprise={{@model.version.isEnterprise}} />
|
||||
<Dashboard::SurveyLinkText />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<div class="is-flex-column is-flex-1 has-gap-l">
|
||||
<Dashboard::SecretsEnginesCard @secretsEngines={{@model.secretsEngines}} />
|
||||
<div>
|
||||
<Dashboard::LearnMoreCard @isEnterprise={{@model.version.isEnterprise}} />
|
||||
<Dashboard::SurveyLinkText />
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex-column is-flex-1 has-gap-l">
|
||||
<Dashboard::QuickActionsCard @secretsEngines={{@model.secretsEngines}} />
|
||||
{{#if @model.vaultConfiguration}}
|
||||
<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{@model.vaultConfiguration}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-grow-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<div class="column is-4-desktop is-6-tablet">
|
||||
{{#if this.model.error}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-grow-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<div class="column is-4-desktop is-6-tablet">
|
||||
{{#if this.model.error}}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
as |Item|
|
||||
>
|
||||
<Item.content>
|
||||
<div class="columns is-flex-1">
|
||||
<div class="columns is-flex-grow-1">
|
||||
<div>
|
||||
<Icon @name="history" class="has-text-grey" />
|
||||
Version
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
~}}
|
||||
|
||||
{{#if this.showLicenseError}}
|
||||
<div class="section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||
<div class="section is-flex-v-centered-tablet is-flex-grow-1 is-fullwidth">
|
||||
<div class="columns is-centered is-gapless is-fullwidth">
|
||||
<EmptyState
|
||||
class="empty-state-transparent"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="is-flex-1 is-flex-v-centered">
|
||||
<div class="is-flex-grow-1 is-flex-v-centered">
|
||||
<div class="empty-state-content">
|
||||
<div class="is-flex-v-centered has-bottom-margin-xxl">
|
||||
<div class="brand-icon-large">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<span class="has-left-margin-s">
|
||||
<Icon @name="certificate" @size="24" data-test-certificate-icon />
|
||||
</span>
|
||||
<div class="has-left-margin-m is-min-width-0 is-flex-1">
|
||||
<div class="has-left-margin-m is-min-width-0 is-flex-grow-1">
|
||||
<p class="has-text-weight-bold" data-test-certificate-label>
|
||||
{{this.format}}
|
||||
</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="is-flex-v-centered is-flex-1 loader-inner-page" data-test-layout-loading>
|
||||
<div class="is-flex-v-centered is-flex-grow-1 loader-inner-page" data-test-layout-loading>
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-narrow has-text-centered has-text-grey-dark has-current-color-fill">
|
||||
<div class="level is-mobile">
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
data-test-list-item-link
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left is-flex-1" data-test-list-item-content>
|
||||
<div class="level-left is-flex-grow-1" data-test-list-item-content>
|
||||
<LinkTo
|
||||
@route={{this.link.route}}
|
||||
@models={{this.link.models}}
|
||||
class="has-text-weight-semibold has-text-black is-display-flex is-flex-1 is-no-underline"
|
||||
class="has-text-weight-semibold has-text-black is-display-flex is-flex-grow-1 is-no-underline"
|
||||
>
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
</LinkTo>
|
||||
@@ -35,7 +35,7 @@
|
||||
{{else}}
|
||||
<div class="list-item-row">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left is-flex-1 has-text-weight-semibold" data-test-list-item>
|
||||
<div class="level-left is-flex-grow-1 has-text-weight-semibold" data-test-list-item>
|
||||
{{yield (hash content=(component "list-item/content"))}}
|
||||
</div>
|
||||
<div class="level-right">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{#if this.isMenu}}
|
||||
{{! this is the status menu }}
|
||||
<div class="level is-mobile">
|
||||
<div class="is-flex-1">
|
||||
<div class="is-flex-grow-1">
|
||||
{{#if this.replicationUnsupported}}
|
||||
Unsupported
|
||||
{{else if this.replicationEnabled}}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<OverviewCard @cardTitle="Generate credentials" @subText="Quickly generate credentials by typing the role name.">
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
class="is-flex-1"
|
||||
class="is-flex-grow-1"
|
||||
@placeholder="Type to find a role..."
|
||||
@disallowNewItems={{true}}
|
||||
@options={{this.roleOptions}}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<OverviewCard @cardTitle="Issue certificate" @subText="Begin issuing a certificate by choosing a role.">
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
class="is-flex-1"
|
||||
class="is-flex-grow-1"
|
||||
@selectLimit="1"
|
||||
@models={{array "pki/role"}}
|
||||
@backend={{@engine.id}}
|
||||
@@ -50,7 +50,7 @@
|
||||
<OverviewCard @cardTitle="View certificate" @subText="Quickly view a certificate by typing its serial number.">
|
||||
<div class="has-top-margin-m {{unless this.certificateValue 'is-flex'}}">
|
||||
<SearchSelect
|
||||
class="is-flex-1"
|
||||
class="is-flex-grow-1"
|
||||
@selectLimit="1"
|
||||
@models={{array "pki/certificate/base"}}
|
||||
@backend={{@engine.id}}
|
||||
@@ -75,7 +75,7 @@
|
||||
<OverviewCard @cardTitle="View issuer" @subText="Choose or type an issuer name or ID to view its details">
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
class="is-flex-1"
|
||||
class="is-flex-grow-1"
|
||||
@selectLimit="1"
|
||||
@models={{array "pki/issuer"}}
|
||||
@backend={{@engine.id}}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
{{#if this.importedResponse}}
|
||||
<div class="is-flex-start has-top-margin-xs">
|
||||
<div class="is-flex-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<div class="is-flex-grow-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<h2>
|
||||
Imported Issuer
|
||||
</h2>
|
||||
</div>
|
||||
<div class="is-flex-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<div class="is-flex-grow-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<h2>
|
||||
Imported Key
|
||||
</h2>
|
||||
@@ -23,7 +23,7 @@
|
||||
data-test-import-pair={{concat imported.issuer "_" imported.key}}
|
||||
>
|
||||
<div class="is-flex-start">
|
||||
<div class="is-flex-1 basis-0 has-bottom-margin-xs" data-test-imported-issuer>
|
||||
<div class="is-flex-grow-1 basis-0 has-bottom-margin-xs" data-test-imported-issuer>
|
||||
{{#if imported.issuer}}
|
||||
<LinkTo @route="issuers.issuer.details" @model={{imported.issuer}}>
|
||||
{{imported.issuer}}
|
||||
@@ -32,7 +32,7 @@
|
||||
None
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="is-flex-1 basis-0 has-bottom-margin-xs" data-test-imported-key>
|
||||
<div class="is-flex-grow-1 basis-0 has-bottom-margin-xs" data-test-imported-key>
|
||||
{{#if imported.key}}
|
||||
<LinkTo @route="keys.key.details" @model={{imported.key}}>
|
||||
{{imported.key}}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</p>
|
||||
<div class="is-flex-start">
|
||||
{{#each this.inputFields as |column|}}
|
||||
<div class="is-flex-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<div class="is-flex-grow-1 basis-0 has-text-grey has-bottom-margin-xs">
|
||||
<h2 data-test-info-table-label={{column.key}}>
|
||||
{{column.label}}
|
||||
{{#if column.helpText}}
|
||||
@@ -40,7 +40,7 @@
|
||||
/>
|
||||
{{#each (map-by "key" this.inputFields) as |columnAttr|}}
|
||||
{{#let (get crossSignRow columnAttr) as |data|}}
|
||||
<div data-test-info-table-column={{columnAttr}} class="is-flex-1 basis-0 has-bottom-margin-xs">
|
||||
<div data-test-info-table-column={{columnAttr}} class="is-flex-grow-1 basis-0 has-bottom-margin-xs">
|
||||
{{#if (eq columnAttr "intermediateMount")}}
|
||||
<LinkTo class="has-text-black has-text-weight-semibold" @route="overview" @model={{data}}>
|
||||
{{data}}
|
||||
|
||||
471
ui/tests/acceptance/dashboard-test.js
Normal file
471
ui/tests/acceptance/dashboard-test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
11
ui/tests/helpers/components/dashboard/quick-actions-card.js
Normal file
11
ui/tests/helpers/components/dashboard/quick-actions-card.js
Normal file
@@ -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;
|
||||
26
ui/tests/helpers/components/dashboard/replication-card.js
Normal file
26
ui/tests/helpers/components/dashboard/replication-card.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
31
ui/tests/helpers/replication.js
Normal file
31
ui/tests/helpers/replication.js
Normal file
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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`<Dashboard::ClientCountCard @license={{this.license}} />`);
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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`<Dashboard::QuickActionsCard @secretsEngines={{this.secretsEngines}} />`);
|
||||
};
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{this.replication}}
|
||||
@version={{this.version}}
|
||||
@updatedAt={{this.updatedAt}}
|
||||
@refresh={{this.refresh}} />
|
||||
`
|
||||
);
|
||||
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`
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{this.replication}}
|
||||
@version={{this.version}}
|
||||
@updatedAt={{this.updatedAt}}
|
||||
@refresh={{this.refresh}} />
|
||||
`
|
||||
);
|
||||
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`
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{this.replication}}
|
||||
@version={{this.version}}
|
||||
@updatedAt={{this.updatedAt}}
|
||||
@refresh={{this.refresh}} />
|
||||
`
|
||||
);
|
||||
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`
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{this.replication}}
|
||||
@version={{this.version}}
|
||||
@updatedAt={{this.updatedAt}}
|
||||
@refresh={{this.refresh}} />
|
||||
`
|
||||
);
|
||||
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`
|
||||
<Dashboard::ReplicationCard
|
||||
@replication={{this.replication}}
|
||||
@version={{this.version}}
|
||||
@updatedAt={{this.updatedAt}}
|
||||
@refresh={{this.refresh}} />
|
||||
`
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
<Dashboard::ReplicationStateText
|
||||
@name={{this.name}}
|
||||
@version={{this.version}}
|
||||
@subText={{this.subText}}
|
||||
@clusterStates={{this.clusterStates}} />
|
||||
`
|
||||
);
|
||||
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`
|
||||
<Dashboard::ReplicationStateText
|
||||
@name={{this.name}}
|
||||
@version={{this.version}}
|
||||
@subText={{this.subText}}
|
||||
@clusterStates={{this.clusterStates}} />
|
||||
`
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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`<Dashboard::SecretsEnginesCard @secretsEngines={{this.secretsEngines}} />`);
|
||||
};
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
11
ui/tests/unit/routes/vault/cluster/dashboard-test.js
Normal file
11
ui/tests/unit/routes/vault/cluster/dashboard-test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user