diff --git a/ui/app/styles/components/selectable-card-container.scss b/ui/app/styles/components/selectable-card-container.scss
index 51c302d6e8..af4f7afee4 100644
--- a/ui/app/styles/components/selectable-card-container.scss
+++ b/ui/app/styles/components/selectable-card-container.scss
@@ -29,3 +29,12 @@
padding: $spacing-l 0 14px $spacing-l; // modify bottom spacing to better align with other cards
}
}
+
+.selectable-card-container.has-grid.has-two-col-grid {
+ grid-template-columns: 2fr 2fr;
+ grid-template-rows: none;
+}
+.selectable-card-container.has-grid.has-three-col-grid {
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-rows: none;
+}
diff --git a/ui/app/templates/components/input-search.hbs b/ui/lib/core/addon/components/input-search.hbs
similarity index 100%
rename from ui/app/templates/components/input-search.hbs
rename to ui/lib/core/addon/components/input-search.hbs
diff --git a/ui/app/components/input-search.js b/ui/lib/core/addon/components/input-search.js
similarity index 100%
rename from ui/app/components/input-search.js
rename to ui/lib/core/addon/components/input-search.js
diff --git a/ui/lib/core/addon/components/overview-card.hbs b/ui/lib/core/addon/components/overview-card.hbs
new file mode 100644
index 0000000000..157a61a779
--- /dev/null
+++ b/ui/lib/core/addon/components/overview-card.hbs
@@ -0,0 +1,18 @@
+
+
+
{{@cardTitle}}
+ {{#if @actionText}}
+
+ {{@actionText}}
+
+
+ {{/if}}
+
+
{{@subText}}
+ {{yield}}
+
\ No newline at end of file
diff --git a/ui/lib/core/app/components/input-search.js b/ui/lib/core/app/components/input-search.js
new file mode 100644
index 0000000000..a8e61ffbda
--- /dev/null
+++ b/ui/lib/core/app/components/input-search.js
@@ -0,0 +1 @@
+export { default } from 'core/components/input-search';
diff --git a/ui/lib/core/app/components/overview-card.js b/ui/lib/core/app/components/overview-card.js
new file mode 100644
index 0000000000..922f634809
--- /dev/null
+++ b/ui/lib/core/app/components/overview-card.js
@@ -0,0 +1 @@
+export { default } from 'core/components/overview-card';
diff --git a/ui/lib/pki/addon/components/page/pki-overview.hbs b/ui/lib/pki/addon/components/page/pki-overview.hbs
new file mode 100644
index 0000000000..caf511b1ac
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-overview.hbs
@@ -0,0 +1,82 @@
+
+
+ {{format-number (if (eq @issuers 404) 0 @issuers.length)}}
+
+ {{#if (not (eq @roles 403))}}
+
+ {{format-number (if (eq @roles 404) 0 @roles.length)}}
+
+ {{/if}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-overview.ts b/ui/lib/pki/addon/components/page/pki-overview.ts
new file mode 100644
index 0000000000..2ee9451289
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-overview.ts
@@ -0,0 +1,55 @@
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import Component from '@glimmer/component';
+// TYPES
+import Store from '@ember-data/store';
+import RouterService from '@ember/routing/router-service';
+import PkiIssuerModel from 'vault/models/pki/issuer';
+import PkiRoleModel from 'vault/models/pki/role';
+
+interface Args {
+ issuers: PkiIssuerModel | number;
+ roles: PkiRoleModel | number;
+ engine: string;
+}
+
+export default class PkiOverview extends Component {
+ @service declare readonly router: RouterService;
+ @service declare readonly store: Store;
+
+ @tracked rolesValue = '';
+ @tracked certificateValue = '';
+
+ @action
+ transitionToViewCertificates(event: Event) {
+ event.preventDefault();
+ this.router.transitionTo(
+ 'vault.cluster.secrets.backend.pki.certificates.certificate.details',
+ this.certificateValue
+ );
+ }
+ @action
+ transitionToIssueCertificates(event: Event) {
+ event.preventDefault();
+ this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.role.generate', this.rolesValue);
+ }
+
+ @action
+ handleRolesInput(roles: string) {
+ if (Array.isArray(roles)) {
+ this.rolesValue = roles[0];
+ } else {
+ this.rolesValue = roles;
+ }
+ }
+
+ @action
+ handleCertificateInput(certificate: string) {
+ if (Array.isArray(certificate)) {
+ this.certificateValue = certificate[0];
+ } else {
+ this.certificateValue = certificate;
+ }
+ }
+}
diff --git a/ui/lib/pki/addon/routes/overview.js b/ui/lib/pki/addon/routes/overview.js
index da7bb93c30..2a3457fc75 100644
--- a/ui/lib/pki/addon/routes/overview.js
+++ b/ui/lib/pki/addon/routes/overview.js
@@ -15,30 +15,46 @@ export default class PkiOverviewRoute extends Route {
// When the engine is configured, it creates a default issuer.
// If the issuers list is empty, we know it hasn't been configured
const endpoint = `${this.win.origin}/v1/${this.secretMountPath.currentPath}/issuers?list=true`;
+
return this.auth
.ajax(endpoint, 'GET', {})
.then(() => true)
.catch(() => false);
}
+ async fetchEngine() {
+ const model = await this.store.query('secret-engine', {
+ path: this.secretMountPath.currentPath,
+ });
+ return model.get('firstObject');
+ }
+
+ async fetchAllRoles() {
+ try {
+ return await this.store.query('pki/role', { backend: this.secretMountPath.currentPath });
+ } catch (e) {
+ return e.httpStatus;
+ }
+ }
+
+ async fetchAllIssuers() {
+ try {
+ return await this.store.query('pki/issuer', { backend: this.secretMountPath.currentPath });
+ } catch (e) {
+ return e.httpStatus;
+ }
+ }
+
async model() {
return hash({
hasConfig: this.hasConfig(),
- engine: this.store
- .query('secret-engine', {
- path: this.secretMountPath.currentPath,
- })
- .then((model) => {
- if (model) {
- return model.get('firstObject');
- }
- }),
+ engine: this.fetchEngine(),
+ roles: this.fetchAllRoles(),
+ issuers: this.fetchAllIssuers(),
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
- const backend = this.secretMountPath.currentPath || 'pki';
- controller.breadcrumbs = [{ label: 'secrets', route: 'secrets', linkExternal: true }, { label: backend }];
}
}
diff --git a/ui/lib/pki/addon/templates/overview.hbs b/ui/lib/pki/addon/templates/overview.hbs
index 7938b39154..f799f788ae 100644
--- a/ui/lib/pki/addon/templates/overview.hbs
+++ b/ui/lib/pki/addon/templates/overview.hbs
@@ -19,7 +19,7 @@
{{#if this.model.hasConfig}}
- {{! TODO show overview items }}
+
{{else}}
diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js
index a02fa42f97..2bb0e88129 100644
--- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js
+++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js
@@ -1,42 +1,12 @@
-import { create } from 'ember-cli-page-object';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
-import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import { click, currentURL, fillIn, find, isSettled, visit } from '@ember/test-helpers';
import { SELECTORS } from 'vault/tests/helpers/pki/workflow';
import { adminPolicy, readerPolicy, updatePolicy } from 'vault/tests/helpers/policy-generator/pki';
-
-const consoleComponent = create(consoleClass);
-
-const tokenWithPolicy = async function (name, policy) {
- await consoleComponent.runCommands([
- `write sys/policies/acl/${name} policy=${btoa(policy)}`,
- `write -field=client_token auth/token/create policies=${name}`,
- ]);
- return consoleComponent.lastLogOutput;
-};
-
-const runCommands = async function (commands) {
- try {
- await consoleComponent.runCommands(commands);
- const res = consoleComponent.lastLogOutput;
- if (res.includes('Error')) {
- throw new Error(res);
- }
- return res;
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error(
- `The following occurred when trying to run the command(s):\n ${commands.join('\n')} \n\n ${
- consoleComponent.lastLogOutput
- }`
- );
- throw error;
- }
-};
+import { tokenWithPolicy, runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
/**
* This test module should test the PKI workflow, including:
diff --git a/ui/tests/acceptance/pki/pki-overview-test.js b/ui/tests/acceptance/pki/pki-overview-test.js
new file mode 100644
index 0000000000..150d9fd1e7
--- /dev/null
+++ b/ui/tests/acceptance/pki/pki-overview-test.js
@@ -0,0 +1,112 @@
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import authPage from 'vault/tests/pages/auth';
+import logout from 'vault/tests/pages/logout';
+import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
+import { click, currentURL, currentRouteName, visit } from '@ember/test-helpers';
+import { SELECTORS } from 'vault/tests/helpers/pki/overview';
+import { tokenWithPolicy, runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
+
+module('Acceptance | pki overview', function (hooks) {
+ setupApplicationTest(hooks);
+
+ hooks.beforeEach(async function () {
+ await authPage.login();
+ // Setup PKI engine
+ const mountPath = `pki`;
+ await enablePage.enable('pki', mountPath);
+ this.mountPath = mountPath;
+ await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]);
+ const pki_admin_policy = `
+ path "${this.mountPath}/*" {
+ capabilities = ["create", "read", "update", "delete", "list"]
+ },
+ `;
+ const pki_issuers_list_policy = `
+ path "${this.mountPath}/issuers" {
+ capabilities = ["list"]
+ },
+ `;
+ const pki_roles_list_policy = `
+ path "${this.mountPath}/roles" {
+ capabilities = ["list"]
+ },
+ `;
+
+ this.pkiRolesList = await tokenWithPolicy('pki-roles-list', pki_roles_list_policy);
+ this.pkiIssuersList = await tokenWithPolicy('pki-issuers-list', pki_issuers_list_policy);
+ this.pkiAdminToken = await tokenWithPolicy('pki-admin', pki_admin_policy);
+ await logout.visit();
+ });
+
+ hooks.afterEach(async function () {
+ await logout.visit();
+ await authPage.login();
+ // Cleanup engine
+ await runCommands([`delete sys/mounts/${this.mountPath}`]);
+ await logout.visit();
+ });
+
+ test('navigates to view issuers when link is clicked on issuer card', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ assert.dom(SELECTORS.issuersCardTitle).hasText('Issuers');
+ assert.dom(SELECTORS.issuersCardOverviewNum).hasText('1');
+ await click(SELECTORS.issuersCardLink);
+ assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ });
+
+ test('navigates to view roles when link is clicked on roles card', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ assert.dom(SELECTORS.rolesCardTitle).hasText('Roles');
+ assert.dom(SELECTORS.rolesCardOverviewNum).hasText('0');
+ await click(SELECTORS.rolesCardLink);
+ assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
+ await runCommands([
+ `write ${this.mountPath}/roles/some-role \
+ issuer_ref="default" \
+ allowed_domains="example.com" \
+ allow_subdomains=true \
+ max_ttl="720h"`,
+ ]);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ assert.dom(SELECTORS.rolesCardOverviewNum).hasText('1');
+ });
+
+ test('hides roles card if user does not have permissions', async function (assert) {
+ await authPage.login(this.pkiIssuersList);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ assert.dom(SELECTORS.rolesCardTitle).doesNotExist('Roles card does not exist');
+ assert.dom(SELECTORS.issuersCardTitle).exists('Issuers card exists');
+ });
+
+ test('navigates to generate certificate page for Issue Certificates card', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await runCommands([
+ `write ${this.mountPath}/roles/some-role \
+ issuer_ref="default" \
+ allowed_domains="example.com" \
+ allow_subdomains=true \
+ max_ttl="720h"`,
+ ]);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ await click(SELECTORS.issueCertificatePowerSearch);
+ await click(SELECTORS.firstPowerSelectOption);
+ await click(SELECTORS.issueCertificateButton);
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.roles.role.generate');
+ });
+
+ test('navigates to certificate details page for View Certificates card', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
+ await click(SELECTORS.viewCertificatePowerSearch);
+ await click(SELECTORS.firstPowerSelectOption);
+ await click(SELECTORS.viewCertificateButton);
+ assert.strictEqual(
+ currentRouteName(),
+ 'vault.cluster.secrets.backend.pki.certificates.certificate.details'
+ );
+ });
+});
diff --git a/ui/tests/helpers/pki/overview.js b/ui/tests/helpers/pki/overview.js
new file mode 100644
index 0000000000..10fcf8973e
--- /dev/null
+++ b/ui/tests/helpers/pki/overview.js
@@ -0,0 +1,19 @@
+export const SELECTORS = {
+ issuersCardTitle: '[data-test-selectable-card-container="Issuers"] h3',
+ issuersCardSubtitle: '[data-test-selectable-card-container="Issuers"] p',
+ issuersCardLink: '[data-test-selectable-card-container="Issuers"] a',
+ issuersCardOverviewNum: '[data-test-selectable-card-container="Issuers"] .title-number',
+ rolesCardTitle: '[data-test-selectable-card-container="Roles"] h3',
+ rolesCardSubtitle: '[data-test-selectable-card-container="Roles"] p',
+ rolesCardLink: '[data-test-selectable-card-container="Roles"] a',
+ rolesCardOverviewNum: '[data-test-selectable-card-container="Roles"] .title-number',
+ issueCertificate: '[data-test-selectable-card-container="Issue certificate"] h3',
+ issueCertificateInput: '[data-test-issue-certificate-input]',
+ issueCertificatePowerSearch: '[data-test-issue-certificate-input] span',
+ issueCertificateButton: '[data-test-issue-certificate-button]',
+ viewCertificate: '[data-test-selectable-card-container="View certificate"] h3',
+ viewCertificateInput: '[data-test-view-certificate-input]',
+ viewCertificatePowerSearch: '[data-test-view-certificate-input] span',
+ viewCertificateButton: '[data-test-view-certificate-button]',
+ firstPowerSelectOption: '[data-option-index="0"]',
+};
diff --git a/ui/tests/helpers/pki/pki-run-commands.js b/ui/tests/helpers/pki/pki-run-commands.js
new file mode 100644
index 0000000000..3dd8cd8d84
--- /dev/null
+++ b/ui/tests/helpers/pki/pki-run-commands.js
@@ -0,0 +1,31 @@
+import consoleClass from 'vault/tests/pages/components/console/ui-panel';
+import { create } from 'ember-cli-page-object';
+
+const consoleComponent = create(consoleClass);
+
+export const tokenWithPolicy = async function (name, policy) {
+ await consoleComponent.runCommands([
+ `write sys/policies/acl/${name} policy=${btoa(policy)}`,
+ `write -field=client_token auth/token/create policies=${name}`,
+ ]);
+ return consoleComponent.lastLogOutput;
+};
+
+export const runCommands = async function (commands) {
+ try {
+ await consoleComponent.runCommands(commands);
+ const res = consoleComponent.lastLogOutput;
+ if (res.includes('Error')) {
+ throw new Error(res);
+ }
+ return res;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(
+ `The following occurred when trying to run the command(s):\n ${commands.join('\n')} \n\n ${
+ consoleComponent.lastLogOutput
+ }`
+ );
+ throw error;
+ }
+};
diff --git a/ui/tests/integration/components/overview-card-test.js b/ui/tests/integration/components/overview-card-test.js
new file mode 100644
index 0000000000..ef4eb4b8f2
--- /dev/null
+++ b/ui/tests/integration/components/overview-card-test.js
@@ -0,0 +1,34 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+const CARD_TITLE = 'Card title';
+const ACTION_TEXT = 'View card';
+const SUBTEXT = 'This is subtext for card';
+
+module('Integration | Component overview-card', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.set('cardTitle', CARD_TITLE);
+ this.set('actionText', ACTION_TEXT);
+ this.set('subText', SUBTEXT);
+ });
+
+ test('it returns card title, ', async function (assert) {
+ await render(hbs``);
+ const titleText = this.element.querySelector('.title').innerText;
+ assert.strictEqual(titleText, 'Card title');
+ });
+ test('it returns card subtext, ', async function (assert) {
+ await render(hbs``);
+ const titleText = this.element.querySelector('p').innerText;
+ assert.strictEqual(titleText, 'This is subtext for card');
+ });
+ test('it returns card action text', async function (assert) {
+ await render(hbs``);
+ const titleText = this.element.querySelector('a').innerText;
+ assert.strictEqual(titleText, 'View card ');
+ });
+});
diff --git a/ui/tests/integration/components/pki/page/pki-overview-test.js b/ui/tests/integration/components/pki/page/pki-overview-test.js
new file mode 100644
index 0000000000..55cfe852c4
--- /dev/null
+++ b/ui/tests/integration/components/pki/page/pki-overview-test.js
@@ -0,0 +1,77 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupEngine } from 'ember-engines/test-support';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { SELECTORS } from 'vault/tests/helpers/pki/overview';
+
+module('Integration | Component | Page::PkiOverview', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'pki');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.secretMountPath = this.owner.lookup('service:secret-mount-path');
+ this.secretMountPath.currentPath = 'pki-test';
+
+ this.store.createRecord('pki/issuer', { issuerId: 'abcd-efgh' });
+ this.store.createRecord('pki/issuer', { issuerId: 'ijkl-mnop' });
+ this.store.createRecord('pki/role', { name: 'role-0' });
+ this.store.createRecord('pki/role', { name: 'role-1' });
+ this.store.createRecord('pki/role', { name: 'role-2' });
+ this.store.createRecord('pki/certificate', { serialNumber: '22:2222:22222:2222' });
+ this.store.createRecord('pki/certificate', { serialNumber: '33:3333:33333:3333' });
+
+ this.issuers = this.store.peekAll('pki/issuer');
+ this.roles = this.store.peekAll('pki/role');
+ this.engineId = 'pki';
+ });
+
+ test('shows the correct information on issuer card', async function (assert) {
+ await render(
+ hbs`,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.issuersCardTitle).hasText('Issuers');
+ assert.dom(SELECTORS.issuersCardOverviewNum).hasText('2');
+ assert.dom(SELECTORS.issuersCardLink).hasText('View issuers');
+ });
+
+ test('shows the correct information on roles card', async function (assert) {
+ await render(
+ hbs`,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.rolesCardTitle).hasText('Roles');
+ assert.dom(SELECTORS.rolesCardOverviewNum).hasText('3');
+ assert.dom(SELECTORS.rolesCardLink).hasText('View roles');
+ this.roles = 404;
+ await render(
+ hbs`,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.rolesCardOverviewNum).hasText('0');
+ });
+
+ test('shows the input search fields for View Certificates card', async function (assert) {
+ await render(
+ hbs`,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.issueCertificate).hasText('Issue certificate');
+ assert.dom(SELECTORS.issueCertificateInput).exists();
+ assert.dom(SELECTORS.issueCertificateButton).hasText('Issue');
+ });
+
+ test('shows the input search fields for Issue Certificates card', async function (assert) {
+ await render(
+ hbs`,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.viewCertificate).hasText('View certificate');
+ assert.dom(SELECTORS.viewCertificateInput).exists();
+ assert.dom(SELECTORS.viewCertificateButton).hasText('View');
+ });
+});