mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: add pagination to new PKI (#23193)
This commit is contained in:
3
changelog/23193.txt
Normal file
3
changelog/23193.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: Add pagination to PKI roles, keys, issuers, and certificates list pages
|
||||||
|
```
|
||||||
@@ -66,9 +66,12 @@ export default class StoreService extends Store {
|
|||||||
// pageFilter: a string that will be used to do a fuzzy match against the
|
// pageFilter: a string that will be used to do a fuzzy match against the
|
||||||
// results, this is done pre-pagination
|
// results, this is done pre-pagination
|
||||||
lazyPaginatedQuery(modelType, query, adapterOptions) {
|
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 adapter = this.adapterFor(modelType);
|
||||||
const modelName = normalizeModelName(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;
|
const responsePath = query.responsePath;
|
||||||
assert('responsePath is required', responsePath);
|
assert('responsePath is required', responsePath);
|
||||||
assert('page is required', typeof query.page === 'number');
|
assert('page is required', typeof query.page === 'number');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
import errorMessage from 'vault/utils/error-message';
|
import errorMessage from 'vault/utils/error-message';
|
||||||
import type RouterService from '@ember/routing/router-service';
|
import type RouterService from '@ember/routing/router-service';
|
||||||
import type FlashMessageService from 'vault/services/flash-messages';
|
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';
|
import type VersionService from 'vault/services/version';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
|
|||||||
@@ -3,68 +3,114 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{#each @issuers as |pkiIssuer idx|}}
|
<PkiPaginatedList @listRoute="issuers.index" @list={{@issuers}}>
|
||||||
<LinkedBlock class="list-item-row" @params={{array "issuers.issuer.details" pkiIssuer.id}} @linkPrefix={{@mountPoint}}>
|
<:actions>
|
||||||
<div class="level is-mobile">
|
<ToolbarLink @route="issuers.import" data-test-generate-issuer="import">
|
||||||
<div class="level-left">
|
Import
|
||||||
<div data-test-issuer-list={{pkiIssuer.id}}>
|
</ToolbarLink>
|
||||||
<Icon @name="certificate" class="has-text-grey-light" />
|
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||||
<span class="has-text-weight-semibold is-underline">
|
<D.Trigger
|
||||||
{{pkiIssuer.issuerRef}}
|
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||||
{{#if pkiIssuer.issuerName}}
|
@htmlTag="button"
|
||||||
<span class="tag has-text-grey-dark">{{pkiIssuer.id}}</span>
|
data-test-issuer-generate-dropdown
|
||||||
{{/if}}
|
>
|
||||||
</span>
|
Generate
|
||||||
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
<Chevron @direction="down" @isButton={{true}} />
|
||||||
{{#if pkiIssuer.isDefault}}
|
</D.Trigger>
|
||||||
<span class="tag has-text-grey-dark" data-test-is-default={{idx}}>default issuer</span>
|
<D.Content @defaultClass="popup-menu-content">
|
||||||
{{/if}}
|
<nav class="box menu" aria-label="generate options">
|
||||||
{{#if (not-eq pkiIssuer.isRoot undefined)}}
|
<ul class="menu-list">
|
||||||
<span class="tag has-text-grey-dark" data-test-is-root-tag={{idx}}>{{if
|
<li class="action">
|
||||||
pkiIssuer.isRoot
|
<LinkTo @route="issuers.generate-root" {{on "click" (fn this.onLinkClick D)}} data-test-generate-issuer="root">
|
||||||
"root"
|
Root
|
||||||
"intermediate"
|
</LinkTo>
|
||||||
}}</span>
|
</li>
|
||||||
{{/if}}
|
<li class="action">
|
||||||
{{#if pkiIssuer.serialNumber}}
|
<LinkTo
|
||||||
<span class="tag is-transparent has-right-margin-none" data-test-serial-number={{idx}}>
|
@route="issuers.generate-intermediate"
|
||||||
<InfoTooltip>
|
{{on "click" (fn this.onLinkClick D)}}
|
||||||
Serial number
|
data-test-generate-issuer="intermediate"
|
||||||
</InfoTooltip>
|
>
|
||||||
{{pkiIssuer.serialNumber}}
|
Intermediate CSR
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</D.Content>
|
||||||
|
</BasicDropdown>
|
||||||
|
</:actions>
|
||||||
|
<:list as |issuers|>
|
||||||
|
{{#each issuers as |pkiIssuer idx|}}
|
||||||
|
<LinkedBlock class="list-item-row" @params={{array "issuers.issuer.details" pkiIssuer.id}} @linkPrefix={{@mountPoint}}>
|
||||||
|
<div class="level is-mobile">
|
||||||
|
<div class="level-left">
|
||||||
|
<div data-test-issuer-list={{pkiIssuer.id}}>
|
||||||
|
<Icon @name="certificate" class="has-text-grey-light" />
|
||||||
|
<span class="has-text-weight-semibold is-underline">
|
||||||
|
{{pkiIssuer.issuerRef}}
|
||||||
|
{{#if pkiIssuer.issuerName}}
|
||||||
|
<span class="tag has-text-grey-dark">{{pkiIssuer.id}}</span>
|
||||||
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
||||||
{{#if pkiIssuer.parsedCertificate.common_name}}
|
{{#if pkiIssuer.isDefault}}
|
||||||
<span class="tag is-transparent has-left-margin-none" data-test-common-name={{idx}}>
|
<span class="tag has-text-grey-dark" data-test-is-default={{idx}}>default issuer</span>
|
||||||
<InfoTooltip>
|
{{/if}}
|
||||||
Common name
|
{{#if (not-eq pkiIssuer.isRoot undefined)}}
|
||||||
</InfoTooltip>
|
<span class="tag has-text-grey-dark" data-test-is-root-tag={{idx}}>{{if
|
||||||
{{pkiIssuer.parsedCertificate.common_name}}
|
pkiIssuer.isRoot
|
||||||
</span>
|
"root"
|
||||||
{{/if}}
|
"intermediate"
|
||||||
|
}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if pkiIssuer.serialNumber}}
|
||||||
|
<span class="tag is-transparent has-right-margin-none" data-test-serial-number={{idx}}>
|
||||||
|
<InfoTooltip>
|
||||||
|
Serial number
|
||||||
|
</InfoTooltip>
|
||||||
|
{{pkiIssuer.serialNumber}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if pkiIssuer.parsedCertificate.common_name}}
|
||||||
|
<span class="tag is-transparent has-left-margin-none" data-test-common-name={{idx}}>
|
||||||
|
<InfoTooltip>
|
||||||
|
Common name
|
||||||
|
</InfoTooltip>
|
||||||
|
{{pkiIssuer.parsedCertificate.common_name}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right is-flex is-paddingless is-marginless">
|
||||||
|
<div class="level-item">
|
||||||
|
<PopupMenu>
|
||||||
|
<nav class="menu" aria-label="issuer config options">
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li data-test-popup-menu-details>
|
||||||
|
<LinkTo @route="issuers.issuer.details" @model={{pkiIssuer.id}}>
|
||||||
|
Details
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkTo @route="issuers.issuer.edit" @model={{pkiIssuer.id}}>
|
||||||
|
Edit
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</PopupMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</LinkedBlock>
|
||||||
<div class="level-right is-flex is-paddingless is-marginless">
|
{{/each}}
|
||||||
<div class="level-item">
|
</:list>
|
||||||
<PopupMenu>
|
<:empty>
|
||||||
<nav class="menu" aria-label="issuer config options">
|
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||||
<ul class="menu-list">
|
<LinkTo @route="configuration.create">
|
||||||
<li data-test-popup-menu-details>
|
Configure PKI
|
||||||
<LinkTo @route="issuers.issuer.details" @model={{pkiIssuer.id}}>
|
</LinkTo>
|
||||||
Details
|
</EmptyState>
|
||||||
</LinkTo>
|
</:empty>
|
||||||
</li>
|
</PkiPaginatedList>
|
||||||
<li>
|
|
||||||
<LinkTo @route="issuers.issuer.edit" @model={{pkiIssuer.id}}>
|
|
||||||
Edit
|
|
||||||
</LinkTo>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</PopupMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LinkedBlock>
|
|
||||||
{{/each}}
|
|
||||||
29
ui/lib/pki/addon/components/page/pki-issuer-list.ts
Normal file
29
ui/lib/pki/addon/components/page/pki-issuer-list.ts
Normal file
@@ -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<Args> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Toolbar>
|
<PkiPaginatedList @listRoute="keys.index" @list={{@keyModels}} @hasConfig={{@hasConfig}}>
|
||||||
<ToolbarActions>
|
<:actions>
|
||||||
{{#if @canImportKey}}
|
{{#if @canImportKey}}
|
||||||
<ToolbarLink @route="keys.import" @type="download" data-test-pki-key-import>
|
<ToolbarLink @route="keys.import" @type="download" data-test-pki-key-import>
|
||||||
Import
|
Import
|
||||||
@@ -15,62 +15,73 @@
|
|||||||
Generate
|
Generate
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ToolbarActions>
|
</:actions>
|
||||||
</Toolbar>
|
<:description>
|
||||||
<p class="has-padding">Below is information about the private keys used by the issuers to sign certificates. While
|
<p class="has-padding">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
|
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.</p>
|
secret used to prove who they are and who they trust.</p>
|
||||||
|
</:description>
|
||||||
{{#if @keyModels.length}}
|
<:list as |keys|>
|
||||||
{{#each @keyModels as |pkiKey|}}
|
{{#each keys as |pkiKey|}}
|
||||||
<LinkedBlock class="list-item-row" @params={{array "keys.key.details" pkiKey.keyId}} @linkPrefix={{@mountPoint}}>
|
<LinkedBlock class="list-item-row" @params={{array "keys.key.details" pkiKey.keyId}} @linkPrefix={{@mountPoint}}>
|
||||||
<div class="level is-mobile">
|
<div class="level is-mobile">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div>
|
<div>
|
||||||
<Icon @name="certificate" class="has-text-grey-light" />
|
<Icon @name="certificate" class="has-text-grey-light" />
|
||||||
<span class="has-text-weight-semibold is-underline" data-test-key={{if pkiKey.keyName "name" "id"}}>
|
<span class="has-text-weight-semibold is-underline" data-test-key={{if pkiKey.keyName "name" "id"}}>
|
||||||
{{or pkiKey.keyName pkiKey.id}}
|
{{or pkiKey.keyName pkiKey.id}}
|
||||||
</span>
|
</span>
|
||||||
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
||||||
{{#if pkiKey.keyName}}
|
{{#if pkiKey.keyName}}
|
||||||
<span class="tag has-text-grey-dark" data-test-key="id">{{pkiKey.id}}</span>
|
<span class="tag has-text-grey-dark" data-test-key="id">{{pkiKey.id}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-right is-flex is-paddingless is-marginless">
|
||||||
|
<div class="level-item">
|
||||||
|
<PopupMenu>
|
||||||
|
<nav class="menu">
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
<LinkTo
|
||||||
|
@route="keys.key.details"
|
||||||
|
@model={{pkiKey.keyId}}
|
||||||
|
@disabled={{not @canRead}}
|
||||||
|
data-test-key-menu-link="details"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkTo
|
||||||
|
@route="keys.key.edit"
|
||||||
|
@model={{pkiKey.keyId}}
|
||||||
|
@disabled={{not @canEdit}}
|
||||||
|
data-test-key-menu-link="edit"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</PopupMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right is-flex is-paddingless is-marginless">
|
</LinkedBlock>
|
||||||
<div class="level-item">
|
{{/each}}
|
||||||
<PopupMenu>
|
</:list>
|
||||||
<nav class="menu">
|
|
||||||
<ul class="menu-list">
|
<:empty>
|
||||||
<li>
|
<EmptyState @title="No keys yet" @message="There are no keys in this PKI mount. You can generate or create one." />
|
||||||
<LinkTo
|
</:empty>
|
||||||
@route="keys.key.details"
|
|
||||||
@model={{pkiKey.keyId}}
|
<:configure>
|
||||||
@disabled={{not @canRead}}
|
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||||
data-test-key-menu-link="details"
|
<LinkTo @route="configuration.create">
|
||||||
>
|
Configure PKI
|
||||||
Details
|
</LinkTo>
|
||||||
</LinkTo>
|
</EmptyState>
|
||||||
</li>
|
</:configure>
|
||||||
<li>
|
</PkiPaginatedList>
|
||||||
<LinkTo
|
|
||||||
@route="keys.key.edit"
|
|
||||||
@model={{pkiKey.keyId}}
|
|
||||||
@disabled={{not @canEdit}}
|
|
||||||
data-test-key-menu-link="edit"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</LinkTo>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</PopupMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LinkedBlock>
|
|
||||||
{{/each}}
|
|
||||||
{{else}}
|
|
||||||
<EmptyState @title="No keys yet" @message="There are no keys in this PKI mount. You can generate or create one." />
|
|
||||||
{{/if}}
|
|
||||||
22
ui/lib/pki/addon/components/page/pki-key-list.ts
Normal file
22
ui/lib/pki/addon/components/page/pki-key-list.ts
Normal file
@@ -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<Args> {
|
||||||
|
notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG;
|
||||||
|
}
|
||||||
25
ui/lib/pki/addon/components/pki-paginated-list.hbs
Normal file
25
ui/lib/pki/addon/components/pki-paginated-list.hbs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Toolbar>
|
||||||
|
<ToolbarActions>
|
||||||
|
{{yield to="actions"}}
|
||||||
|
</ToolbarActions>
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
{{#if this.hasConfig}}
|
||||||
|
{{#if @list.meta.total}}
|
||||||
|
{{yield to="description"}}
|
||||||
|
{{yield @list to="list"}}
|
||||||
|
<Hds::Pagination::Numbered
|
||||||
|
@currentPage={{@list.meta.currentPage}}
|
||||||
|
@currentPageSize={{@list.meta.pageSize}}
|
||||||
|
@route={{@listRoute}}
|
||||||
|
@showSizeSelector={{false}}
|
||||||
|
@totalItems={{@list.meta.total}}
|
||||||
|
@queryFunction={{this.paginationQueryParams}}
|
||||||
|
data-test-pagination
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
{{yield to="empty"}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{yield to="configure"}}
|
||||||
|
{{/if}}
|
||||||
33
ui/lib/pki/addon/components/pki-paginated-list.ts
Normal file
33
ui/lib/pki/addon/components/pki-paginated-list.ts
Normal file
@@ -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
|
||||||
|
* <PkiPaginatedList @list={{this.model.roles}} @hasConfig={{this.model.hasConfig}} @listRoute="roles.index">
|
||||||
|
* <:list as |items|>
|
||||||
|
* {{#each items as |item}}
|
||||||
|
* <div>for each thing</div>
|
||||||
|
* {{/each}}
|
||||||
|
* </:list>
|
||||||
|
* </PkiPaginatedList>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
list: unknown[];
|
||||||
|
listRoute: string;
|
||||||
|
hasConfig?: boolean;
|
||||||
|
}
|
||||||
|
export default class PkiPaginatedListComponent extends Component<Args> {
|
||||||
|
get paginationQueryParams() {
|
||||||
|
return (page: number) => ({ page });
|
||||||
|
}
|
||||||
|
get hasConfig() {
|
||||||
|
if (typeof this.args.hasConfig === 'boolean') return this.args.hasConfig;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{#if @model.serialNumber}}
|
{{#if @model.serialNumber}}
|
||||||
<Page::PkiCertificateDetails @model={{@model}} @onRevoke={{this.cancel}} @onBack={{this.cancel}} />
|
<Page::PkiCertificateDetails @model={{@model}} @onBack={{this.cancel}} />
|
||||||
{{else}}
|
{{else}}
|
||||||
<form {{on "submit" (perform this.save)}} data-test-pki-generate-cert-form>
|
<form {{on "submit" (perform this.save)}} data-test-pki-generate-cert-form>
|
||||||
<div class="box is-bottomless is-fullwidth is-marginless">
|
<div class="box is-bottomless is-fullwidth is-marginless">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { getOwner } from '@ember/application';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
export default class PkiCertificatesIndexController extends Controller {
|
export default class PkiCertificatesIndexController extends Controller {
|
||||||
|
queryParams = ['page'];
|
||||||
|
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).mountPoint;
|
return getOwner(this).mountPoint;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { next } from '@ember/runloop';
|
|
||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
|
|
||||||
export default class PkiIssuerIndexController extends Controller {
|
export default class PkiIssuerIndexController extends Controller {
|
||||||
|
queryParams = ['page'];
|
||||||
|
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Controller from '@ember/controller';
|
|||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
|
|
||||||
export default class PkiKeysIndexController extends Controller {
|
export default class PkiKeysIndexController extends Controller {
|
||||||
|
queryParams = ['page'];
|
||||||
|
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).mountPoint;
|
return getOwner(this).mountPoint;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Controller from '@ember/controller';
|
|||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
|
|
||||||
export default class PkiRolesIndexController extends Controller {
|
export default class PkiRolesIndexController extends Controller {
|
||||||
|
queryParams = ['page'];
|
||||||
|
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).mountPoint;
|
return getOwner(this).mountPoint;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,35 @@ export default class PkiCertificatesIndexRoute extends Route {
|
|||||||
@service store;
|
@service store;
|
||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
|
|
||||||
async fetchCertificates() {
|
queryParams = {
|
||||||
|
page: {
|
||||||
|
refreshModel: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fetchCertificates(params) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
if (e.httpStatus === 404) {
|
if (e.httpStatus === 404) {
|
||||||
return { parentModel: this.modelFor('certificates') };
|
return { parentModel: this.modelFor('certificates') };
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model() {
|
model(params) {
|
||||||
return hash({
|
return hash({
|
||||||
hasConfig: this.shouldPromptConfig,
|
hasConfig: this.shouldPromptConfig,
|
||||||
certificates: this.fetchCertificates(),
|
certificates: this.fetchCertificates(params),
|
||||||
parentModel: this.modelFor('certificates'),
|
parentModel: this.modelFor('certificates'),
|
||||||
|
pageFilter: params.pageFilter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,4 +53,10 @@ export default class PkiCertificatesIndexRoute extends Route {
|
|||||||
if (certificates?.length) controller.notConfiguredMessage = getCliMessage('certificates');
|
if (certificates?.length) controller.notConfiguredMessage = getCliMessage('certificates');
|
||||||
else controller.notConfiguredMessage = getCliMessage();
|
else controller.notConfiguredMessage = getCliMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
controller.set('page', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,21 @@
|
|||||||
|
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview';
|
|
||||||
|
|
||||||
export default class PkiIssuersListRoute extends Route {
|
export default class PkiIssuersListRoute extends Route {
|
||||||
@service store;
|
@service store;
|
||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
|
|
||||||
model() {
|
model(params) {
|
||||||
|
const page = Number(params.page) || 1;
|
||||||
return this.store
|
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) => {
|
.then((issuersModel) => {
|
||||||
return { issuersModel, parentModel: this.modelFor('issuers') };
|
return { issuersModel, parentModel: this.modelFor('issuers') };
|
||||||
})
|
})
|
||||||
@@ -33,6 +39,11 @@ export default class PkiIssuersListRoute extends Route {
|
|||||||
{ label: this.secretMountPath.currentPath, route: 'overview' },
|
{ label: this.secretMountPath.currentPath, route: 'overview' },
|
||||||
{ label: 'issuers', route: 'issuers.index' },
|
{ label: 'issuers', route: 'issuers.index' },
|
||||||
];
|
];
|
||||||
controller.notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG;
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
controller.set('page', undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,31 @@ export default class PkiKeysIndexRoute extends Route {
|
|||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
model() {
|
queryParams = {
|
||||||
|
page: {
|
||||||
|
refreshModel: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
const page = Number(params.page) || 1;
|
||||||
return hash({
|
return hash({
|
||||||
hasConfig: this.shouldPromptConfig,
|
hasConfig: this.shouldPromptConfig,
|
||||||
parentModel: this.modelFor('keys'),
|
parentModel: this.modelFor('keys'),
|
||||||
keyModels: this.store.query('pki/key', { backend: this.secretMountPath.currentPath }).catch((err) => {
|
keyModels: this.store
|
||||||
if (err.httpStatus === 404) {
|
.lazyPaginatedQuery('pki/key', {
|
||||||
return [];
|
backend: this.secretMountPath.currentPath,
|
||||||
} else {
|
responsePath: 'data.keys',
|
||||||
throw err;
|
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;
|
controller.notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
controller.set('page', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,23 +13,35 @@ export default class PkiRolesIndexRoute extends Route {
|
|||||||
@service store;
|
@service store;
|
||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
|
|
||||||
async fetchRoles() {
|
queryParams = {
|
||||||
|
page: {
|
||||||
|
refreshModel: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fetchRoles(params) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
if (e.httpStatus === 404) {
|
if (e.httpStatus === 404) {
|
||||||
return { parentModel: this.modelFor('roles') };
|
return { parentModel: this.modelFor('roles') };
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model() {
|
model(params) {
|
||||||
return hash({
|
return hash({
|
||||||
hasConfig: this.shouldPromptConfig,
|
hasConfig: this.shouldPromptConfig,
|
||||||
roles: this.fetchRoles(),
|
roles: this.fetchRoles(params),
|
||||||
parentModel: this.modelFor('roles'),
|
parentModel: this.modelFor('roles'),
|
||||||
|
pageFilter: params.pageFilter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,4 +52,10 @@ export default class PkiRolesIndexRoute extends Route {
|
|||||||
if (roles?.length) controller.notConfiguredMessage = getCliMessage('roles');
|
if (roles?.length) controller.notConfiguredMessage = getCliMessage('roles');
|
||||||
else controller.notConfiguredMessage = getCliMessage();
|
else controller.notConfiguredMessage = getCliMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
controller.set('page', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,9 @@
|
|||||||
|
|
||||||
<PkiPageHeader @backend={{this.model.parentModel}} />
|
<PkiPageHeader @backend={{this.model.parentModel}} />
|
||||||
|
|
||||||
{{outlet}}
|
<PkiPaginatedList @listRoute="certificates.index" @list={{this.model.certificates}} @hasConfig={{this.model.hasConfig}}>
|
||||||
<Toolbar>
|
<:list as |certs|>
|
||||||
{{#if this.model.certificates.length}}
|
{{#each certs as |pkiCertificate|}}
|
||||||
<ToolbarFilters>
|
|
||||||
{{! TODO add NavigateInput component }}
|
|
||||||
</ToolbarFilters>
|
|
||||||
{{/if}}
|
|
||||||
</Toolbar>
|
|
||||||
|
|
||||||
{{#if this.model.hasConfig}}
|
|
||||||
{{#if this.model.certificates.length}}
|
|
||||||
{{#each this.model.certificates as |pkiCertificate|}}
|
|
||||||
<LinkedBlock
|
<LinkedBlock
|
||||||
class="list-item-row"
|
class="list-item-row"
|
||||||
@params={{array "certificates.certificate.details" pkiCertificate.id}}
|
@params={{array "certificates.certificate.details" pkiCertificate.id}}
|
||||||
@@ -51,7 +42,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</LinkedBlock>
|
</LinkedBlock>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
</:list>
|
||||||
|
<:empty>
|
||||||
<EmptyState @title="No certificates yet">
|
<EmptyState @title="No certificates yet">
|
||||||
<div>
|
<div>
|
||||||
<p>When created, certificates will be listed here. Select a role to start generating certificates.</p>
|
<p>When created, certificates will be listed here. Select a role to start generating certificates.</p>
|
||||||
@@ -62,11 +54,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
{{/if}}
|
</:empty>
|
||||||
{{else}}
|
<:configure>
|
||||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||||
<LinkTo @route="configuration.create">
|
<LinkTo @route="configuration.create">
|
||||||
Configure PKI
|
Configure PKI
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
{{/if}}
|
</:configure>
|
||||||
|
</PkiPaginatedList>
|
||||||
@@ -5,50 +5,4 @@
|
|||||||
|
|
||||||
<PkiPageHeader @backend={{this.model.parentModel}} />
|
<PkiPageHeader @backend={{this.model.parentModel}} />
|
||||||
|
|
||||||
<Toolbar>
|
<Page::PkiIssuerList @issuers={{this.model.issuersModel}} @mountPoint={{this.mountPoint}} />
|
||||||
<ToolbarActions>
|
|
||||||
<ToolbarLink @route="issuers.import" data-test-generate-issuer="import">
|
|
||||||
Import
|
|
||||||
</ToolbarLink>
|
|
||||||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
|
||||||
<D.Trigger
|
|
||||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
|
||||||
@htmlTag="button"
|
|
||||||
data-test-issuer-generate-dropdown
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
<Chevron @direction="down" @isButton={{true}} />
|
|
||||||
</D.Trigger>
|
|
||||||
<D.Content @defaultClass="popup-menu-content">
|
|
||||||
<nav class="box menu" aria-label="generate options">
|
|
||||||
<ul class="menu-list">
|
|
||||||
<li class="action">
|
|
||||||
<LinkTo @route="issuers.generate-root" {{on "click" (fn this.onLinkClick D)}} data-test-generate-issuer="root">
|
|
||||||
Root
|
|
||||||
</LinkTo>
|
|
||||||
</li>
|
|
||||||
<li class="action">
|
|
||||||
<LinkTo
|
|
||||||
@route="issuers.generate-intermediate"
|
|
||||||
{{on "click" (fn this.onLinkClick D)}}
|
|
||||||
data-test-generate-issuer="intermediate"
|
|
||||||
>
|
|
||||||
Intermediate CSR
|
|
||||||
</LinkTo>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</D.Content>
|
|
||||||
</BasicDropdown>
|
|
||||||
</ToolbarActions>
|
|
||||||
</Toolbar>
|
|
||||||
|
|
||||||
{{#if this.model.issuersModel.length}}
|
|
||||||
<Page::PkiIssuerList @issuers={{this.model.issuersModel}} @mountPoint={{this.mountPoint}} />
|
|
||||||
{{else}}
|
|
||||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
|
||||||
<LinkTo @route="configuration.create">
|
|
||||||
Configure PKI
|
|
||||||
</LinkTo>
|
|
||||||
</EmptyState>
|
|
||||||
{{/if}}
|
|
||||||
@@ -5,20 +5,12 @@
|
|||||||
|
|
||||||
<PkiPageHeader @backend={{this.model.parentModel}} />
|
<PkiPageHeader @backend={{this.model.parentModel}} />
|
||||||
|
|
||||||
{{#if (or this.model.hasConfig this.model.keyModels)}}
|
<Page::PkiKeyList
|
||||||
<Page::PkiKeyList
|
@keyModels={{this.model.keyModels}}
|
||||||
@keyModels={{this.model.keyModels}}
|
@mountPoint={{this.mountPoint}}
|
||||||
@mountPoint={{this.mountPoint}}
|
@canImportKey={{this.model.keyModels.firstObject.canImportKey}}
|
||||||
@canImportKey={{this.model.keyModels.firstObject.canImportKey}}
|
@canGenerateKey={{this.model.keyModels.firstObject.canGenerateKey}}
|
||||||
@canGenerateKey={{this.model.keyModels.firstObject.canGenerateKey}}
|
@canRead={{this.model.keyModels.firstObject.canRead}}
|
||||||
@canRead={{this.model.keyModels.firstObject.canRead}}
|
@canEdit={{this.model.keyModels.firstObject.canEdit}}
|
||||||
@canEdit={{this.model.keyModels.firstObject.canEdit}}
|
@hasConfig={{this.model.hasConfig}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
|
||||||
<Toolbar />
|
|
||||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
|
||||||
<LinkTo @route="configuration.create">
|
|
||||||
Configure PKI
|
|
||||||
</LinkTo>
|
|
||||||
</EmptyState>
|
|
||||||
{{/if}}
|
|
||||||
@@ -5,17 +5,16 @@
|
|||||||
|
|
||||||
<PkiPageHeader @backend={{this.model.parentModel}} />
|
<PkiPageHeader @backend={{this.model.parentModel}} />
|
||||||
|
|
||||||
{{#if this.model.hasConfig}}
|
<PkiPaginatedList @listRoute="roles.index" @list={{this.model.roles}} @hasConfig={{this.model.hasConfig}}>
|
||||||
<Toolbar>
|
<:actions>
|
||||||
<ToolbarActions>
|
{{#if this.model.hasConfig}}
|
||||||
<ToolbarLink @type="add" @route="roles.create" data-test-pki-role-create-link>
|
<ToolbarLink @type="add" @route="roles.create" data-test-pki-role-create-link>
|
||||||
Create role
|
Create role
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</ToolbarActions>
|
{{/if}}
|
||||||
</Toolbar>
|
</:actions>
|
||||||
|
<:list as |roles|>
|
||||||
{{#if this.model.roles.length}}
|
{{#each roles as |pkiRole|}}
|
||||||
{{#each this.model.roles as |pkiRole|}}
|
|
||||||
<LinkedBlock class="list-item-row" @params={{array "roles.role.details" pkiRole.id}} @linkPrefix={{this.mountPoint}}>
|
<LinkedBlock class="list-item-row" @params={{array "roles.role.details" pkiRole.id}} @linkPrefix={{this.mountPoint}}>
|
||||||
<div class="level is-mobile">
|
<div class="level is-mobile">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
@@ -49,7 +48,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</LinkedBlock>
|
</LinkedBlock>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
</:list>
|
||||||
|
<:empty>
|
||||||
<EmptyState @title="No roles yet">
|
<EmptyState @title="No roles yet">
|
||||||
<div>
|
<div>
|
||||||
<p>When created, roles will be listed here. Create a role to start generating certificates.</p>
|
<p>When created, roles will be listed here. Create a role to start generating certificates.</p>
|
||||||
@@ -60,12 +60,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
{{/if}}
|
</:empty>
|
||||||
{{else}}
|
<:configure>
|
||||||
<Toolbar />
|
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
<LinkTo @route="configuration.create">
|
||||||
<LinkTo @route="configuration.create">
|
Configure PKI
|
||||||
Configure PKI
|
</LinkTo>
|
||||||
</LinkTo>
|
</EmptyState>
|
||||||
</EmptyState>
|
</:configure>
|
||||||
{{/if}}
|
</PkiPaginatedList>
|
||||||
@@ -231,18 +231,21 @@ module('Acceptance | landing page dashboard', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('shows the correct actions and links associated with pki', async function (assert) {
|
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([
|
await runCommands([
|
||||||
`write pki/roles/some-role \
|
`write ${backend}/roles/some-role \
|
||||||
issuer_ref="default" \
|
issuer_ref="default" \
|
||||||
allowed_domains="example.com" \
|
allowed_domains="example.com" \
|
||||||
allow_subdomains=true \
|
allow_subdomains=true \
|
||||||
max_ttl="720h"`,
|
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 settled();
|
||||||
await visit('/vault/dashboard');
|
await visit('/vault/dashboard');
|
||||||
await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki');
|
await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend);
|
||||||
await fillIn(SELECTORS.selectEl, 'Issue certificate');
|
await fillIn(SELECTORS.selectEl, 'Issue certificate');
|
||||||
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
||||||
assert.dom(SELECTORS.subtitle('param')).hasText('Role to use');
|
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 visit('/vault/dashboard');
|
||||||
|
|
||||||
await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki');
|
await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend);
|
||||||
await fillIn(SELECTORS.selectEl, 'View certificate');
|
await fillIn(SELECTORS.selectEl, 'View certificate');
|
||||||
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
||||||
assert.dom(SELECTORS.subtitle('param')).hasText('Certificate serial number');
|
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 visit('/vault/dashboard');
|
||||||
|
|
||||||
await selectChoose(SELECTORS.searchSelect('secrets-engines'), 'pki');
|
await selectChoose(SELECTORS.searchSelect('secrets-engines'), backend);
|
||||||
await fillIn(SELECTORS.selectEl, 'View issuer');
|
await fillIn(SELECTORS.selectEl, 'View issuer');
|
||||||
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
assert.dom(SELECTORS.emptyState('quick-actions')).doesNotExist();
|
||||||
assert.dom(SELECTORS.subtitle('param')).hasText('Issuer');
|
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');
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details');
|
||||||
|
|
||||||
// cleanup engine mount
|
// cleanup engine mount
|
||||||
await consoleComponent.runCommands(deleteEngineCmd('pki'));
|
await consoleComponent.runCommands(deleteEngineCmd(backend));
|
||||||
});
|
});
|
||||||
|
|
||||||
const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
|
const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
|
||||||
|
|||||||
@@ -69,28 +69,50 @@ module('Acceptance | pki configuration test', function (hooks) {
|
|||||||
await authPage.login(this.pkiAdminToken);
|
await authPage.login(this.pkiAdminToken);
|
||||||
await visit(`/vault/secrets/${this.mountPath}/pki/configuration`);
|
await visit(`/vault/secrets/${this.mountPath}/pki/configuration`);
|
||||||
await click(SELECTORS.configuration.configureButton);
|
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 click(SELECTORS.configuration.generateRootOption);
|
||||||
await fillIn(SELECTORS.configuration.typeField, 'exported');
|
await fillIn(SELECTORS.configuration.typeField, 'exported');
|
||||||
await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0');
|
await fillIn(SELECTORS.configuration.generateRootCommonNameField, 'issuer-common-0');
|
||||||
await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0');
|
await fillIn(SELECTORS.configuration.generateRootIssuerNameField, 'issuer-0');
|
||||||
await click(SELECTORS.configuration.generateRootSave);
|
await click(SELECTORS.configuration.generateRootSave);
|
||||||
await click(SELECTORS.configuration.doneButton);
|
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);
|
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);
|
await click(SELECTORS.configuration.issuerLink);
|
||||||
assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists();
|
assert.dom(SELECTORS.configuration.deleteAllIssuerModal).exists();
|
||||||
await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all');
|
await fillIn(SELECTORS.configuration.deleteAllIssuerInput, 'delete-all');
|
||||||
await click(SELECTORS.configuration.deleteAllIssuerButton);
|
await click(SELECTORS.configuration.deleteAllIssuerButton);
|
||||||
await isSettled();
|
await isSettled();
|
||||||
assert.dom(SELECTORS.configuration.deleteAllIssuerModal).doesNotExist();
|
assert
|
||||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`);
|
.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 isSettled();
|
||||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||||
await waitUntil(() => currentURL() === `/vault/secrets/${this.mountPath}/pki/overview`);
|
await waitUntil(() => currentURL() === `/vault/secrets/${this.mountPath}/pki/overview`);
|
||||||
await isSettled();
|
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
|
assert
|
||||||
.dom(SELECTORS.emptyStateMessage)
|
.dom(SELECTORS.emptyStateMessage)
|
||||||
.hasText(
|
.hasText(
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ module('Acceptance | pki workflow', function (hooks) {
|
|||||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||||
await click(SELECTORS.issuersTab);
|
await click(SELECTORS.issuersTab);
|
||||||
assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag');
|
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) {
|
test('details view renders correct number of info items', async function (assert) {
|
||||||
|
|||||||
@@ -15,3 +15,9 @@ export const SELECTORS = {
|
|||||||
revocationTime: '[data-test-row-value="Revocation time"]',
|
revocationTime: '[data-test-row-value="Revocation time"]',
|
||||||
serialNumber: '[data-test-row-value="Serial number"]',
|
serialNumber: '[data-test-row-value="Serial number"]',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const STANDARD_META = {
|
||||||
|
total: 2,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
};
|
||||||
|
|||||||
158
ui/tests/integration/components/pki-paginated-list-test.js
Normal file
158
ui/tests/integration/components/pki-paginated-list-test.js
Normal file
@@ -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`
|
||||||
|
<PkiPaginatedList @list={{this.list}} @hasConfig={{this.hasConfig}}>
|
||||||
|
<:list as |items|>
|
||||||
|
{{#each items as |item|}}
|
||||||
|
<div data-test-item={{item.keyId}}>{{item.keyName}}</div>
|
||||||
|
{{/each}}
|
||||||
|
</:list>
|
||||||
|
<:empty>
|
||||||
|
No items found
|
||||||
|
</:empty>
|
||||||
|
<:configure>
|
||||||
|
Not configured
|
||||||
|
</:configure>
|
||||||
|
</PkiPaginatedList>
|
||||||
|
`,
|
||||||
|
{ 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`
|
||||||
|
<PkiPaginatedList @list={{this.emptyList}} @hasConfig={{this.hasConfig}}>
|
||||||
|
<:list>
|
||||||
|
List item
|
||||||
|
</:list>
|
||||||
|
<:empty>
|
||||||
|
No items found
|
||||||
|
</:empty>
|
||||||
|
<:configure>
|
||||||
|
Not configured
|
||||||
|
</:configure>
|
||||||
|
</PkiPaginatedList>
|
||||||
|
`,
|
||||||
|
{ 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`
|
||||||
|
<PkiPaginatedList @list={{this.model}} @hasConfig={{this.hasConfig}}>
|
||||||
|
<:actions>
|
||||||
|
<div data-test-button>Action</div>
|
||||||
|
</:actions>
|
||||||
|
<:description>
|
||||||
|
Description goes here
|
||||||
|
</:description>
|
||||||
|
<:list>
|
||||||
|
List items
|
||||||
|
</:list>
|
||||||
|
<:empty>
|
||||||
|
No items found
|
||||||
|
</:empty>
|
||||||
|
<:configure>
|
||||||
|
Not configured
|
||||||
|
</:configure>
|
||||||
|
</PkiPaginatedList>
|
||||||
|
`,
|
||||||
|
{ 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { setupEngine } from 'ember-engines/test-support';
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
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
|
* 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',
|
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`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
await render(hbs`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
@@ -70,8 +73,9 @@ module('Integration | Component | page/pki-issuer-list', function (hooks) {
|
|||||||
issuerName: 'issuer-1',
|
issuerName: 'issuer-1',
|
||||||
isDefault: true,
|
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`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
await render(hbs`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
|||||||
import { setupEngine } from 'ember-engines/test-support';
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { SELECTORS } from 'vault/tests/helpers/pki/page/pki-keys';
|
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) {
|
module('Integration | Component | pki key list page', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -32,12 +33,20 @@ module('Integration | Component | pki key list page', function (hooks) {
|
|||||||
key_type: 'rsa',
|
key_type: 'rsa',
|
||||||
key_name: 'another-key',
|
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) {
|
test('it renders empty state when no keys exist', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
this.keyModels = [];
|
this.keyModels = {
|
||||||
|
meta: {
|
||||||
|
total: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
<Page::PkiKeyList
|
<Page::PkiKeyList
|
||||||
|
|||||||
Reference in New Issue
Block a user