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 57de625419..2d627a205b 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -77,9 +77,12 @@ export default Store.extend({ // pageFilter: a string that will be used to do a fuzzy match against the // results, this is done pre-pagination lazyPaginatedQuery(modelType, query /*, options*/) { + 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'); @@ -144,6 +147,7 @@ export default Store.extend({ prevPage: clamp(currentPage - 1, 1, lastPage), total: dataset.length || 0, filteredTotal: data.length || 0, + pageSize: size, }; return response; 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 7ee8438152..33bef7e013 100644 --- a/ui/lib/pki/addon/components/page/pki-issuer-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-issuer-list.hbs @@ -1,65 +1,111 @@ -{{#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 8b7a16e752..120ce77d2e 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-list.hbs @@ -1,5 +1,5 @@ - - + + <:actions> {{#if @canImportKey}} Import @@ -10,62 +10,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 94fb3cc62f..81c4b67ef3 100644 --- a/ui/lib/pki/addon/components/pki-role-generate.hbs +++ b/ui/lib/pki/addon/components/pki-role-generate.hbs @@ -1,5 +1,5 @@ {{#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 6c52422765..18172cd283 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 80de13bf3a..000aae119e 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 2f9dc81861..e59710e7d6 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 74d6342041..cd777a8493 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 2b88c90f0e..e0e3802bf9 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 a41fe59cc7..3ffd0dcd9a 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 1c00969b0d..358adc3bfa 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 25c6dddfeb..633adcdbee 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 e7e76fb668..e6b536ad0a 100644 --- a/ui/lib/pki/addon/templates/certificates/index.hbs +++ b/ui/lib/pki/addon/templates/certificates/index.hbs @@ -8,18 +8,10 @@ }} @isEngine={{true}} /> -{{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.

@@ -66,11 +59,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 4ad6b33719..6a00cc447c 100644 --- a/ui/lib/pki/addon/templates/issuers/index.hbs +++ b/ui/lib/pki/addon/templates/issuers/index.hbs @@ -8,50 +8,5 @@ }} @isEngine={{true}} /> - - - - 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 eddf92d868..939bdcd8e3 100644 --- a/ui/lib/pki/addon/templates/keys/index.hbs +++ b/ui/lib/pki/addon/templates/keys/index.hbs @@ -8,20 +8,13 @@ }} @isEngine={{true}} /> -{{#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 89ad0fcb7e..d415231d41 100644 --- a/ui/lib/pki/addon/templates/roles/index.hbs +++ b/ui/lib/pki/addon/templates/roles/index.hbs @@ -9,17 +9,16 @@ @isEngine={{true}} /> -{{#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|}}
@@ -53,7 +52,8 @@
{{/each}} - {{else}} + + <:empty>

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

@@ -64,12 +64,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/pki/pki-configuration-test.js b/ui/tests/acceptance/pki/pki-configuration-test.js index de0d672e96..f2203672a4 100644 --- a/ui/tests/acceptance/pki/pki-configuration-test.js +++ b/ui/tests/acceptance/pki/pki-configuration-test.js @@ -70,28 +70,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 987ccbb694..4927f08d83 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -380,7 +380,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 c91dbaebd9..4ee42612d0 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 e03b36afd8..206afb8549 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 @@ -4,6 +4,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 @@ -37,7 +38,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, @@ -65,8 +68,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 a38ca6d2f7..08f97e2447 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`