From 82378ae23237bf583c285b25768f3c6ad9fc6364 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Thu, 21 Sep 2023 18:32:01 -0500 Subject: [PATCH] UI: add pagination to new PKI (#23193) --- changelog/23193.txt | 3 + ui/app/services/store.js | 5 +- .../page/pki-configuration-details.ts | 2 +- .../addon/components/page/pki-issuer-list.hbs | 170 +++++++++++------- .../addon/components/page/pki-issuer-list.ts | 29 +++ .../addon/components/page/pki-key-list.hbs | 127 +++++++------ .../pki/addon/components/page/pki-key-list.ts | 22 +++ .../addon/components/pki-paginated-list.hbs | 25 +++ .../addon/components/pki-paginated-list.ts | 33 ++++ .../addon/components/pki-role-generate.hbs | 2 +- .../addon/controllers/certificates/index.js | 2 + ui/lib/pki/addon/controllers/issuers/index.js | 8 +- ui/lib/pki/addon/controllers/keys/index.js | 2 + ui/lib/pki/addon/controllers/roles/index.js | 2 + ui/lib/pki/addon/routes/certificates/index.js | 30 +++- ui/lib/pki/addon/routes/issuers/index.js | 19 +- ui/lib/pki/addon/routes/keys/index.js | 36 +++- ui/lib/pki/addon/routes/roles/index.js | 30 +++- .../addon/templates/certificates/index.hbs | 35 ++-- ui/lib/pki/addon/templates/issuers/index.hbs | 48 +---- ui/lib/pki/addon/templates/keys/index.hbs | 26 +-- ui/lib/pki/addon/templates/roles/index.hbs | 36 ++-- ui/tests/acceptance/dashboard-test.js | 17 +- .../acceptance/pki/pki-configuration-test.js | 34 +++- .../pki/pki-engine-workflow-test.js | 2 +- ui/tests/helpers/pki.js | 6 + .../components/pki-paginated-list-test.js | 158 ++++++++++++++++ .../pki/page/pki-issuer-list-test.js | 10 +- .../components/pki/page/pki-key-list-test.js | 13 +- 29 files changed, 657 insertions(+), 275 deletions(-) create mode 100644 changelog/23193.txt create mode 100644 ui/lib/pki/addon/components/page/pki-issuer-list.ts create mode 100644 ui/lib/pki/addon/components/page/pki-key-list.ts create mode 100644 ui/lib/pki/addon/components/pki-paginated-list.hbs create mode 100644 ui/lib/pki/addon/components/pki-paginated-list.ts create mode 100644 ui/tests/integration/components/pki-paginated-list-test.js diff --git a/changelog/23193.txt b/changelog/23193.txt new file mode 100644 index 0000000000..b895907ec6 --- /dev/null +++ b/changelog/23193.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add pagination to PKI roles, keys, issuers, and certificates list pages +``` diff --git a/ui/app/services/store.js b/ui/app/services/store.js index d2f45ab399..79fe55e599 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -66,9 +66,12 @@ export default class StoreService extends Store { // pageFilter: a string that will be used to do a fuzzy match against the // results, this is done pre-pagination lazyPaginatedQuery(modelType, query, adapterOptions) { + const skipCache = query.skipCache; + // We don't want skipCache to be part of the actual query key, so remove it + delete query.skipCache; const adapter = this.adapterFor(modelType); const modelName = normalizeModelName(modelType); - const dataCache = this.getDataset(modelName, query); + const dataCache = skipCache ? this.clearDataset(modelName) : this.getDataset(modelName, query); const responsePath = query.responsePath; assert('responsePath is required', responsePath); assert('page is required', typeof query.page === 'number'); diff --git a/ui/lib/pki/addon/components/page/pki-configuration-details.ts b/ui/lib/pki/addon/components/page/pki-configuration-details.ts index 5d068184a6..cf7f08f77d 100644 --- a/ui/lib/pki/addon/components/page/pki-configuration-details.ts +++ b/ui/lib/pki/addon/components/page/pki-configuration-details.ts @@ -10,7 +10,7 @@ import { tracked } from '@glimmer/tracking'; import errorMessage from 'vault/utils/error-message'; import type RouterService from '@ember/routing/router-service'; import type FlashMessageService from 'vault/services/flash-messages'; -import type Store from '@ember-data/store'; +import type Store from 'vault/services/store'; import type VersionService from 'vault/services/version'; interface Args { diff --git a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs index 4c94131f21..6942dacc1c 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs @@ -3,68 +3,114 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -{{#each @issuers as |pkiIssuer idx|}} - -
-
-
- - - {{pkiIssuer.issuerRef}} - {{#if pkiIssuer.issuerName}} - {{pkiIssuer.id}} - {{/if}} - -
- {{#if pkiIssuer.isDefault}} - default issuer - {{/if}} - {{#if (not-eq pkiIssuer.isRoot undefined)}} - {{if - pkiIssuer.isRoot - "root" - "intermediate" - }} - {{/if}} - {{#if pkiIssuer.serialNumber}} - - - Serial number - - {{pkiIssuer.serialNumber}} + + <:actions> + + Import + + + + Generate + + + + + + + + <:list as |issuers|> + {{#each issuers as |pkiIssuer idx|}} + +
+
+
+ + + {{pkiIssuer.issuerRef}} + {{#if pkiIssuer.issuerName}} + {{pkiIssuer.id}} + {{/if}} - {{/if}} - {{#if pkiIssuer.parsedCertificate.common_name}} - - - Common name - - {{pkiIssuer.parsedCertificate.common_name}} - - {{/if}} +
+ {{#if pkiIssuer.isDefault}} + default issuer + {{/if}} + {{#if (not-eq pkiIssuer.isRoot undefined)}} + {{if + pkiIssuer.isRoot + "root" + "intermediate" + }} + {{/if}} + {{#if pkiIssuer.serialNumber}} + + + Serial number + + {{pkiIssuer.serialNumber}} + + {{/if}} + {{#if pkiIssuer.parsedCertificate.common_name}} + + + Common name + + {{pkiIssuer.parsedCertificate.common_name}} + + {{/if}} +
+
+
+
+
+ + + +
-
-
-
- - - -
-
-
- -{{/each}} \ No newline at end of file + + {{/each}} + + <:empty> + + + Configure PKI + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-issuer-list.ts b/ui/lib/pki/addon/components/page/pki-issuer-list.ts new file mode 100644 index 0000000000..00f38e4197 --- /dev/null +++ b/ui/lib/pki/addon/components/page/pki-issuer-list.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { action } from '@ember/object'; +import { next } from '@ember/runloop'; +import Component from '@glimmer/component'; +import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; +import type PkiIssuerModel from 'vault/models/pki/issuer'; + +interface BasicDropdown { + actions: { + close: CallableFunction; + }; +} +interface Args { + issuers: PkiIssuerModel[]; + mountPoint: string; +} + +export default class PkiIssuerList extends Component { + notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG; + + // To prevent production build bug of passing D.actions to on "click": https://github.com/hashicorp/vault/pull/16983 + @action onLinkClick(D: BasicDropdown) { + next(() => D.actions.close()); + } +} diff --git a/ui/lib/pki/addon/components/page/pki-key-list.hbs b/ui/lib/pki/addon/components/page/pki-key-list.hbs index a5c6b58344..a650a2025a 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-list.hbs @@ -3,8 +3,8 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - - + + <:actions> {{#if @canImportKey}} Import @@ -15,62 +15,73 @@ Generate {{/if}} - - -

Below is information about the private keys used by the issuers to sign certificates. While - certificates represent a public assertion of an identity, private keys represent the private part of that identity, a - secret used to prove who they are and who they trust.

- -{{#if @keyModels.length}} - {{#each @keyModels as |pkiKey|}} - -
-
-
- - - {{or pkiKey.keyName pkiKey.id}} - -
- {{#if pkiKey.keyName}} - {{pkiKey.id}} - {{/if}} + + <:description> +

Below is information about the private keys used by the issuers to sign certificates. While + certificates represent a public assertion of an identity, private keys represent the private part of that identity, a + secret used to prove who they are and who they trust.

+ + <:list as |keys|> + {{#each keys as |pkiKey|}} + +
+
+
+ + + {{or pkiKey.keyName pkiKey.id}} + +
+ {{#if pkiKey.keyName}} + {{pkiKey.id}} + {{/if}} +
+
+
+
+
+ + +
-
-
- - - -
-
-
- - {{/each}} -{{else}} - -{{/if}} \ No newline at end of file + + {{/each}} + + + <:empty> + + + + <:configure> + + + Configure PKI + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-key-list.ts b/ui/lib/pki/addon/components/page/pki-key-list.ts new file mode 100644 index 0000000000..a6ce292f4d --- /dev/null +++ b/ui/lib/pki/addon/components/page/pki-key-list.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; +import type PkiKeyModel from 'vault/models/pki/key'; + +interface Args { + keyModels: PkiKeyModel[]; + mountPoint: string; + canImportKey: boolean; + canGenerateKey: boolean; + canRead: boolean; + canEdit: boolean; + hasConfig: boolean; +} + +export default class PkiKeyList extends Component { + notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG; +} diff --git a/ui/lib/pki/addon/components/pki-paginated-list.hbs b/ui/lib/pki/addon/components/pki-paginated-list.hbs new file mode 100644 index 0000000000..93e66c7e13 --- /dev/null +++ b/ui/lib/pki/addon/components/pki-paginated-list.hbs @@ -0,0 +1,25 @@ + + + {{yield to="actions"}} + + + +{{#if this.hasConfig}} + {{#if @list.meta.total}} + {{yield to="description"}} + {{yield @list to="list"}} + + {{else}} + {{yield to="empty"}} + {{/if}} +{{else}} + {{yield to="configure"}} +{{/if}} \ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-paginated-list.ts b/ui/lib/pki/addon/components/pki-paginated-list.ts new file mode 100644 index 0000000000..f9eedb041c --- /dev/null +++ b/ui/lib/pki/addon/components/pki-paginated-list.ts @@ -0,0 +1,33 @@ +import Component from '@glimmer/component'; + +/** + * @module AuthForm + * The `PkiPaginatedList` is used to handle a list page layout with lazyPagination response. + * It is specific to PKI so we can make certain assumptions about routing. + * The toolbar has no filtering since users can go directly to an item from the overview page. + * + * @example ```js + * + * <:list as |items|> + * {{#each items as |item}} + *
for each thing
+ * {{/each}} + * + *
+ * ``` + */ + +interface Args { + list: unknown[]; + listRoute: string; + hasConfig?: boolean; +} +export default class PkiPaginatedListComponent extends Component { + get paginationQueryParams() { + return (page: number) => ({ page }); + } + get hasConfig() { + if (typeof this.args.hasConfig === 'boolean') return this.args.hasConfig; + return true; + } +} diff --git a/ui/lib/pki/addon/components/pki-role-generate.hbs b/ui/lib/pki/addon/components/pki-role-generate.hbs index c86bd6af94..ad710f6205 100644 --- a/ui/lib/pki/addon/components/pki-role-generate.hbs +++ b/ui/lib/pki/addon/components/pki-role-generate.hbs @@ -4,7 +4,7 @@ ~}} {{#if @model.serialNumber}} - + {{else}}
diff --git a/ui/lib/pki/addon/controllers/certificates/index.js b/ui/lib/pki/addon/controllers/certificates/index.js index cd6b448b04..3aeb60e007 100644 --- a/ui/lib/pki/addon/controllers/certificates/index.js +++ b/ui/lib/pki/addon/controllers/certificates/index.js @@ -8,6 +8,8 @@ import { getOwner } from '@ember/application'; import { action } from '@ember/object'; export default class PkiCertificatesIndexController extends Controller { + queryParams = ['page']; + get mountPoint() { return getOwner(this).mountPoint; } diff --git a/ui/lib/pki/addon/controllers/issuers/index.js b/ui/lib/pki/addon/controllers/issuers/index.js index a118855597..74c4ee5b25 100644 --- a/ui/lib/pki/addon/controllers/issuers/index.js +++ b/ui/lib/pki/addon/controllers/issuers/index.js @@ -4,16 +4,12 @@ */ import Controller from '@ember/controller'; -import { action } from '@ember/object'; -import { next } from '@ember/runloop'; import { getOwner } from '@ember/application'; export default class PkiIssuerIndexController extends Controller { + queryParams = ['page']; + get mountPoint() { return getOwner(this).mountPoint; } - // To prevent production build bug of passing D.actions to on "click": https://github.com/hashicorp/vault/pull/16983 - @action onLinkClick(D) { - next(() => D.actions.close()); - } } diff --git a/ui/lib/pki/addon/controllers/keys/index.js b/ui/lib/pki/addon/controllers/keys/index.js index 64c51f5ce1..24ff89ae55 100644 --- a/ui/lib/pki/addon/controllers/keys/index.js +++ b/ui/lib/pki/addon/controllers/keys/index.js @@ -7,6 +7,8 @@ import Controller from '@ember/controller'; import { getOwner } from '@ember/application'; export default class PkiKeysIndexController extends Controller { + queryParams = ['page']; + get mountPoint() { return getOwner(this).mountPoint; } diff --git a/ui/lib/pki/addon/controllers/roles/index.js b/ui/lib/pki/addon/controllers/roles/index.js index d54b66fa97..0730673e2f 100644 --- a/ui/lib/pki/addon/controllers/roles/index.js +++ b/ui/lib/pki/addon/controllers/roles/index.js @@ -7,6 +7,8 @@ import Controller from '@ember/controller'; import { getOwner } from '@ember/application'; export default class PkiRolesIndexController extends Controller { + queryParams = ['page']; + get mountPoint() { return getOwner(this).mountPoint; } diff --git a/ui/lib/pki/addon/routes/certificates/index.js b/ui/lib/pki/addon/routes/certificates/index.js index b0121ada00..1fc49cf4c8 100644 --- a/ui/lib/pki/addon/routes/certificates/index.js +++ b/ui/lib/pki/addon/routes/certificates/index.js @@ -14,23 +14,35 @@ export default class PkiCertificatesIndexRoute extends Route { @service store; @service secretMountPath; - async fetchCertificates() { + queryParams = { + page: { + refreshModel: true, + }, + }; + + async fetchCertificates(params) { try { - return await this.store.query('pki/certificate/base', { backend: this.secretMountPath.currentPath }); + const page = Number(params.page) || 1; + return await this.store.lazyPaginatedQuery('pki/certificate/base', { + backend: this.secretMountPath.currentPath, + responsePath: 'data.keys', + page, + skipCache: page === 1, + }); } catch (e) { if (e.httpStatus === 404) { return { parentModel: this.modelFor('certificates') }; - } else { - throw e; } + throw e; } } - model() { + model(params) { return hash({ hasConfig: this.shouldPromptConfig, - certificates: this.fetchCertificates(), + certificates: this.fetchCertificates(params), parentModel: this.modelFor('certificates'), + pageFilter: params.pageFilter, }); } @@ -41,4 +53,10 @@ export default class PkiCertificatesIndexRoute extends Route { if (certificates?.length) controller.notConfiguredMessage = getCliMessage('certificates'); else controller.notConfiguredMessage = getCliMessage(); } + + resetController(controller, isExiting) { + if (isExiting) { + controller.set('page', undefined); + } + } } diff --git a/ui/lib/pki/addon/routes/issuers/index.js b/ui/lib/pki/addon/routes/issuers/index.js index c8321b39c1..13fe236ba6 100644 --- a/ui/lib/pki/addon/routes/issuers/index.js +++ b/ui/lib/pki/addon/routes/issuers/index.js @@ -5,15 +5,21 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; -import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; export default class PkiIssuersListRoute extends Route { @service store; @service secretMountPath; - model() { + model(params) { + const page = Number(params.page) || 1; return this.store - .query('pki/issuer', { backend: this.secretMountPath.currentPath, isListView: true }) + .lazyPaginatedQuery('pki/issuer', { + backend: this.secretMountPath.currentPath, + responsePath: 'data.keys', + page, + skipCache: page === 1, + isListView: true, + }) .then((issuersModel) => { return { issuersModel, parentModel: this.modelFor('issuers') }; }) @@ -33,6 +39,11 @@ export default class PkiIssuersListRoute extends Route { { label: this.secretMountPath.currentPath, route: 'overview' }, { label: 'issuers', route: 'issuers.index' }, ]; - controller.notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG; + } + + resetController(controller, isExiting) { + if (isExiting) { + controller.set('page', undefined); + } } } diff --git a/ui/lib/pki/addon/routes/keys/index.js b/ui/lib/pki/addon/routes/keys/index.js index 2453df20f7..7d4f6e9df1 100644 --- a/ui/lib/pki/addon/routes/keys/index.js +++ b/ui/lib/pki/addon/routes/keys/index.js @@ -14,17 +14,31 @@ export default class PkiKeysIndexRoute extends Route { @service secretMountPath; @service store; - model() { + queryParams = { + page: { + refreshModel: true, + }, + }; + + model(params) { + const page = Number(params.page) || 1; return hash({ hasConfig: this.shouldPromptConfig, parentModel: this.modelFor('keys'), - keyModels: this.store.query('pki/key', { backend: this.secretMountPath.currentPath }).catch((err) => { - if (err.httpStatus === 404) { - return []; - } else { - throw err; - } - }), + keyModels: this.store + .lazyPaginatedQuery('pki/key', { + backend: this.secretMountPath.currentPath, + responsePath: 'data.keys', + page, + skipCache: page === 1, + }) + .catch((err) => { + if (err.httpStatus === 404) { + return []; + } else { + throw err; + } + }), }); } @@ -37,4 +51,10 @@ export default class PkiKeysIndexRoute extends Route { ]; controller.notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG; } + + resetController(controller, isExiting) { + if (isExiting) { + controller.set('page', undefined); + } + } } diff --git a/ui/lib/pki/addon/routes/roles/index.js b/ui/lib/pki/addon/routes/roles/index.js index 9f7a2184f0..a60fdc6c53 100644 --- a/ui/lib/pki/addon/routes/roles/index.js +++ b/ui/lib/pki/addon/routes/roles/index.js @@ -13,23 +13,35 @@ export default class PkiRolesIndexRoute extends Route { @service store; @service secretMountPath; - async fetchRoles() { + queryParams = { + page: { + refreshModel: true, + }, + }; + + async fetchRoles(params) { try { - return await this.store.query('pki/role', { backend: this.secretMountPath.currentPath }); + const page = Number(params.page) || 1; + return await this.store.lazyPaginatedQuery('pki/role', { + backend: this.secretMountPath.currentPath, + responsePath: 'data.keys', + page, + skipCache: page === 1, + }); } catch (e) { if (e.httpStatus === 404) { return { parentModel: this.modelFor('roles') }; - } else { - throw e; } + throw e; } } - model() { + model(params) { return hash({ hasConfig: this.shouldPromptConfig, - roles: this.fetchRoles(), + roles: this.fetchRoles(params), parentModel: this.modelFor('roles'), + pageFilter: params.pageFilter, }); } @@ -40,4 +52,10 @@ export default class PkiRolesIndexRoute extends Route { if (roles?.length) controller.notConfiguredMessage = getCliMessage('roles'); else controller.notConfiguredMessage = getCliMessage(); } + + resetController(controller, isExiting) { + if (isExiting) { + controller.set('page', undefined); + } + } } diff --git a/ui/lib/pki/addon/templates/certificates/index.hbs b/ui/lib/pki/addon/templates/certificates/index.hbs index 58586fcae6..345240ae9c 100644 --- a/ui/lib/pki/addon/templates/certificates/index.hbs +++ b/ui/lib/pki/addon/templates/certificates/index.hbs @@ -5,18 +5,9 @@ -{{outlet}} - - {{#if this.model.certificates.length}} - - {{! TODO add NavigateInput component }} - - {{/if}} - - -{{#if this.model.hasConfig}} - {{#if this.model.certificates.length}} - {{#each this.model.certificates as |pkiCertificate|}} + + <:list as |certs|> + {{#each certs as |pkiCertificate|}} {{/each}} - {{else}} + + <:empty>

When created, certificates will be listed here. Select a role to start generating certificates.

@@ -62,11 +54,12 @@
- {{/if}} -{{else}} - - - Configure PKI - - -{{/if}} \ No newline at end of file + + <:configure> + + + Configure PKI + + + + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/issuers/index.hbs b/ui/lib/pki/addon/templates/issuers/index.hbs index beb01bbf5d..4a4ce9dda0 100644 --- a/ui/lib/pki/addon/templates/issuers/index.hbs +++ b/ui/lib/pki/addon/templates/issuers/index.hbs @@ -5,50 +5,4 @@ - - - - Import - - - - Generate - - - - - - - - - -{{#if this.model.issuersModel.length}} - -{{else}} - - - Configure PKI - - -{{/if}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/keys/index.hbs b/ui/lib/pki/addon/templates/keys/index.hbs index 6776517b23..09db5994a9 100644 --- a/ui/lib/pki/addon/templates/keys/index.hbs +++ b/ui/lib/pki/addon/templates/keys/index.hbs @@ -5,20 +5,12 @@ -{{#if (or this.model.hasConfig this.model.keyModels)}} - -{{else}} - - - - Configure PKI - - -{{/if}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/index.hbs b/ui/lib/pki/addon/templates/roles/index.hbs index a460a28e2f..c1c2f86f09 100644 --- a/ui/lib/pki/addon/templates/roles/index.hbs +++ b/ui/lib/pki/addon/templates/roles/index.hbs @@ -5,17 +5,16 @@ -{{#if this.model.hasConfig}} - - + + <:actions> + {{#if this.model.hasConfig}} Create role - - - - {{#if this.model.roles.length}} - {{#each this.model.roles as |pkiRole|}} + {{/if}} + + <:list as |roles|> + {{#each roles as |pkiRole|}}
@@ -49,7 +48,8 @@
{{/each}} - {{else}} + + <:empty>

When created, roles will be listed here. Create a role to start generating certificates.

@@ -60,12 +60,12 @@
- {{/if}} -{{else}} - - - - Configure PKI - - -{{/if}} \ No newline at end of file + + <:configure> + + + Configure PKI + + + + \ No newline at end of file diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js index 91fbd60f5a..189e9232a1 100644 --- a/ui/tests/acceptance/dashboard-test.js +++ b/ui/tests/acceptance/dashboard-test.js @@ -231,18 +231,21 @@ module('Acceptance | landing page dashboard', function (hooks) { }); test('shows the correct actions and links associated with pki', async function (assert) { - await mountSecrets.enable('pki', 'pki'); + const backend = 'pki-dashboard'; + await mountSecrets.enable('pki', backend); await runCommands([ - `write pki/roles/some-role \ + `write ${backend}/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 runCommands([ + `write ${backend}/root/generate/internal issuer_name="Hashicorp" common_name="Hello"`, + ]); await settled(); await visit('/vault/dashboard'); - await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki'); + await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend); await fillIn(SELECTORS.selectEl, 'Issue certificate'); assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist(); assert.dom(SELECTORS.subtitle('param')).hasText('Role to use'); @@ -254,7 +257,7 @@ module('Acceptance | landing page dashboard', function (hooks) { await visit('/vault/dashboard'); - await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki'); + await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend); await fillIn(SELECTORS.selectEl, 'View certificate'); assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist(); assert.dom(SELECTORS.subtitle('param')).hasText('Certificate serial number'); @@ -268,7 +271,7 @@ module('Acceptance | landing page dashboard', function (hooks) { await visit('/vault/dashboard'); - await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki'); + await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend); await fillIn(SELECTORS.selectEl, 'View issuer'); assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist(); assert.dom(SELECTORS.subtitle('param')).hasText('Issuer'); @@ -278,7 +281,7 @@ module('Acceptance | landing page dashboard', function (hooks) { assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details'); // cleanup engine mount - await consoleComponent.runCommands(deleteEngineCmd('pki')); + await consoleComponent.runCommands(deleteEngineCmd(backend)); }); const newConnection = async (backend, plugin = 'mongodb-database-plugin') => { diff --git a/ui/tests/acceptance/pki/pki-configuration-test.js b/ui/tests/acceptance/pki/pki-configuration-test.js index f7d11cacb6..90af6c1c88 100644 --- a/ui/tests/acceptance/pki/pki-configuration-test.js +++ b/ui/tests/acceptance/pki/pki-configuration-test.js @@ -69,28 +69,50 @@ module('Acceptance | pki configuration test', function (hooks) { await authPage.login(this.pkiAdminToken); await visit(`/vault/secrets/${this.mountPath}/pki/configuration`); await click(SELECTORS.configuration.configureButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/configuration/create`, + 'goes to pki configure page' + ); await click(SELECTORS.configuration.generateRootOption); await fillIn(SELECTORS.configuration.typeField, 'exported'); await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0'); await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0'); await click(SELECTORS.configuration.generateRootSave); await click(SELECTORS.configuration.doneButton); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/overview`, + 'goes to overview page' + ); await click(SELECTORS.configTab); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/configuration`, + 'goes to configuration page' + ); await click(SELECTORS.configuration.issuerLink); assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists(); await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all'); await click(SELECTORS.configuration.deleteAllIssuerButton); await isSettled(); - assert.dom(SELECTORS.configuration.deleteAllIssuerModal).doesNotExist(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`); + assert + .dom(SELECTORS.configuration.deleteAllIssuerModal) + .doesNotExist('delete all issuers modal closes'); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/configuration`, + 'is still on configuration page' + ); await isSettled(); await visit(`/vault/secrets/${this.mountPath}/pki/overview`); await waitUntil(() => currentURL() === `/vault/secrets/${this.mountPath}/pki/overview`); await isSettled(); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/overview`, + 'goes to overview page' + ); assert .dom(SELECTORS.emptyStateMessage) .hasText( diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index 160ab6d19f..17d363923a 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -379,7 +379,7 @@ module('Acceptance | pki workflow', function (hooks) { await visit(`/vault/secrets/${this.mountPath}/pki/overview`); await click(SELECTORS.issuersTab); assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag'); - assert.dom('[data-test-common-name="0"]').exists({ count: 1 }, 'displays cert common name tag'); + assert.dom('[data-test-common-name="0"]').doesNotExist('does not display cert common name tag'); }); test('details view renders correct number of info items', async function (assert) { diff --git a/ui/tests/helpers/pki.js b/ui/tests/helpers/pki.js index 2863d6a07b..441fa6131a 100644 --- a/ui/tests/helpers/pki.js +++ b/ui/tests/helpers/pki.js @@ -15,3 +15,9 @@ export const SELECTORS = { revocationTime: '[data-test-row-value="Revocation time"]', serialNumber: '[data-test-row-value="Serial number"]', }; + +export const STANDARD_META = { + total: 2, + currentPage: 1, + pageSize: 100, +}; diff --git a/ui/tests/integration/components/pki-paginated-list-test.js b/ui/tests/integration/components/pki-paginated-list-test.js new file mode 100644 index 0000000000..89c069ae84 --- /dev/null +++ b/ui/tests/integration/components/pki-paginated-list-test.js @@ -0,0 +1,158 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { setupEngine } from 'ember-engines/test-support'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { STANDARD_META } from 'vault/tests/helpers/pki'; + +module('Integration | Component | pki-paginated-list', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + + 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.pushPayload('pki/key', { + modelName: 'pki/key', + data: { + key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', + key_type: 'ec', + key_name: 'test-key', + }, + }); + this.store.pushPayload('pki/key', { + modelName: 'pki/key', + data: { + key_id: '9fdddf12-9ce3-0268-6b34-dc1553b00175', + key_type: 'rsa', + key_name: 'another-key', + }, + }); + // toArray to mimic what happens in lazyPaginatedQuery + const keyModels = this.store.peekAll('pki/key').toArray(); + keyModels.meta = STANDARD_META; + this.list = keyModels; + const emptyList = this.store.peekAll('pki/foo'); + emptyList.meta = { + meta: { + total: 0, + currentPage: 1, + pageSize: 100, + }, + }; + this.emptyList = emptyList; + }); + + test('it renders correctly with a list', async function (assert) { + this.set('hasConfig', null); + await render( + hbs` + + <:list as |items|> + {{#each items as |item|}} +
{{item.keyName}}
+ {{/each}} + + <:empty> + No items found + + <:configure> + Not configured + +
+ `, + { owner: this.engine } + ); + + assert.dom(this.element).doesNotContainText('Not configured', 'defaults to has config if not boolean'); + assert.dom(this.element).doesNotContainText('No items found', 'does not render empty state'); + assert.dom('[data-test-item]').exists({ count: 2 }, 'lists the items'); + assert.dom('[data-test-item="724862ff-6438-bad0-b598-77a6c7f4e934"]').hasText('test-key'); + assert.dom('[data-test-item="9fdddf12-9ce3-0268-6b34-dc1553b00175"]').hasText('another-key'); + assert.dom('[data-test-pagination]').exists('shows pagination'); + await this.set('hasConfig', false); + assert.dom(this.element).doesNotContainText('No items found', 'does not render empty state'); + assert.dom(this.element).containsText('Not configured', 'shows configuration prompt'); + assert.dom('[data-test-item]').doesNotExist('Does not show list items when not configured'); + assert.dom('[data-test-pagination]').doesNotExist('hides pagination'); + }); + + test('it renders correctly with an empty list', async function (assert) { + this.set('hasConfig', true); + await render( + hbs` + + <:list> + List item + + <:empty> + No items found + + <:configure> + Not configured + + + `, + { owner: this.engine } + ); + + assert.dom(this.element).doesNotContainText('list item', 'does not render list items if empty'); + assert.dom(this.element).hasText('No items found', 'shows empty block'); + assert.dom(this.element).doesNotContainText('Not configured', 'does not show configuration prompt'); + assert.dom('[data-test-pagination]').doesNotExist('hides pagination'); + await this.set('hasConfig', false); + assert.dom(this.element).doesNotContainText('list item', 'does not render list items if empty'); + assert.dom(this.element).doesNotContainText('No items found', 'does not show empty state'); + assert.dom(this.element).hasText('Not configured', 'shows configuration prompt'); + assert.dom('[data-test-pagination]').doesNotExist('hides pagination'); + }); + + test('it renders actions, description, pagination', async function (assert) { + this.set('hasConfig', true); + this.set('model', this.list); + await render( + hbs` + + <:actions> +
Action
+ + <:description> + Description goes here + + <:list> + List items + + <:empty> + No items found + + <:configure> + Not configured + +
+ `, + { owner: this.engine } + ); + assert + .dom('[data-test-button]') + .includesText('Action', 'Renders actions in toolbar when list and config'); + assert + .dom(this.element) + .includesText('Description goes here', 'renders description when list and config'); + assert.dom('[data-test-pagination]').exists('shows pagination when list and config'); + + this.set('model', this.emptyList); + assert + .dom('[data-test-button]') + .hasText('Action', 'Renders actions in toolbar when empty list and config'); + assert + .dom(this.element) + .doesNotIncludeText('Description goes here', 'hides description when empty list and config'); + assert.dom('[data-test-pagination]').doesNotExist('hides pagination when empty list and config'); + + this.set('hasConfig', false); + assert.dom('[data-test-button]').hasText('Action', 'Renders actions in toolbar when no config'); + assert.dom(this.element).doesNotIncludeText('Description goes here', 'hides description when no config'); + assert.dom('[data-test-pagination]').doesNotExist('hides pagination when no config'); + }); +}); diff --git a/ui/tests/integration/components/pki/page/pki-issuer-list-test.js b/ui/tests/integration/components/pki/page/pki-issuer-list-test.js index f467176202..e2fc2c7e1c 100644 --- a/ui/tests/integration/components/pki/page/pki-issuer-list-test.js +++ b/ui/tests/integration/components/pki/page/pki-issuer-list-test.js @@ -9,6 +9,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; import { setupRenderingTest } from 'vault/tests/helpers'; +import { STANDARD_META } from 'vault/tests/helpers/pki'; /** * this test is for the page component only. A separate test is written for the form rendered @@ -42,7 +43,9 @@ module('Integration | Component | page/pki-issuer-list', function (hooks) { }, serialNumber: '74:2d:ed:f2:c4:3b:76:5e:6e:0d:f1:6a:c0:8b:6f:e3:3c:62:f9:03', }); - this.issuers = this.store.peekAll('pki/issuer'); + const issuers = this.store.peekAll('pki/issuer'); + issuers.meta = STANDARD_META; + this.issuers = issuers; await render(hbs``, { owner: this.engine, @@ -70,8 +73,9 @@ module('Integration | Component | page/pki-issuer-list', function (hooks) { issuerName: 'issuer-1', isDefault: true, }); - this.issuers = this.store.peekAll('pki/issuer'); - + const issuers = this.store.peekAll('pki/issuer'); + issuers.meta = STANDARD_META; + this.issuers = issuers; await render(hbs``, { owner: this.engine, }); diff --git a/ui/tests/integration/components/pki/page/pki-key-list-test.js b/ui/tests/integration/components/pki/page/pki-key-list-test.js index f82669f73d..2562d28c4e 100644 --- a/ui/tests/integration/components/pki/page/pki-key-list-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-list-test.js @@ -10,6 +10,7 @@ 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/page/pki-keys'; +import { STANDARD_META } from 'vault/tests/helpers/pki'; module('Integration | Component | pki key list page', function (hooks) { setupRenderingTest(hooks); @@ -32,12 +33,20 @@ module('Integration | Component | pki key list page', function (hooks) { key_type: 'rsa', key_name: 'another-key', }); - this.keyModels = this.store.peekAll('pki/key'); + const keyModels = this.store.peekAll('pki/key'); + keyModels.meta = STANDARD_META; + this.keyModels = keyModels; }); test('it renders empty state when no keys exist', async function (assert) { assert.expect(3); - this.keyModels = []; + this.keyModels = { + meta: { + total: 0, + currentPage: 1, + pageSize: 100, + }, + }; await render( hbs`