mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
UI: Build KV v2 overview page (#28106)
* move date-from-now helper to addon * make overview cards consistent across engines * make kv-paths-card component * remove overview margin all together * small styling changes for paths card * small selector additions * add overview card test * add overview page and test * add default timestamp format * cleanup paths test * fix dateFromNow import * fix selectors, cleanup pki selectors * and more selector cleanup * make deactivated state single arg * fix template and remove @isDeleted and @isDestroyed * add test and hide badge unless deactivated * address failings from changing selectors * oops, not ready to show overview tab just yet! * add deletionTime to currentSecret metadata getter
This commit is contained in:
parent
255db7aab1
commit
30da9aef46
@ -69,11 +69,6 @@ export default class KvSecretMetadataModel extends Model {
|
||||
return keyIsFolder(this.path);
|
||||
}
|
||||
|
||||
// cannot use isDeleted due to ember property conflict
|
||||
get isSecretDeleted() {
|
||||
return isDeleted(this.deletionTime);
|
||||
}
|
||||
|
||||
// turns version object into an array for version dropdown menu
|
||||
get sortedVersions() {
|
||||
const array = [];
|
||||
@ -93,6 +88,7 @@ export default class KvSecretMetadataModel extends Model {
|
||||
return {
|
||||
state,
|
||||
isDeactivated: state !== 'created',
|
||||
deletionTime: data.deletion_time,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ import { obfuscateData } from 'core/utils/advanced-secret';
|
||||
* <JsonEditor @title="Policy" @value={{hash foo="bar"}} @viewportMargin={{100}} />
|
||||
*
|
||||
* @param {string} [title] - Name above codemirror view
|
||||
* @param {boolean} [showToolbar=true] - If false, toolbar and title are hidden
|
||||
* @param {string} value - a specific string the comes from codemirror. It's the value inside the codemirror display
|
||||
* @param {Function} [valueUpdated] - action to preform when you edit the codemirror value.
|
||||
* @param {Function} [onFocusOut] - action to preform when you focus out of codemirror.
|
||||
|
||||
@ -10,10 +10,14 @@
|
||||
data-test-overview-card-container={{@cardTitle}}
|
||||
...attributes
|
||||
>
|
||||
<div class="flex row-wrap space-between has-bottom-margin-s" data-test-overview-card={{@cardTitle}}>
|
||||
<Hds::Text::Display @weight="bold" @size="300" data-test-overview-card-title={{@cardTitle}}>
|
||||
{{@cardTitle}}
|
||||
</Hds::Text::Display>
|
||||
<div class="flex row-wrap space-between" data-test-overview-card={{@cardTitle}}>
|
||||
{{#if (has-block "customTitle")}}
|
||||
{{yield to="customTitle"}}
|
||||
{{else}}
|
||||
<Hds::Text::Display @weight="semibold" @size="300" data-test-overview-card-title={{@cardTitle}}>
|
||||
{{@cardTitle}}
|
||||
</Hds::Text::Display>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "action")}}
|
||||
{{yield to="action"}}
|
||||
|
||||
@ -43,7 +43,7 @@ function dateFromString(str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function dateFormat([value, style], { withTimeZone = false }) {
|
||||
export function dateFormat([value, style = 'MMM d yyyy, h:mm:ss aa'], { withTimeZone = false }) {
|
||||
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
|
||||
let date;
|
||||
switch (checkType(value)) {
|
||||
|
||||
6
ui/lib/core/app/helpers/date-from-now.js
Normal file
6
ui/lib/core/app/helpers/date-from-now.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/date-from-now';
|
||||
82
ui/lib/kv/addon/components/kv-paths-card.hbs
Normal file
82
ui/lib/kv/addon/components/kv-paths-card.hbs
Normal file
@ -0,0 +1,82 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div data-test-overview-card-container="Paths" ...attributes>
|
||||
<Hds::Text::Display @weight="semibold" @size="300" @tag="h2">
|
||||
Paths
|
||||
</Hds::Text::Display>
|
||||
{{#if @isCondensed}}
|
||||
<Hds::Text::Body @tag="p" @color="faint">
|
||||
The paths to use when referring to this secret in API or CLI.
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless {{if @isCondensed 'is-shadowless'}} ">
|
||||
{{#each this.paths as |path|}}
|
||||
<InfoTableRow
|
||||
@label={{path.label}}
|
||||
@labelWidth={{if @isCondensed "is-one-quarter" "is-one-third"}}
|
||||
@helperText={{if @isCondensed "" path.text}}
|
||||
@truncateValue={{true}}
|
||||
>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{path.snippet}}
|
||||
@onError={{fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")}}
|
||||
data-test-copy-button={{path.snippet}}
|
||||
class="transparent"
|
||||
/>
|
||||
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
|
||||
{{path.snippet}}
|
||||
</code>
|
||||
</InfoTableRow>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#unless @isCondensed}}
|
||||
<Hds::Text::Display @weight="semibold" @size="300" @tag="h3" class="has-top-margin-xl">
|
||||
Commands
|
||||
</Hds::Text::Display>
|
||||
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
<h3 class="is-label">
|
||||
CLI
|
||||
<Hds::Badge @text="kv get" @color="neutral" />
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command retrieves the value from KV secrets engine at the given key name. See our
|
||||
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/docs/commands/kv"}}>
|
||||
documentation</Hds::Link::Inline>
|
||||
for other CLI commands.
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="cli"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.cli}}
|
||||
/>
|
||||
|
||||
<h3 class="has-top-margin-l is-label">
|
||||
API read secret version
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
|
||||
https://127.0.0.1:8200. For other API commands,
|
||||
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="api"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.api}}
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
@ -9,23 +9,21 @@ import { kvMetadataPath, kvDataPath } from 'vault/utils/kv-path';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
/**
|
||||
* @module KvSecretPaths is used to display copyable secret paths for KV v2 for CLI and API use.
|
||||
* This view is permission agnostic because args come from the views mount path and url params.
|
||||
* @module KvPathsCard is used to display copyable secret paths for KV v2 for CLI and API use.
|
||||
* This component is permission agnostic because args come from the views mount path and url params.
|
||||
*
|
||||
* <Page::Secret::Paths
|
||||
* <KvPathsCard
|
||||
* @path={{this.model.path}}
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadMetadata={{this.model.secret.canReadMetadata}}
|
||||
* @isCondensed={{false}}
|
||||
* />
|
||||
*
|
||||
* @param {string} path - kv secret path for building the CLI and API paths
|
||||
* @param {string} backend - the secret engine mount path, comes from the secretMountPath service defined in the route
|
||||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
|
||||
* @param {boolean} [canReadMetadata=true] - if true, displays tab for Version History
|
||||
* @param {boolean} isCondensed - if true a smaller version displays with no commands section or extra explanatory text
|
||||
*/
|
||||
|
||||
export default class KvSecretPaths extends Component {
|
||||
export default class KvPathsCard extends Component {
|
||||
@service namespace;
|
||||
|
||||
get paths() {
|
||||
@ -46,11 +44,15 @@ export default class KvSecretPaths extends Component {
|
||||
snippet: namespace ? `-namespace=${namespace} ${cli}` : cli,
|
||||
text: 'Use this path when referring to this secret in the CLI.',
|
||||
},
|
||||
{
|
||||
label: 'API path for metadata',
|
||||
snippet: namespace ? `/v1/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`,
|
||||
text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`,
|
||||
},
|
||||
...(this.args.isCondensed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: 'API path for metadata',
|
||||
snippet: namespace ? `/v1/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`,
|
||||
text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`,
|
||||
},
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<OverviewCard @cardTitle="Subkeys">
|
||||
<OverviewCard @cardTitle="Subkeys" class="has-top-margin-l">
|
||||
<:customSubtext>
|
||||
<Hds::Text::Body @color="faint" data-test-overview-card-subtitle="Subkeys">
|
||||
<Hds::Text::Body @tag="p" @color="faint" data-test-overview-card-subtitle="Subkeys">
|
||||
{{#if this.showJson}}
|
||||
These are the subkeys within this secret. All underlying values of leaf keys are not retrieved and are replaced with
|
||||
<code>null</code>
|
||||
|
||||
105
ui/lib/kv/addon/components/page/secret/overview.hbs
Normal file
105
ui/lib/kv/addon/components/page/secret/overview.hbs
Normal file
@ -0,0 +1,105 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
|
||||
<:tabLinks>
|
||||
<li>
|
||||
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="secret.details" @models={{array @backend @path}} data-test-secrets-tab="Secret">Secret</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="secret.metadata.index"
|
||||
@models={{array @backend @path}}
|
||||
data-test-secrets-tab="Metadata"
|
||||
>Metadata</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="secret.paths" @models={{array @backend @path}} data-test-secrets-tab="Paths">Paths</LinkTo>
|
||||
</li>
|
||||
{{#if @canReadMetadata}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="secret.metadata.versions"
|
||||
@models={{array @backend @path}}
|
||||
data-test-secrets-tab="Version History"
|
||||
>Version History</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</:tabLinks>
|
||||
</KvPageHeader>
|
||||
|
||||
{{#if (or @metadata @subkeys)}}
|
||||
<div class="flex row-wrap gap-24 has-top-margin-l">
|
||||
<OverviewCard @cardTitle="Current version" @subText={{this.versionSubtext}} class="is-flex-1">
|
||||
<:customTitle>
|
||||
<Hds::Text::Display @weight="semibold" @size="300">
|
||||
Current version
|
||||
{{#unless this.isActive}}
|
||||
<Hds::Badge
|
||||
@text={{capitalize @secretState}}
|
||||
@type={{if (eq @secretState "destroyed") "outlined" "inverted"}}
|
||||
@color={{if (eq @secretState "destroyed") "critical"}}
|
||||
@icon="x"
|
||||
/>
|
||||
{{/unless}}
|
||||
</Hds::Text::Display>
|
||||
</:customTitle>
|
||||
<:action>
|
||||
{{#if @canUpdateSecret}}
|
||||
<Hds::Link::Standalone
|
||||
@text="Create new"
|
||||
@route="secret.details.edit"
|
||||
@models={{array @backend @path}}
|
||||
@icon="plus"
|
||||
@iconPosition="trailing"
|
||||
/>
|
||||
{{/if}}
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{or @metadata.currentVersion @subkeys.metadata.version}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
|
||||
{{#if this.isActive}}
|
||||
{{#let (or @metadata.createdTime @subkeys.metadata.created_time) as |timestamp|}}
|
||||
<OverviewCard
|
||||
@cardTitle="Secret age"
|
||||
@subText="Time since last update at {{date-format timestamp}}."
|
||||
class="is-flex-1"
|
||||
>
|
||||
<:action>
|
||||
{{#if @canReadMetadata}}
|
||||
<Hds::Link::Standalone
|
||||
@text="View metadata"
|
||||
@route="secret.metadata"
|
||||
@models={{array @backend @path}}
|
||||
@icon="arrow-right"
|
||||
@iconPosition="trailing"
|
||||
/>
|
||||
{{/if}}
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{date-from-now timestamp}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l has-top-margin-l">
|
||||
<KvPathsCard @backend={{@backend}} @path={{@path}} @isCondensed={{true}} />
|
||||
</Hds::Card::Container>
|
||||
|
||||
{{#if @subkeys.subkeys}}
|
||||
<KvSubkeys @subkeys={{@subkeys.subkeys}} />
|
||||
{{/if}}
|
||||
51
ui/lib/kv/addon/components/page/secret/overview.js
Normal file
51
ui/lib/kv/addon/components/page/secret/overview.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { dateFormat } from 'core/helpers/date-format';
|
||||
|
||||
/**
|
||||
* @module KvSecretOverview
|
||||
* <Page::Secret::Overview
|
||||
* @backend={{this.model.backend}}
|
||||
* @breadcrumbs={{this.breadcrumbs}}
|
||||
* @canReadMetadata={{true}}
|
||||
* @canUpdateSecret={{true}}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path={{this.model.path}}
|
||||
* @secretState="created"
|
||||
* @subkeys={{this.model.subkeys}}
|
||||
* />
|
||||
*
|
||||
* @param {string} backend - kv secret mount to make network request
|
||||
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
|
||||
* @param {boolean} canReadMetadata - permissions to read metadata
|
||||
* @param {boolean} canUpdateSecret - permissions to create a new version of a secret
|
||||
* @param {model} metadata - Ember data model: 'kv/metadata'
|
||||
* @param {string} path - path to request secret data for selected version
|
||||
* @param {string} secretState - if a secret has been "destroyed", "deleted" or "created" (still active)
|
||||
* @param {object} subkeys - API response from subkeys endpoint, object with "subkeys" and "metadata" keys
|
||||
*/
|
||||
|
||||
export default class KvSecretOverview extends Component {
|
||||
get isActive() {
|
||||
const state = this.args.secretState;
|
||||
return state !== 'destroyed' && state !== 'deleted';
|
||||
}
|
||||
|
||||
get versionSubtext() {
|
||||
const state = this.args.secretState;
|
||||
if (state === 'destroyed') {
|
||||
return 'The current version of this secret has been permanently deleted and cannot be restored.';
|
||||
}
|
||||
if (state === 'deleted') {
|
||||
const time =
|
||||
this.args.metadata?.currentSecret.deletionTime || this.args.subkeys?.metadata.deletion_time;
|
||||
const date = dateFormat([time], {});
|
||||
return `The current version of this secret was deleted ${date}.`;
|
||||
}
|
||||
return 'The current version of this secret.';
|
||||
}
|
||||
}
|
||||
@ -36,66 +36,4 @@
|
||||
</:tabLinks>
|
||||
</KvPageHeader>
|
||||
|
||||
<h2 class="title is-5 has-top-margin-xl">
|
||||
Paths
|
||||
</h2>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.paths as |path|}}
|
||||
<InfoTableRow @label={{path.label}} @labelWidth="is-one-third" @helperText={{path.text}} @truncateValue={{true}}>
|
||||
<Hds::Copy::Button
|
||||
@text="Copy"
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{path.snippet}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
data-test-copy-button={{path.snippet}}
|
||||
class="transparent"
|
||||
/>
|
||||
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
|
||||
{{path.snippet}}
|
||||
</code>
|
||||
</InfoTableRow>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<h2 class="title is-5 has-top-margin-xl">
|
||||
Commands
|
||||
</h2>
|
||||
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
<h3 class="is-label">
|
||||
CLI
|
||||
<Hds::Badge @text="kv get" @color="neutral" />
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command retrieves the value from KV secrets engine at the given key name. For other CLI commands,
|
||||
<DocLink @path="/vault/docs/commands/kv">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="cli"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.cli}}
|
||||
/>
|
||||
|
||||
<h3 class="has-top-margin-l is-label">
|
||||
API read secret version
|
||||
</h3>
|
||||
<p class="helper-text has-text-grey-light has-bottom-padding-s">
|
||||
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
|
||||
https://127.0.0.1:8200. For other API commands,
|
||||
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
|
||||
learn more.
|
||||
</DocLink>
|
||||
</p>
|
||||
<Hds::CodeBlock
|
||||
data-test-commands="api"
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{this.commands.api}}
|
||||
/>
|
||||
</div>
|
||||
<KvPathsCard @backend={{@backend}} @path={{@path}} class="has-top-margin-xl" />
|
||||
@ -18,7 +18,7 @@
|
||||
/>
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="h2" @size="500">
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
@ -38,7 +38,7 @@
|
||||
/>
|
||||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="h2" @size="500">
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @roles 404) 0 @roles.length)}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
|
||||
@ -14,6 +14,8 @@ import { click, currentURL, currentRouteName, visit } from '@ember/test-helpers'
|
||||
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { clearRecords } from 'vault/tests/helpers/pki/pki-helpers';
|
||||
import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
const { overviewCard } = GENERAL;
|
||||
|
||||
module('Acceptance | pki overview', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@ -59,9 +61,9 @@ module('Acceptance | pki overview', function (hooks) {
|
||||
test('navigates to view issuers when link is clicked on issuer card', async function (assert) {
|
||||
await authPage.login(this.pkiAdminToken);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers');
|
||||
assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('1');
|
||||
await click(PKI_OVERVIEW.issuersCardLink);
|
||||
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
|
||||
assert.dom(`${overviewCard.container('Issuers')} p`).hasText('1');
|
||||
await click(overviewCard.actionLink('Issuers'));
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
});
|
||||
@ -69,9 +71,9 @@ module('Acceptance | pki overview', function (hooks) {
|
||||
test('navigates to view roles when link is clicked on roles card', async function (assert) {
|
||||
await authPage.login(this.pkiAdminToken);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles');
|
||||
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0');
|
||||
await click(PKI_OVERVIEW.rolesCardLink);
|
||||
assert.dom(overviewCard.title('Roles')).hasText('Roles');
|
||||
assert.dom(`${overviewCard.container('Roles')} p`).hasText('0');
|
||||
await click(overviewCard.actionLink('Roles'));
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
|
||||
await runCmd([
|
||||
`write ${this.mountPath}/roles/some-role \
|
||||
@ -81,14 +83,14 @@ module('Acceptance | pki overview', function (hooks) {
|
||||
max_ttl="720h"`,
|
||||
]);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('1');
|
||||
assert.dom(`${overviewCard.container('Roles')} p`).hasText('1');
|
||||
});
|
||||
|
||||
test('hides roles card if user does not have permissions', async function (assert) {
|
||||
await authPage.login(this.pkiIssuersList);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
assert.dom(PKI_OVERVIEW.rolesCardTitle).doesNotExist('Roles card does not exist');
|
||||
assert.dom(PKI_OVERVIEW.issuersCardTitle).exists('Issuers card exists');
|
||||
assert.dom(overviewCard.title('Roles')).doesNotExist('Roles card does not exist');
|
||||
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
|
||||
});
|
||||
|
||||
test('navigates to generate certificate page for Issue Certificates card', async function (assert) {
|
||||
|
||||
@ -64,7 +64,7 @@ module('Acceptance | sync | overview', function (hooks) {
|
||||
await click(ts.navLink('Secrets Sync'));
|
||||
await click(ts.destinations.list.create);
|
||||
await click(ts.createCancel);
|
||||
await click(ts.overviewCard.actionLink('Create new'));
|
||||
await click(ts.overviewCard.actionText('Create new'));
|
||||
await click(ts.createCancel);
|
||||
await waitFor(ts.overview.table.actionToggle(0));
|
||||
await click(ts.overview.table.actionToggle(0));
|
||||
|
||||
@ -74,11 +74,12 @@ export const GENERAL = {
|
||||
removeSelected: '[data-test-selected-list-button="delete"]',
|
||||
},
|
||||
overviewCard: {
|
||||
container: (title: string) => `[data-test-overview-card-container="${title}"]`,
|
||||
title: (title: string) => `[data-test-overview-card-title="${title}"]`,
|
||||
description: (title: string) => `[data-test-overview-card-subtitle="${title}"]`,
|
||||
content: (title: string) => `[data-test-overview-card-content="${title}"]`,
|
||||
action: (title: string) => `[data-test-overview-card-container="${title}"] [data-test-action-text]`,
|
||||
actionLink: (label: string) => `[data-test-action-text="${label}"]`,
|
||||
actionText: (text: string) => `[data-test-action-text="${text}"]`,
|
||||
actionLink: (label: string) => `[data-test-overview-card="${label}"] a`,
|
||||
},
|
||||
pagination: {
|
||||
next: '.hds-pagination-nav__arrow--direction-next',
|
||||
|
||||
@ -7,6 +7,9 @@
|
||||
import { click, fillIn, visit, settled } from '@ember/test-helpers';
|
||||
import { FORM } from './kv-selectors';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { assert } from '@ember/debug';
|
||||
import { kvMetadataPath } from 'vault/utils/kv-path';
|
||||
|
||||
// CUSTOM ACTIONS RELEVANT TO KV-V2
|
||||
|
||||
@ -66,3 +69,30 @@ export function clearRecords(store) {
|
||||
store.unloadAll('kv/metatata');
|
||||
store.unloadAll('capabilities');
|
||||
}
|
||||
|
||||
// TEST SETUP HELPERS
|
||||
|
||||
// sets basic path, backend, and metadata
|
||||
export const baseSetup = (context) => {
|
||||
assert(
|
||||
`'baseSetup()' requires mirage: import { setupMirage } from 'ember-cli-mirage/test-support'`,
|
||||
context.server
|
||||
);
|
||||
context.store = context.owner.lookup('service:store');
|
||||
context.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
context.backend = 'kv-engine';
|
||||
context.path = 'my-secret';
|
||||
context.metadata = metadataModel(context, { withCustom: false });
|
||||
};
|
||||
|
||||
export const metadataModel = (context, { withCustom = false }) => {
|
||||
const metadata = withCustom
|
||||
? context.server.create('kv-metadatum', 'withCustomMetadata')
|
||||
: context.server.create('kv-metadatum');
|
||||
metadata.id = kvMetadataPath(context.backend, context.path);
|
||||
context.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...metadata,
|
||||
});
|
||||
return context.store.peekRecord('kv/metadata', metadata.id);
|
||||
};
|
||||
|
||||
@ -4,23 +4,12 @@
|
||||
*/
|
||||
|
||||
export const PKI_OVERVIEW = {
|
||||
issuersCardTitle: '[data-test-overview-card-title="Issuers"]',
|
||||
issuersCardSubtitle: '[data-test-overview-card-subtitle="Issuers"]',
|
||||
issuersCardLink: '[data-test-overview-card-container="Issuers"] a',
|
||||
issuersCardOverviewNum: '[data-test-overview-card-container="Issuers"] h2',
|
||||
rolesCardTitle: '[data-test-overview-card-title="Roles"]',
|
||||
rolesCardSubtitle: '[data-test-overview-card-subtitle="Roles"]',
|
||||
rolesCardLink: '[data-test-overview-card-container="Roles"] a',
|
||||
rolesCardOverviewNum: '[data-test-overview-card-container="Roles"] h2',
|
||||
issueCertificate: '[data-test-overview-card-title="Issue certificate"]',
|
||||
issueCertificateInput: '[data-test-issue-certificate-input]',
|
||||
issueCertificatePowerSearch: '[data-test-issue-certificate-input] span',
|
||||
issueCertificateButton: '[data-test-issue-certificate-button]',
|
||||
viewCertificate: '[data-test-overview-card-title="View certificate"]',
|
||||
viewCertificateInput: '[data-test-view-certificate-input]',
|
||||
viewCertificatePowerSearch: '[data-test-view-certificate-input] span',
|
||||
viewCertificateButton: '[data-test-view-certificate-button]',
|
||||
viewIssuerInput: '[data-test-issue-issuer-input]',
|
||||
viewIssuerPowerSearch: '[data-test-issue-issuer-input] span',
|
||||
viewIssuerButton: '[data-test-view-issuer-button]',
|
||||
firstPowerSelectOption: '[data-option-index="0"]',
|
||||
|
||||
137
ui/tests/integration/components/kv/kv-paths-card-test.js
Normal file
137
ui/tests/integration/components/kv/kv-paths-card-test.js
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
/* eslint-disable no-useless-escape */
|
||||
|
||||
module('Integration | Component | kv-v2 | KvPathsCard', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.backend = 'kv-engine';
|
||||
this.path = 'my-secret';
|
||||
this.isCondensed = false;
|
||||
|
||||
this.assertClipboard = (assert, element, expected) => {
|
||||
assert.dom(element).hasAttribute('data-test-copy-button', expected);
|
||||
};
|
||||
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`<KvPathsCard @backend={{this.backend}} @path={{this.path}} @isCondensed={{this.isCondensed}} />`,
|
||||
{
|
||||
owner: this.engine,
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders condensed version', async function (assert) {
|
||||
this.isCondensed = true;
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-component="info-table-row"] .helper-text').doesNotExist('subtext does not render');
|
||||
assert.dom('[data-test-label-div]').hasClass('is-one-quarter');
|
||||
assert.dom(PAGE.infoRowValue('API path for metadata')).doesNotExist();
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).doesNotExist();
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).doesNotExist();
|
||||
|
||||
const paths = [
|
||||
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
|
||||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
|
||||
];
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders uncondensed version', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-component="info-table-row"] .helper-text').exists('subtext renders');
|
||||
assert.dom('[data-test-label-div]').hasClass('is-one-third');
|
||||
assert.dom(PAGE.infoRowValue('API path for metadata')).exists();
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).exists();
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).exists();
|
||||
});
|
||||
|
||||
test('it renders copyable paths', async function (assert) {
|
||||
const paths = [
|
||||
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
|
||||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
|
||||
{ label: 'API path for metadata', expected: `/v1/${this.backend}/metadata/${this.path}` },
|
||||
];
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders copyable encoded mount and secret paths', async function (assert) {
|
||||
this.path = `my spacey!"secret`;
|
||||
this.backend = `my fancy!"backend`;
|
||||
const backend = encodeURIComponent(this.backend);
|
||||
const path = encodeURIComponent(this.path);
|
||||
const paths = [
|
||||
{
|
||||
label: 'API path',
|
||||
expected: `/v1/${backend}/data/${path}`,
|
||||
},
|
||||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
|
||||
{
|
||||
label: 'API path for metadata',
|
||||
expected: `/v1/${backend}/metadata/${path}`,
|
||||
},
|
||||
];
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders copyable commands', async function (assert) {
|
||||
const url = `https://127.0.0.1:8200/v1/${this.backend}/data/${this.path}`;
|
||||
const expected = {
|
||||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
|
||||
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
|
||||
};
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
|
||||
});
|
||||
|
||||
test('it renders copyable encoded mount and path commands', async function (assert) {
|
||||
this.path = `my spacey!"secret`;
|
||||
this.backend = `my fancy!"backend`;
|
||||
|
||||
const backend = encodeURIComponent(this.backend);
|
||||
const path = encodeURIComponent(this.path);
|
||||
const url = `https://127.0.0.1:8200/v1/${backend}/data/${path}`;
|
||||
|
||||
const expected = {
|
||||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
|
||||
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
|
||||
};
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
|
||||
});
|
||||
});
|
||||
341
ui/tests/integration/components/kv/page/kv-page-overview-test.js
Normal file
341
ui/tests/integration/components/kv/page/kv-page-overview-test.js
Normal file
@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { dateFormat } from 'core/helpers/date-format';
|
||||
import { dateFromNow } from 'core/helpers/date-from-now';
|
||||
import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
|
||||
const { overviewCard } = GENERAL;
|
||||
module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
baseSetup(this);
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend, route: 'list' },
|
||||
{ label: this.path },
|
||||
];
|
||||
this.subkeys = {
|
||||
subkeys: {
|
||||
foo: null,
|
||||
bar: {
|
||||
baz: null,
|
||||
},
|
||||
quux: null,
|
||||
},
|
||||
metadata: {
|
||||
created_time: '2021-12-14T20:28:00.773477Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
this.canReadMetadata = true;
|
||||
this.canUpdateSecret = true;
|
||||
this.secretState = 'created';
|
||||
|
||||
this.format = (time) => dateFormat([time, 'MMM d yyyy, h:mm:ss aa'], {});
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<Page::Secret::Overview
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
@canUpdateSecret={{this.canUpdateSecret}}
|
||||
@metadata={{this.metadata}}
|
||||
@path={{this.path}}
|
||||
@secretState={{this.secretState}}
|
||||
@subkeys={{this.subkeys}}
|
||||
/>`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
module('it renders when version is not deleted nor destroyed', function () {
|
||||
test('it renders tabs', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const tabs = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History'];
|
||||
for (const tab of tabs) {
|
||||
assert.dom(PAGE.secretTab(tab)).hasText(tab);
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders header', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`);
|
||||
assert.dom(PAGE.title).hasText(this.path);
|
||||
});
|
||||
|
||||
test('it renders with full permissions', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util
|
||||
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).doesNotExist();
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Create new The current version of this secret. ${this.metadata.currentVersion}`
|
||||
);
|
||||
assert
|
||||
.dom(overviewCard.container('Secret age'))
|
||||
.hasText(
|
||||
`Secret age View metadata Time since last update at ${this.format(
|
||||
this.metadata.createdTime
|
||||
)}. ${fromNow}`
|
||||
);
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
assert
|
||||
.dom(overviewCard.container('Subkeys'))
|
||||
.hasText(
|
||||
`Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys(
|
||||
this.subkeys.subkeys
|
||||
).join(' ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('it hides link when no secret update permissions', async function (assert) {
|
||||
// creating a new version of a secret is updating a secret
|
||||
// the overview only exists after an initial version is created
|
||||
// which is why we just check for update and not also create
|
||||
this.canUpdateSecret = false;
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} a`)
|
||||
.doesNotExist('create link does not render');
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(`Current version The current version of this secret. ${this.metadata.currentVersion}`);
|
||||
});
|
||||
|
||||
test('it renders with no metadata permissions', async function (assert) {
|
||||
this.metadata = null;
|
||||
this.canReadMetadata = false;
|
||||
// all secret metadata instead comes from subkeys endpoint
|
||||
const subkeyMeta = this.subkeys.metadata;
|
||||
await this.renderComponent();
|
||||
const fromNow = dateFromNow([subkeyMeta.created_time]); // uses date-fns so can't stub timestamp util
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(`Current version Create new The current version of this secret. ${subkeyMeta.version}`);
|
||||
assert
|
||||
.dom(overviewCard.container('Secret age'))
|
||||
.hasText(`Secret age Time since last update at ${this.format(subkeyMeta.created_time)}. ${fromNow}`);
|
||||
assert.dom(`${overviewCard.container('Secret age')} a`).doesNotExist('metadata link does not render');
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
assert
|
||||
.dom(overviewCard.container('Subkeys'))
|
||||
.hasText(
|
||||
`Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys(
|
||||
this.subkeys.subkeys
|
||||
).join(' ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with no subkeys permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
await this.renderComponent();
|
||||
const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util
|
||||
const expectedTime = this.format(this.metadata.createdTime);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Create new The current version of this secret. ${this.metadata.currentVersion}`
|
||||
);
|
||||
assert
|
||||
.dom(overviewCard.container('Secret age'))
|
||||
.hasText(`Secret age View metadata Time since last update at ${expectedTime}. ${fromNow}`);
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders with no subkey or metadata permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
this.metadata = null;
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.container('Current version')).doesNotExist();
|
||||
assert.dom(overviewCard.container('Secret age')).doesNotExist();
|
||||
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('it renders when version is deleted', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.secretState = 'deleted';
|
||||
// subkeys is null but metadata still has data
|
||||
this.subkeys = {
|
||||
subkeys: null,
|
||||
metadata: {
|
||||
created_time: '2021-12-14T20:28:00.773477Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '2022-02-14T20:28:00.773477Z',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
this.metadata.versions[4].deletion_time = '2024-08-15T23:01:08.312332Z';
|
||||
this.assertBadge = (assert) => {
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} .hds-badge`)
|
||||
.hasClass('hds-badge--color-neutral');
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} .hds-badge`)
|
||||
.hasClass('hds-badge--type-inverted');
|
||||
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Deleted');
|
||||
};
|
||||
});
|
||||
|
||||
test('with full permissions', async function (assert) {
|
||||
const expectedTime = this.format(this.metadata.versions[4].deletion_time);
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}`
|
||||
);
|
||||
assert.dom(overviewCard.container('Secret age')).doesNotExist();
|
||||
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no metadata permissions', async function (assert) {
|
||||
this.metadata = null;
|
||||
const expectedTime = this.format(this.subkeys.metadata.deletion_time);
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.subkeys.metadata.version}`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no subkey permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
const expectedTime = this.format(this.metadata.versions[4].deletion_time);
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
this.metadata = null;
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.container('Current version')).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
module('it renders when version is destroyed', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.secretState = 'destroyed';
|
||||
// subkeys is null but metadata still has data
|
||||
this.subkeys = {
|
||||
subkeys: null,
|
||||
metadata: {
|
||||
created_time: '2024-08-15T01:24:43.658478Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: true,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
this.metadata.versions[4].destroyed = true;
|
||||
this.assertBadge = (assert) => {
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} .hds-badge`)
|
||||
.hasClass('hds-badge--color-critical');
|
||||
assert
|
||||
.dom(`${overviewCard.container('Current version')} .hds-badge`)
|
||||
.hasClass('hds-badge--type-outlined');
|
||||
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Destroyed');
|
||||
};
|
||||
});
|
||||
|
||||
test('with full permissions', async function (assert) {
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}`
|
||||
);
|
||||
assert.dom(overviewCard.container('Secret age')).doesNotExist();
|
||||
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
|
||||
assert
|
||||
.dom(overviewCard.container('Paths'))
|
||||
.hasText(
|
||||
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no metadata permissions', async function (assert) {
|
||||
this.metadata = null;
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.subkeys.metadata.version}`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no subkeys permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
await this.renderComponent();
|
||||
this.assertBadge(assert);
|
||||
assert
|
||||
.dom(overviewCard.container('Current version'))
|
||||
.hasText(
|
||||
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}`
|
||||
);
|
||||
});
|
||||
|
||||
test('with no permissions', async function (assert) {
|
||||
this.subkeys = null;
|
||||
this.metadata = null;
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.container('Current version')).doesNotExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -9,8 +9,6 @@ import { setupEngine } from 'ember-engines/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
/* eslint-disable no-useless-escape */
|
||||
|
||||
module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
@ -18,123 +16,55 @@ module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks)
|
||||
hooks.beforeEach(async function () {
|
||||
this.backend = 'kv-engine';
|
||||
this.path = 'my-secret';
|
||||
this.canReadMetadata = true;
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend, route: 'list' },
|
||||
{ label: this.path },
|
||||
];
|
||||
|
||||
this.assertClipboard = (assert, element, expected) => {
|
||||
assert.dom(element).hasAttribute('data-test-copy-button', expected);
|
||||
this.renderComponent = async () => {
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@canReadMetadata={{this.canReadMetadata}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders copyable paths', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const paths = [
|
||||
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
|
||||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
|
||||
{ label: 'API path for metadata', expected: `/v1/${this.backend}/metadata/${this.path}` },
|
||||
];
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
test('it renders tabs', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const tabs = ['Secret', 'Metadata', 'Paths', 'Version History'];
|
||||
for (const tab of tabs) {
|
||||
assert.dom(PAGE.secretTab(tab)).hasText(tab);
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders copyable encoded mount and secret paths', async function (assert) {
|
||||
assert.expect(6);
|
||||
this.path = `my spacey!"secret`;
|
||||
this.backend = `my fancy!"backend`;
|
||||
const backend = encodeURIComponent(this.backend);
|
||||
const path = encodeURIComponent(this.path);
|
||||
const paths = [
|
||||
{
|
||||
label: 'API path',
|
||||
expected: `/v1/${backend}/data/${path}`,
|
||||
},
|
||||
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
|
||||
{
|
||||
label: 'API path for metadata',
|
||||
expected: `/v1/${backend}/metadata/${path}`,
|
||||
},
|
||||
];
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
for (const path of paths) {
|
||||
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
|
||||
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
|
||||
test('it hides version history when cannot READ metadata', async function (assert) {
|
||||
this.canReadMetadata = false;
|
||||
await this.renderComponent();
|
||||
const tabs = ['Secret', 'Metadata', 'Paths'];
|
||||
for (const tab of tabs) {
|
||||
assert.dom(PAGE.secretTab(tab)).hasText(tab);
|
||||
}
|
||||
assert.dom(PAGE.secretTab('Version History')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders copyable commands', async function (assert) {
|
||||
const url = `https://127.0.0.1:8200/v1/${this.backend}/data/${this.path}`;
|
||||
const expected = {
|
||||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
|
||||
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
|
||||
};
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
|
||||
test('it renders header', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`);
|
||||
assert.dom(PAGE.title).hasText(this.path);
|
||||
});
|
||||
|
||||
test('it renders copyable encoded mount and path commands', async function (assert) {
|
||||
this.path = `my spacey!"secret`;
|
||||
this.backend = `my fancy!"backend`;
|
||||
|
||||
const backend = encodeURIComponent(this.backend);
|
||||
const path = encodeURIComponent(this.path);
|
||||
const url = `https://127.0.0.1:8200/v1/${backend}/data/${path}`;
|
||||
|
||||
const expected = {
|
||||
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
|
||||
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
|
||||
};
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Paths
|
||||
@path={{this.path}}
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
|
||||
test('it renders commands which is the uncondensed version of KvPathsCard', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(PAGE.paths.codeSnippet('cli')).exists();
|
||||
assert.dom(PAGE.paths.codeSnippet('api')).exists();
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,6 +13,7 @@ const ACTION_TEXT = 'View card';
|
||||
const SUBTEXT = 'This is subtext for card';
|
||||
|
||||
const SELECTORS = {
|
||||
container: '[data-test-overview-card-container]',
|
||||
title: '[data-test-overview-card-title]',
|
||||
subtitle: '[data-test-overview-card-subtitle]',
|
||||
action: '[data-test-action-text]',
|
||||
@ -28,10 +29,21 @@ module('Integration | Component | overview-card', function (hooks) {
|
||||
this.set('subText', SUBTEXT);
|
||||
});
|
||||
|
||||
test('it returns card title, ', async function (assert) {
|
||||
test('it returns card title', async function (assert) {
|
||||
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}}/>`);
|
||||
assert.dom(SELECTORS.title).hasText('Card title');
|
||||
});
|
||||
test('it returns custom title if both exist', async function (assert) {
|
||||
await render(hbs`
|
||||
<OverviewCard @cardTitle={{this.cardTitle}}>
|
||||
<:customTitle>
|
||||
Fancy custom title
|
||||
</:customTitle>
|
||||
</OverviewCard>
|
||||
`);
|
||||
assert.dom(SELECTORS.container).hasText('Fancy custom title');
|
||||
assert.dom(SELECTORS.container).doesNotIncludeText(this.cardTitle);
|
||||
});
|
||||
test('it renders card @subText arg, ', async function (assert) {
|
||||
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}} @subText={{this.subText}} />`);
|
||||
assert.dom(SELECTORS.subtitle).hasText('This is subtext for card');
|
||||
|
||||
@ -10,7 +10,9 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const { overviewCard } = GENERAL;
|
||||
module('Integration | Component | Page::PkiOverview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
@ -39,9 +41,11 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers');
|
||||
assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('2');
|
||||
assert.dom(PKI_OVERVIEW.issuersCardLink).hasText('View issuers');
|
||||
assert
|
||||
.dom(overviewCard.container('Issuers'))
|
||||
.hasText(
|
||||
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows the correct information on roles card', async function (assert) {
|
||||
@ -49,15 +53,21 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles');
|
||||
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('3');
|
||||
assert.dom(PKI_OVERVIEW.rolesCardLink).hasText('View roles');
|
||||
assert
|
||||
.dom(overviewCard.container('Roles'))
|
||||
.hasText(
|
||||
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 3'
|
||||
);
|
||||
this.roles = 404;
|
||||
await render(
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0');
|
||||
assert
|
||||
.dom(overviewCard.container('Roles'))
|
||||
.hasText(
|
||||
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 0'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows the input search fields for View Certificates card', async function (assert) {
|
||||
@ -65,7 +75,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_OVERVIEW.issueCertificate).hasText('Issue certificate');
|
||||
assert.dom(overviewCard.title('Issue certificate')).hasText('Issue certificate');
|
||||
assert.dom(PKI_OVERVIEW.issueCertificateInput).exists();
|
||||
assert.dom(PKI_OVERVIEW.issueCertificateButton).hasText('Issue');
|
||||
});
|
||||
@ -75,7 +85,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(PKI_OVERVIEW.viewCertificate).hasText('View certificate');
|
||||
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
|
||||
});
|
||||
|
||||
@ -358,7 +358,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
||||
test('it should show the Totals cards', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
const { title, description, action, content } = overviewCard;
|
||||
const { title, description, actionLink, content } = overviewCard;
|
||||
const cardData = [
|
||||
{
|
||||
cardTitle: 'Total destinations',
|
||||
@ -379,7 +379,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
||||
assert.dom(title(cardTitle)).hasText(cardTitle, `${cardTitle} card title renders`);
|
||||
assert.dom(description(cardTitle)).hasText(subText, ` ${cardTitle} card description renders`);
|
||||
assert.dom(content(cardTitle)).hasText(count, 'Total count renders');
|
||||
assert.dom(action(cardTitle)).hasText(actionText, 'Card action renders');
|
||||
assert.dom(actionLink(cardTitle)).hasText(actionText, 'Card action renders');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,6 +8,7 @@ import { setupRenderingTest } from 'ember-qunit';
|
||||
import { find, render, settled } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { formatTimeZone } from 'core/helpers/date-format';
|
||||
import { isMatch } from 'date-fns';
|
||||
|
||||
const TEST_DATE = new Date('2018-04-03T14:15:30');
|
||||
|
||||
@ -56,6 +57,14 @@ module('Integration | Helper | date-format', function (hooks) {
|
||||
assert.strictEqual(resultLengthWithTimezone - 4, 4, 'Adds 4 characters for timezone');
|
||||
});
|
||||
|
||||
test('it renders default format', async function (assert) {
|
||||
this.set('timestampDate', TEST_DATE);
|
||||
await render(hbs`<span data-test-formatted>{{date-format this.timestampDate}}</span>`);
|
||||
const value = find('[data-test-formatted]').innerText;
|
||||
const format = 'MMM d yyyy, h:mm:ss aa';
|
||||
assert.true(isMatch(value, format), `${value} is formatted as ${format}`);
|
||||
});
|
||||
|
||||
test('fails gracefully if given a non-date value', async function (assert) {
|
||||
this.set('value', 'not a date');
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { dateFromNow } from '../../../helpers/date-from-now';
|
||||
import { dateFromNow } from 'core/helpers/date-from-now';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user