UI: move selectable card to add-on (#23739)

* remove title-number class and consolidate border radius

* move selectable card to core addon

* add top padding to db cards

* update transform icon color

* new selectable card component

* fix db test

* use selectable card in mount backend form

* fix query param for overview card

* update tests

* fix replication card styling

* make card accessible;

* update tabindex

* change to standalone for error handling

* update test selector

* update tests

* go back to number only css class

* fix on click tests

* add changelog

* update class name in template file

* delete box radio
This commit is contained in:
claire bontempo
2023-10-20 15:10:49 -07:00
committed by GitHub
parent b2870dd23f
commit a5b60cd8cc
54 changed files with 220 additions and 602 deletions

3
changelog/14998.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: Update mount backend form to use selectable cards
```

View File

@@ -3,13 +3,12 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<form {{on "submit" this.transitionToCredential}} class="selectable-card is-rounded no-flex"> <Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l" ...attributes>
<div class="is-flex-between is-fullwidth card-details"> <form {{on "submit" this.transitionToCredential}}>
<div class="is-flex-between is-fullwidth">
<h3 class="title is-5">{{@title}}</h3> <h3 class="title is-5">{{@title}}</h3>
</div> </div>
<div class="has-top-bottom-margin"> <p class="has-top-margin-s has-bottom-margin-xs has-text-weight-semibold">{{@searchLabel}}</p>
<p class="is-label search-label">{{@searchLabel}}</p>
</div>
<SearchSelect <SearchSelect
@id="search-input-role" @id="search-input-role"
@models={{@models}} @models={{@models}}
@@ -20,7 +19,12 @@
@placeholder={{@placeholder}} @placeholder={{@placeholder}}
data-test-search-roles data-test-search-roles
/> />
<button type="submit" class="button is-secondary" disabled={{not this.role}} data-test-get-credentials> <Hds::Button
Get credentials @text="Get credentials"
</button> type="submit"
</form> @color="secondary"
disabled={{not this.role}}
data-test-get-credentials
/>
</form>
</Hds::Card::Container>

View File

@@ -4,39 +4,30 @@
~}} ~}}
{{#each (array "generic" "cloud" "infra") as |category|}} {{#each (array "generic" "cloud" "infra") as |category|}}
<h3 class="title box-radio-header"> <Hds::Text::Display class="has-top-padding-m has-bottom-padding-s" @tag="h2" size="400">
{{capitalize category}} {{capitalize category}}
</h3> </Hds::Text::Display>
<div class="box-radio-container"> <div class="flex row-wrap row-gap-8 column-gap-16 has-bottom-padding-m">
{{#each (filter-by "category" category this.mountTypes) as |type|}} {{#each (filter-by "category" category this.mountTypes) as |type|}}
<BoxRadio <SelectableCard
@displayName={{type.displayName}} id={{type.type}}
@type={{type.type}} class="has-top-padding-m has-text-centered small-card"
@glyph={{or type.glyph type.type}} @onClick={{fn @setMountType type.type}}
@groupValue={{this.selection}}
@groupName="mount-type"
@onRadioChange={{mut this.selection}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}} @disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
@tooltipMessage={{if data-test-mount-type={{type.type}}
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt")) >
(concat <Icon @name={{or type.glyph type.type}} @size="24" class="has-bottom-margin-xs" />
type.displayName <Hds::Text::Body @tag="h3" @size="300">
" is part of the Advanced Data Protection module, which is not included in your enterprise license." {{type.displayName}}
) </Hds::Text::Body>
"This secret engine is not included in your license." {{#if (and type.requiredFeature (not (has-feature type.requiredFeature)))}}
}} <Hds::Badge @text="Enterprise" @icon="enterprise" @size="small" />
/> {{/if}}
</SelectableCard>
{{/each}} {{/each}}
</div> </div>
{{/each}} {{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless"> <div class="field is-grouped box is-fullwidth is-bottomless">
<button <Hds::Button @text="Cancel" @color="secondary" @route="vault.cluster.secrets.backends" />
data-test-mount-next
type="button"
class="button is-primary"
{{on "click" (fn @setMountType this.selection)}}
disabled={{not this.selection}}
>
Next
</button>
</div> </div>

View File

@@ -7,7 +7,6 @@ import Component from '@glimmer/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; import { allMethods, methods } from 'vault/helpers/mountable-auth-methods';
import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines';
import { tracked } from '@glimmer/tracking';
/** /**
* *
@@ -19,13 +18,12 @@ import { tracked } from '@glimmer/tracking';
* ```js * ```js
* <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType="secret" /> * <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType="secret" />
* ``` * ```
* @param {CallableFunction} setMountType - function will recieve the mount type string. Should update the model type value * @param {CallableFunction} setMountType - function will receive the mount type string. Should update the model type value
* @param {string} [mountType=auth] - mount type can be `auth` or `secret` * @param {string} [mountType=auth] - mount type can be `auth` or `secret`
*/ */
export default class MountBackendTypeForm extends Component { export default class MountBackendTypeForm extends Component {
@service version; @service version;
@tracked selection;
get secretEngines() { get secretEngines() {
return this.version.isEnterprise ? allEngines() : mountableEngines(); return this.version.isEnterprise ? allEngines() : mountableEngines();

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
/**
* @module SelectableCard
* SelectableCard components are card-like components that display a title, total, subtotal, and anything after the yield.
* They are designed to be used in containers that act as flexbox or css grid containers.
*
* @example
* ```js
* <SelectableCard @cardTitle="Tokens" @total={{totalHttpRequests}} @subText="Total"/>
* ```
* @param {string} [cardTitle] - cardTitle displays the card title.
* @param {number} [total = 0] - the number displayed as the largest text in the component.
* @param {string} [subText] - subText describes the total.
* @param {string} [actionText] - action text link.
* @param {string} [actionTo] - route where link will take you.
* @param {string} [queryParam] - tab for the route the link will take you.
* @param {string} [type] - type used in the link type.
*/
export default class SelectableCard extends Component {
get gridContainer() {
return this.args.gridContainer || false;
}
get total() {
return this.args.total || 0;
}
}

View File

@@ -11,6 +11,7 @@ const ENTERPRISE_AUTH_METHODS = [
value: 'saml', value: 'saml',
type: 'saml', type: 'saml',
category: 'generic', category: 'generic',
glyph: 'saml-color',
}, },
]; ];
@@ -20,6 +21,7 @@ const MOUNTABLE_AUTH_METHODS = [
value: 'alicloud', value: 'alicloud',
type: 'alicloud', type: 'alicloud',
category: 'cloud', category: 'cloud',
glyph: 'alibaba-color',
}, },
{ {
displayName: 'AppRole', displayName: 'AppRole',
@@ -108,6 +110,7 @@ const MOUNTABLE_AUTH_METHODS = [
value: 'userpass', value: 'userpass',
type: 'userpass', type: 'userpass',
category: 'generic', category: 'generic',
glyph: 'users',
}, },
]; ];

View File

@@ -9,6 +9,7 @@ const ENTERPRISE_SECRET_ENGINES = [
{ {
displayName: 'KMIP', displayName: 'KMIP',
type: 'kmip', type: 'kmip',
glyph: 'lock',
engineRoute: 'kmip.scopes.index', engineRoute: 'kmip.scopes.index',
category: 'generic', category: 'generic',
requiredFeature: 'KMIP', requiredFeature: 'KMIP',
@@ -33,6 +34,7 @@ const MOUNTABLE_SECRET_ENGINES = [
{ {
displayName: 'AliCloud', displayName: 'AliCloud',
type: 'alicloud', type: 'alicloud',
glyph: 'alibaba-color',
category: 'cloud', category: 'cloud',
}, },
{ {
@@ -50,6 +52,7 @@ const MOUNTABLE_SECRET_ENGINES = [
{ {
displayName: 'Consul', displayName: 'Consul',
type: 'consul', type: 'consul',
glyph: 'consul-color',
category: 'infra', category: 'infra',
}, },
{ {
@@ -72,17 +75,20 @@ const MOUNTABLE_SECRET_ENGINES = [
{ {
displayName: 'KV', displayName: 'KV',
type: 'kv', type: 'kv',
glyph: 'key-values',
engineRoute: 'kv.list', engineRoute: 'kv.list',
category: 'generic', category: 'generic',
}, },
{ {
displayName: 'Nomad', displayName: 'Nomad',
type: 'nomad', type: 'nomad',
glyph: 'nomad-color',
category: 'infra', category: 'infra',
}, },
{ {
displayName: 'PKI Certificates', displayName: 'PKI Certificates',
type: 'pki', type: 'pki',
glyph: 'certificate',
engineRoute: 'pki.overview', engineRoute: 'pki.overview',
category: 'generic', category: 'generic',
}, },
@@ -94,16 +100,19 @@ const MOUNTABLE_SECRET_ENGINES = [
{ {
displayName: 'SSH', displayName: 'SSH',
type: 'ssh', type: 'ssh',
glyph: 'terminal-screen',
category: 'generic', category: 'generic',
}, },
{ {
displayName: 'Transit', displayName: 'Transit',
type: 'transit', type: 'transit',
glyph: 'swap-horizontal',
category: 'generic', category: 'generic',
}, },
{ {
displayName: 'TOTP', displayName: 'TOTP',
type: 'totp', type: 'totp',
glyph: 'history',
category: 'generic', category: 'generic',
}, },
{ {

View File

@@ -12,7 +12,7 @@
} }
.action-block { .action-block {
@extend .selectable-card; box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
display: grid; display: grid;
padding: $spacing-m $spacing-l; padding: $spacing-m $spacing-l;

View File

@@ -1,67 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.box-radio-container {
display: flex;
flex-wrap: wrap;
}
.title.box-radio-header {
font-size: $size-6;
color: $grey;
margin: $size-7 0 0 0;
}
.box-radio-spacing {
margin: $size-6 $size-3 $size-6 0;
}
.box-radio {
box-sizing: border-box;
flex-basis: 7rem;
width: 7rem;
min-height: 7.5rem;
padding: $size-10 $size-6 $size-10;
flex-direction: column;
justify-content: space-between;
align-items: center;
display: flex;
border-radius: $radius;
box-shadow: $box-shadow;
text-align: center;
color: $grey;
font-weight: $font-weight-semibold;
line-height: 1;
margin: $size-6 $size-3 $size-6 0;
font-size: 12px;
transition: box-shadow ease-in-out $speed;
will-change: box-shadow;
&.is-selected {
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
}
&.is-disabled {
opacity: 0.5;
}
input[type='radio'].radio + label {
border: 1px solid $grey-light;
border-radius: 50%;
cursor: pointer;
display: block;
margin: 1rem auto 0;
height: 1rem;
width: 1rem;
flex-shrink: 0;
flex-grow: 0;
}
input[type='radio'].radio:checked + label {
background: $blue;
border: 1px solid $blue;
box-shadow: inset 0 0 0 0.15rem $white;
}
input[type='radio'].radio:focus + label {
box-shadow: 0 0 10px 1px rgba($blue, 0.4), inset 0 0 0 0.15rem $white;
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
.selectable-card.secondaries { .known-secondaries-card {
grid-column: 2/3; grid-column: 2/3;
grid-row: 1/3; grid-row: 1/3;

View File

@@ -6,12 +6,4 @@
.overview-card { .overview-card {
padding: $spacing-l; padding: $spacing-l;
line-height: initial; line-height: initial;
.title-number {
color: $black;
padding-top: $spacing-s;
font-size: 36px;
font-weight: 500;
line-height: 1.33;
}
} }

View File

@@ -6,7 +6,7 @@
.replication-dashboard { .replication-dashboard {
box-shadow: none; box-shadow: none;
.selectable-card { .replication-summary-card {
line-height: normal; line-height: normal;
&:hover { &:hover {

View File

@@ -1,20 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.replication {
.selectable-card {
display: initial;
line-height: normal;
padding: $spacing-l;
&:hover {
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
}
.card-title {
margin-bottom: 2rem;
}
}
}

View File

@@ -26,13 +26,6 @@
@include until($mobile) { @include until($mobile) {
grid-template-columns: 2fr; grid-template-columns: 2fr;
} }
.selectable-card.is-grid-container {
display: grid;
grid-template-columns: 2fr 0.5fr;
grid-template-rows: 1fr 2fr 0.5fr;
padding: $spacing-l 0 14px $spacing-l; // modify bottom spacing to better align with other cards
}
} }
.selectable-card-container.has-grid.has-three-col-grid { .selectable-card-container.has-grid.has-three-col-grid {

View File

@@ -4,106 +4,20 @@
*/ */
.selectable-card { .selectable-card {
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3); &:hover:not(.disabled),
display: flex; &:focus,
justify-content: space-between; &:focus-visible {
padding: $spacing-l 0 $spacing-l $spacing-l; cursor: pointer;
line-height: 0; box-shadow: var(--token-focus-ring-action-box-shadow);
&.no-flex {
padding: $spacing-l;
display: initial;
line-height: initial;
.title-number {
padding-top: $spacing-s;
} }
.search-label { &.disabled {
margin-bottom: -$spacing-xs; cursor: not-allowed;
} opacity: 0.5;
} }
&:hover { &.small-card {
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle; width: 7rem;
} min-height: 8rem;
> a {
text-decoration: none;
}
.button {
&:disabled {
border-color: $ui-gray-300;
}
}
.card-details {
grid-column-start: 2;
grid-row-start: 3;
align-self: center;
justify-self: right;
padding-right: $spacing-l;
}
.change-metric {
justify-self: right;
padding-right: $spacing-l;
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: 1fr 1fr;
.hs-icon {
color: $grey-light;
align-self: center;
justify-self: right;
}
.amount-change {
align-self: center;
justify-self: center;
font-weight: 500;
}
.item-c {
grid-column: 1 / span 2;
align-self: start;
justify-self: end;
font-weight: $font-weight-semibold;
white-space: nowrap;
@include until($mobile) {
overflow: hidden;
}
}
}
.title-number {
color: $black;
font-size: 36px;
font-weight: 500;
line-height: 1.33;
}
.vlt-table {
max-height: 200px;
overflow-y: auto;
} }
} }
.selectable-card.is-rounded {
border-radius: $radius;
}
.selectable-card.has-error-border {
box-shadow: none;
}
.change-metric-icon.is-decrease {
transform: rotate(135deg);
}
.change-metric-icon.is-increase {
transform: rotate(45deg);
}

View File

@@ -56,7 +56,6 @@
@import './components/autocomplete-input'; @import './components/autocomplete-input';
@import './components/b64-toggle'; @import './components/b64-toggle';
@import './components/box-label'; @import './components/box-label';
@import './components/box-radio';
@import './components/calendar-widget'; @import './components/calendar-widget';
@import './components/cluster-banners'; @import './components/cluster-banners';
@import './components/codemirror'; @import './components/codemirror';
@@ -93,14 +92,12 @@
@import './components/replication-dashboard'; @import './components/replication-dashboard';
@import './components/replication-mode-summary'; @import './components/replication-mode-summary';
@import './components/replication-page'; @import './components/replication-page';
@import './components/replication-primary-card';
@import './components/replication-summary'; @import './components/replication-summary';
@import './components/role-item'; @import './components/role-item';
@import './components/search-select'; @import './components/search-select';
@import './components/selectable-card'; @import './components/selectable-card';
@import './components/selectable-card-container'; @import './components/selectable-card-container';
@import './components/secrets-engines-card'; @import './components/secrets-engines-card';
// action-block extends selectable-card
@import './components/action-block'; @import './components/action-block';
@import './components/shamir-progress'; @import './components/shamir-progress';
@import './components/sidebar'; @import './components/sidebar';

View File

@@ -104,6 +104,28 @@
flex: 50%; flex: 50%;
} }
// moving away from !important, fresh flex styles below
.flex {
display: flex;
// direction
&.row-wrap {
flex-flow: row wrap;
}
// alignment
&.space-between {
justify-content: space-between;
}
&.row-gap-8 {
row-gap: $spacing-xs;
}
&.column-gap-16 {
column-gap: $spacing-m;
}
}
/* Flex Responsive */ /* Flex Responsive */
@media screen and (min-width: 769px), print { @media screen and (min-width: 769px), print {
.is-flex-v-centered-tablet { .is-flex-v-centered-tablet {

View File

@@ -114,10 +114,6 @@
} }
// border-radius // border-radius
.border-radius-2 {
border-radius: $radius;
}
.border-radius-4 { .border-radius-4 {
border-radius: $radius-large; border-radius: $radius-large;
} }

View File

@@ -24,7 +24,7 @@
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
<div class="box is-sideless is-fullwidth is-marginless"> <div class="box is-sideless is-bottomless is-fullwidth is-marginless">
<NamespaceReminder @mode="enable" @noun={{if (eq @mountType "secret") "Secret Engine" "Auth Method"}} /> <NamespaceReminder @mode="enable" @noun={{if (eq @mountType "secret") "Secret Engine" "Auth Method"}} />
<MessageError @errorMessage={{this.errorMessage}} /> <MessageError @errorMessage={{this.errorMessage}} />
{{#if @mountModel.type}} {{#if @mountModel.type}}

View File

@@ -1,36 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{! conditional to check if SelectableCard is apart of a CSS Grid, if yes return grid item class }}
{{#if this.gridContainer}}
<div class="selectable-card is-rounded is-grid-container">
<div class="selectable-card-title">
<h2 class="title-number">{{format-number this.total}}</h2>
<h3 class="title is-5" data-test-selectable-card-title={{@cardTitle}}>{{@cardTitle}}</h3>
<p class="has-text-grey is-size-8">{{@subText}}</p>
</div>
{{yield}}
</div>
{{else}}
<div class="selectable-card is-rounded no-flex">
<div class="is-flex-between is-fullwidth card-details" data-test-selectable-card={{@cardTitle}}>
<h3 class="title is-5">{{@cardTitle}}</h3>
<LinkTo
@route={{@actionTo}}
class="has-icon-right is-ghost is-no-underline has-text-weight-semibold"
@query={{hash itemType=@queryParam}}
data-test-action-text={{@actionText}}
>
{{@actionText}}
{{#if @actionText}}
<Icon @name="chevron-right" />
{{/if}}
</LinkTo>
</div>
<p class="has-text-grey is-size-8">{{@subText}}</p>
<h2 class="title-number">{{format-number this.total}}</h2>
{{yield}}
</div>
{{/if}}

View File

@@ -32,26 +32,30 @@
<div class="box is-fullwidth is-shadowless has-tall-padding"> <div class="box is-fullwidth is-shadowless has-tall-padding">
<div class="selectable-card-container {{if (and (eq this.model.connections 403) (eq this.model.roles 403)) 'one-card'}}"> <div class="selectable-card-container {{if (and (eq this.model.connections 403) (eq this.model.roles 403)) 'one-card'}}">
{{#if this.model.connectionCapabilities.canList}} {{#if this.model.connectionCapabilities.canList}}
<SelectableCard <OverviewCard
@cardTitle="Connections" @cardTitle="Connections"
@total={{if (eq this.model.connections 404) 0 this.model.connections.length}}
@subText="The total number of connections to external databases that you have access to." @subText="The total number of connections to external databases that you have access to."
@actionText="Configure new" @actionText="Configure new"
@actionTo="vault.cluster.secrets.backend.create-root" @actionTo="vault.cluster.secrets.backend.create-root"
@queryParam={{"connection"}} @actionQuery={{hash itemType="connection"}}
@type="role" >
/> <Hds::Text::Display class="has-top-padding-m" @tag="h2" @size="500">
{{format-number (if (eq this.model.connections 404) 0 this.model.connections.length)}}
</Hds::Text::Display>
</OverviewCard>
{{/if}} {{/if}}
{{#if (or this.model.roleCapabilities.canList this.model.staticRoleCapabilities.canList)}} {{#if (or this.model.roleCapabilities.canList this.model.staticRoleCapabilities.canList)}}
<SelectableCard <OverviewCard
@cardTitle="Roles" @cardTitle="Roles"
@total={{if (eq this.model.roles 404) 0 this.model.roles.length}} @subText="The total number of roles configured that you have permissions to list."
{{! TODO: Messaging needs massaging }}
@subText="The total number of roles that have been set up that you can list."
@actionText="Create new" @actionText="Create new"
@actionTo="vault.cluster.secrets.backend.create-root" @actionTo="vault.cluster.secrets.backend.create-root"
@queryParam={{"role"}} @actionQuery={{hash itemType="role"}}
/> >
<Hds::Text::Display class="has-top-padding-m" @tag="h2" @size="500">
{{format-number (if (eq this.model.roles 404) 0 this.model.roles.length)}}
</Hds::Text::Display>
</OverviewCard>
{{/if}} {{/if}}
<GetCredentialsCard <GetCredentialsCard
@title="Get Credentials" @title="Get Credentials"

View File

@@ -1,57 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if @disabled}}
<div class="box-radio-spacing">
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
<T.Trigger @tabindex="-1">
<label
for={{@type}}
class="box-radio is-disabled is-marginless"
data-test-mount-type-radio
data-test-mount-type={{@type}}
>
<Icon @name={{@glyph}} @size="24" class="has-text-grey-light" />
{{@displayName}}
<RadioButton
id={{@type}}
name={{@groupName}}
class="radio"
@disabled={{@disabled}}
@value={{@type}}
@groupValue={{@groupValue}}
@onChange={{@onRadioChange}}
/>
</label>
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box">
{{@tooltipMessage}}
</div>
</T.Content>
</ToolTip>
</div>
{{else}}
<div class="box-radio-spacing">
<label
for={{@type}}
class="box-radio is-marginless {{if (eq @groupValue @type) ' is-selected'}}"
data-test-mount-type-radio
data-test-mount-type={{@type}}
>
<Icon @name={{@glyph}} @size="24" class="has-text-grey-light" />
{{@displayName}}
<RadioButton
id={{@type}}
name={{@groupName}}
class="radio"
@disabled={{@disabled}}
@value={{@type}}
@groupValue={{@mountType}}
@onChange={{@onRadioChange}}
/>
</label>
</div>
{{/if}}

View File

@@ -1,30 +0,0 @@
import Component from '@glimmer/component';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* @module BoxRadio
* BoxRadio components are used to display options for a radio selection.
*
* @example
* ```js
* <BoxRadio @displayName="Catahoula Leopard" @type="catahoula" @glyph="dog" @groupValue="labrador" @groupName="my-favorite-dog" @onRadioChange={{handleRadioChange}} />
* ```
* @param {string} displayName - This is the string that will show on the box radio option.
* @param {string} type - type is the key that the radio input will be identified by. Please use a value without spaces.
* @param {string} glyph - glyph is the name of the icon that will be used in the box
* @param {string} groupValue - The key of the radio option that is currently selected for this radio group
* @param {string} groupName - The name (key) of the group that this radio option belongs to
* @param {function} onRadioChange - This callback will trigger when the radio option is selected (if enabled)
* @param {boolean} [disabled=false] - This parameter controls whether the radio option is selectable. If not, it will be grayed out and show a tooltip.
* @param {string} [tooltipMessage=default] - The message that shows in the tooltip if the radio option is disabled
*/
export default class BoxRadio extends Component {
get tooltipMessage() {
return this.args.tooltipMessage || 'This option is not available to you at this time.';
}
}

View File

@@ -43,6 +43,6 @@ export default class Icon extends Component {
} }
get hsIconClass() { get hsIconClass() {
return this.size === '24' ? 'hs-icon-xl' : 'hs-icon-l'; return this.size === '24' ? 'hs-icon-xlm' : 'hs-icon-l';
} }
} }

View File

@@ -6,22 +6,21 @@
<Hds::Card::Container <Hds::Card::Container
@level="mid" @level="mid"
@hasBorder={{true}} @hasBorder={{true}}
class="overview-card border-radius-2" class="overview-card"
data-test-overview-card-container={{@cardTitle}} data-test-overview-card-container={{@cardTitle}}
...attributes ...attributes
> >
<div class="is-flex-between" data-test-overview-card={{@cardTitle}}> <div class="flex row-wrap space-between has-bottom-margin-m" data-test-overview-card={{@cardTitle}}>
<h3 class="title is-5">{{@cardTitle}}</h3> <h3 class="has-text-weight-bold is-size-5">{{@cardTitle}}</h3>
{{#if @actionText}} {{#if @actionText}}
<LinkTo <Hds::Link::Standalone
class="has-icon-right is-ghost is-no-underline has-text-weight-semibold" @icon={{or @actionIcon "chevron-right"}}
@iconPosition="trailing"
@text={{@actionText}}
@route={{@actionTo}} @route={{@actionTo}}
@query={{hash @actionQuery}} @query={{@actionQuery}}
data-test-action-text={{@actionText}} data-test-action-text={{@actionText}}
> />
{{@actionText}}
<Icon @name="chevron-right" />
</LinkTo>
{{/if}} {{/if}}
</div> </div>
<p class="has-text-grey is-size-8 {{unless @actionText 'has-top-margin-s'}}">{{@subText}}</p> <p class="has-text-grey is-size-8 {{unless @actionText 'has-top-margin-s'}}">{{@subText}}</p>

View File

@@ -0,0 +1,16 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<Hds::Card::Container
tabindex={{unless @disabled "0"}}
class="selectable-card {{if @disabled 'disabled'}}"
@level={{if @disabled "base" "mid"}}
@hasBorder={{true}}
{{on "click" @onClick}}
{{on "keypress" @onClick}}
...attributes
>
{{yield}}
</Hds::Card::Container>

View File

@@ -3,8 +3,9 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div <Hds::Card::Container
class="selectable-card is-rounded card-container {{if this.hasErrorClass 'has-error-border'}}" @hasBorder={{true}}
class="has-padding-m card-container {{if this.hasErrorClass 'has-error-border'}}"
data-test-replication-secondary-card data-test-replication-secondary-card
> >
{{! Check if Status or Primary Cluster Card }} {{! Check if Status or Primary Cluster Card }}
@@ -119,4 +120,4 @@
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
</div> </Hds::Card::Container>

View File

@@ -3,8 +3,9 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div <Hds::Card::Container
class="selectable-card is-rounded card-container summary {{if this.hasErrorClass 'has-error-border'}}" @hasBorder={{true}}
class="replication-summary-card card-container summary {{if this.hasErrorClass 'has-error-border'}}"
data-test-replication-summary-card data-test-replication-summary-card
> >
{{! Check if DR or Performance Card }} {{! Check if DR or Performance Card }}
@@ -59,4 +60,4 @@
<div><code class="is-word-break has-text-black">{{this.merkleRootPerformance}}</code></div> <div><code class="is-word-break has-text-black">{{this.merkleRootPerformance}}</code></div>
</div> </div>
{{/if}} {{/if}}
</div> </Hds::Card::Container>

View File

@@ -3,4 +3,4 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
export { default } from 'core/components/box-radio'; export { default } from 'core/components/selectable-card';

View File

@@ -5,7 +5,7 @@
<div class="is-grid has-top-margin-l grid-2-columns grid-gap-2"> <div class="is-grid has-top-margin-l grid-2-columns grid-gap-2">
<div> <div>
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l is-flex-half border-radius-2"> <Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l is-flex-half">
<div class="is-flex-between"> <div class="is-flex-between">
<h3 class="is-size-5 has-text-weight-semibold">All accounts</h3> <h3 class="is-size-5 has-text-weight-semibold">All accounts</h3>
{{#if @library.canCheckOut}} {{#if @library.canCheckOut}}

View File

@@ -10,7 +10,9 @@
@actionText="View issuers" @actionText="View issuers"
@actionTo="issuers" @actionTo="issuers"
> >
<h2 class="title-number">{{format-number (if (eq @issuers 404) 0 @issuers.length)}}</h2> <Hds::Text::Display @tag="h2" @size="500">
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
</Hds::Text::Display>
</OverviewCard> </OverviewCard>
{{#if (not-eq @roles 403)}} {{#if (not-eq @roles 403)}}
<OverviewCard <OverviewCard
@@ -19,7 +21,9 @@
@actionText="View roles" @actionText="View roles"
@actionTo="roles" @actionTo="roles"
> >
<h2 class="title-number">{{format-number (if (eq @roles 404) 0 @roles.length)}}</h2> <Hds::Text::Display @tag="h2" @size="500">
{{format-number (if (eq @roles 404) 0 @roles.length)}}
</Hds::Text::Display>
</OverviewCard> </OverviewCard>
{{/if}} {{/if}}
<OverviewCard @cardTitle="Issue certificate" @subText="Begin issuing a certificate by choosing a role."> <OverviewCard @cardTitle="Issue certificate" @subText="Begin issuing a certificate by choosing a role.">

View File

@@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div class="selectable-card is-rounded secondaries"> <Hds::Card::Container
@hasBorder={{true}}
class="has-padding-m known-secondaries-card {{if this.hasErrorClass 'has-error-border'}}"
>
<div class="level"> <div class="level">
<h3 class="card-title title is-5">{{this.replicationAttrs.secondaries.length}} Known secondaries</h3> <h3 class="card-title title is-5">{{this.replicationAttrs.secondaries.length}} Known secondaries</h3>
<ToolbarLink @route="mode.secondaries" @model={{this.cluster.replicationMode}} data-test-manage-link> <ToolbarLink @route="mode.secondaries" @model={{this.cluster.replicationMode}} data-test-manage-link>
@@ -25,4 +28,4 @@
Add secondary Add secondary
</LinkTo> </LinkTo>
{{/if}} {{/if}}
</div> </Hds::Card::Container>

View File

@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div class="selectable-card is-rounded {{dasherize this.title}} {{if this.hasError 'has-error-border'}}"> <Hds::Card::Container @hasBorder={{true}} class="has-padding-m {{if this.hasErrorClass 'has-error-border'}}">
<h3 class="card-title title is-5">{{this.title}}</h3> <h3 class="card-title title is-5">{{this.title}}</h3>
<div> <div>
{{#if this.hasError}} {{#if this.hasError}}
@@ -23,4 +23,4 @@
{{/if}} {{/if}}
</h3> </h3>
</div> </div>
</div> </Hds::Card::Container>

View File

@@ -1,16 +1,16 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/> <rect x="1" y="1" width="10" height="10" rx="1" fill="white" stroke="#000000" stroke-width="2"/>
<rect x="13" y="13" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/> <rect x="13" y="13" width="10" height="10" rx="1" fill="white" stroke="#000000" stroke-width="2"/>
<rect x="19" y="6" width="2" height="2" fill="#BAC1CC"/> <rect x="19" y="6" width="2" height="2" fill="#000000"/>
<rect x="19" y="9" width="2" height="2" fill="#BAC1CC"/> <rect x="19" y="9" width="2" height="2" fill="#000000"/>
<rect x="11" y="21" width="2" height="2" transform="rotate(-180 11 21)" fill="#BAC1CC"/> <rect x="11" y="21" width="2" height="2" transform="rotate(-180 11 21)" fill="#000000"/>
<rect x="8" y="21" width="2" height="2" transform="rotate(-180 8 21)" fill="#BAC1CC"/> <rect x="8" y="21" width="2" height="2" transform="rotate(-180 8 21)" fill="#000000"/>
<rect x="5" y="21" width="2" height="2" transform="rotate(-180 5 21)" fill="#BAC1CC"/> <rect x="5" y="21" width="2" height="2" transform="rotate(-180 5 21)" fill="#000000"/>
<rect x="5" y="18" width="2" height="2" transform="rotate(-180 5 18)" fill="#BAC1CC"/> <rect x="5" y="18" width="2" height="2" transform="rotate(-180 5 18)" fill="#000000"/>
<rect x="5" y="15" width="2" height="2" transform="rotate(-180 5 15)" fill="#BAC1CC"/> <rect x="5" y="15" width="2" height="2" transform="rotate(-180 5 15)" fill="#000000"/>
<path d="M7.02393 9H8.78174L6.98438 2.65869H5.01123L3.21826 9H4.81787L5.12109 7.60693H6.72949L7.02393 9ZM5.89453 4.08691H5.97803L6.479 6.44678H5.37598L5.89453 4.08691Z" fill="#BAC1CC"/> <path d="M7.02393 9H8.78174L6.98438 2.65869H5.01123L3.21826 9H4.81787L5.12109 7.60693H6.72949L7.02393 9ZM5.89453 4.08691H5.97803L6.479 6.44678H5.37598L5.89453 4.08691Z" fill="#000000"/>
<path d="M15.4248 17.3833L16.9761 18.1743L15.4248 18.9653L16.0269 20.0068L17.4902 19.062L17.3979 20.8022H18.6021L18.5098 19.0576L19.9731 20.0112L20.5752 18.9653L19.0195 18.1743L20.5752 17.3833L19.9731 16.3418L18.5142 17.291L18.6021 15.5464H17.3979L17.4858 17.291L16.0269 16.3374L15.4248 17.3833Z" fill="#BAC1CC"/> <path d="M15.4248 17.3833L16.9761 18.1743L15.4248 18.9653L16.0269 20.0068L17.4902 19.062L17.3979 20.8022H18.6021L18.5098 19.0576L19.9731 20.0112L20.5752 18.9653L19.0195 18.1743L20.5752 17.3833L19.9731 16.3418L18.5142 17.291L18.6021 15.5464H17.3979L17.4858 17.291L16.0269 16.3374L15.4248 17.3833Z" fill="#000000"/>
<rect x="13" y="3" width="2" height="2" fill="#BAC1CC"/> <rect x="13" y="3" width="2" height="2" fill="#000000"/>
<rect x="16" y="3" width="2" height="2" fill="#BAC1CC"/> <rect x="16" y="3" width="2" height="2" fill="#000000"/>
<rect x="19" y="3" width="2" height="2" fill="#BAC1CC"/> <rect x="19" y="3" width="2" height="2" fill="#000000"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -99,7 +99,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type).submit(); await mountSecrets.path(engine.type).submit();
assert.strictEqual( assert.strictEqual(
currentRouteName(), currentRouteName(),
`vault.cluster.secrets.backend.${engine.engineRoute}`, `vault.cluster.secrets.backend.${engine.engineRoute}`,

View File

@@ -28,7 +28,7 @@ module('Acceptance | Enterprise | keymgmt', function (hooks) {
await runCmd([`delete sys/mounts/${engine.type}`]); await runCmd([`delete sys/mounts/${engine.type}`]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type); await mountSecrets.path(engine.type);
await mountSecrets.submit(); await mountSecrets.submit();
assert.strictEqual( assert.strictEqual(

View File

@@ -75,7 +75,7 @@ module('Acceptance | Enterprise | Transform secrets', function (hooks) {
await runCmd([`delete sys/mounts/${engine.type}`]); await runCmd([`delete sys/mounts/${engine.type}`]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type); await mountSecrets.path(engine.type);
await mountSecrets.submit(); await mountSecrets.submit();
assert.strictEqual( assert.strictEqual(

View File

@@ -26,7 +26,7 @@ module('Acceptance | alicloud/enable', function (hooks) {
await settled(); await settled();
await mountSecrets.selectType('alicloud'); await mountSecrets.selectType('alicloud');
await settled(); await settled();
await mountSecrets.next().path(enginePath).submit(); await mountSecrets.path(enginePath).submit();
await settled(); await settled();
assert.strictEqual( assert.strictEqual(

View File

@@ -478,11 +478,11 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert.dom('[data-test-secret-create]').doesNotExist('Add role button does not show due to permissions'); assert.dom('[data-test-secret-create]').doesNotExist('Add role button does not show due to permissions');
assert.dom('[data-test-edit-link]').doesNotExist('Edit button does not show due to permissions'); assert.dom('[data-test-edit-link]').doesNotExist('Edit button does not show due to permissions');
await visit(`/vault/secrets/${backend}/overview`); await visit(`/vault/secrets/${backend}/overview`);
assert.dom('[data-test-selectable-card="Connections"]').exists('Connections card exists on overview'); assert.dom('[data-test-overview-card="Connections"]').exists('Connections card exists on overview');
assert assert
.dom('[data-test-selectable-card="Roles"]') .dom('[data-test-overview-card="Roles"]')
.doesNotExist('Roles card does not exist on overview w/ policy'); .doesNotExist('Roles card does not exist on overview w/ policy');
assert.dom('.title-number').hasText('1', 'Lists the correct number of connections'); assert.dom('.overview-card h2').hasText('1', 'Lists the correct number of connections');
// confirm get credentials card is an option to select. Regression bug. // confirm get credentials card is an option to select. Regression bug.
await typeIn('.ember-text-field', 'blah'); await typeIn('.ember-text-field', 'blah');
assert.dom('[data-test-get-credentials]').isEnabled(); assert.dom('[data-test-get-credentials]').isEnabled();
@@ -570,9 +570,8 @@ module('Acceptance | secrets/database/*', function (hooks) {
.dom('[data-test-secret-list-tab="Roles"]') .dom('[data-test-secret-list-tab="Roles"]')
.doesNotExist(`does not show the roles tab because it does not have permissions`); .doesNotExist(`does not show the roles tab because it does not have permissions`);
assert assert
.dom('[data-test-selectable-card="Connections"]') .dom('[data-test-overview-card="Connections"]')
.exists({ count: 1 }, 'renders only the connection card'); .exists({ count: 1 }, 'renders only the connection card');
await click('[data-test-action-text="Configure new"]'); await click('[data-test-action-text="Configure new"]');
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create?itemType=connection`); assert.strictEqual(currentURL(), `/vault/secrets/${backend}/create?itemType=connection`);
}); });

View File

@@ -26,7 +26,7 @@ module('Acceptance | gcpkms/enable', function (hooks) {
await mountSecrets.visit(); await mountSecrets.visit();
await settled(); await settled();
await mountSecrets.selectType('gcpkms'); await mountSecrets.selectType('gcpkms');
await mountSecrets.next().path(enginePath).submit(); await mountSecrets.path(enginePath).submit();
await settled(); await settled();
assert.strictEqual( assert.strictEqual(
currentRouteName(), currentRouteName(),

View File

@@ -51,8 +51,6 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
await mountSecrets.visit(); await mountSecrets.visit();
await click('[data-test-mount-type="kv"]'); await click('[data-test-mount-type="kv"]');
await click('[data-test-mount-next]');
await fillIn('[data-test-input="path"]', enginePath); await fillIn('[data-test-input="path"]', enginePath);
await fillIn('[data-test-input="maxVersions"]', maxVersion); await fillIn('[data-test-input="maxVersions"]', maxVersion);
await click('[data-test-input="casRequired"]'); await click('[data-test-input="casRequired"]');
@@ -141,7 +139,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) {
// mount version 1 engine // mount version 1 engine
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType('kv'); await mountSecrets.selectType('kv');
await mountSecrets.next().path(this.backend).toggleOptions().version(1).submit(); await mountSecrets.path(this.backend).toggleOptions().version(1).submit();
}); });
hooks.afterEach(async function () { hooks.afterEach(async function () {
await consoleComponent.runCommands([`delete sys/mounts/${this.backend}`]); await consoleComponent.runCommands([`delete sys/mounts/${this.backend}`]);

View File

@@ -33,7 +33,6 @@ module('Acceptance | ldap | overview', function (hooks) {
await visit('/vault/secrets'); await visit('/vault/secrets');
await click('[data-test-enable-engine]'); await click('[data-test-enable-engine]');
await click('[data-test-mount-type="ldap"]'); await click('[data-test-mount-type="ldap"]');
await click('[data-test-mount-next]');
await fillIn('[data-test-input="path"]', 'ldap-test'); await fillIn('[data-test-input="path"]', 'ldap-test');
await click('[data-test-mount-submit]'); await click('[data-test-mount-submit]');
assert.true(isURL('overview'), 'Transitions to ldap overview route on mount success'); assert.true(isURL('overview'), 'Transitions to ldap overview route on mount success');

View File

@@ -31,7 +31,6 @@ module('Acceptance | settings', function (hooks) {
await mountSecrets.selectType(type); await mountSecrets.selectType(type);
await mountSecrets await mountSecrets
.next()
.path(path) .path(path)
.toggleOptions() .toggleOptions()
.enableDefaultTtl() .enableDefaultTtl()

View File

@@ -45,7 +45,6 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
await page.selectType('kv'); await page.selectType('kv');
await page await page
.next()
.path(path) .path(path)
.toggleOptions() .toggleOptions()
.enableDefaultTtl() .enableDefaultTtl()
@@ -70,7 +69,6 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
await page.selectType('kv'); await page.selectType('kv');
await page await page
.next()
.path(path) .path(path)
.toggleOptions() .toggleOptions()
.enableDefaultTtl() .enableDefaultTtl()
@@ -91,7 +89,6 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
await page.visit(); await page.visit();
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
await page.selectType('pki'); await page.selectType('pki');
await page.next();
assert.dom('[data-test-input="maxLeaseTtl"]').exists(); assert.dom('[data-test-input="maxLeaseTtl"]').exists();
assert assert
.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]') .dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]')
@@ -102,7 +99,6 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
// Go back and choose a different type // Go back and choose a different type
await page.back(); await page.back();
await page.selectType('database'); await page.selectType('database');
await page.next();
assert.dom('[data-test-input="maxLeaseTtl"]').exists('3650'); assert.dom('[data-test-input="maxLeaseTtl"]').exists('3650');
assert assert
.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]') .dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]')
@@ -124,12 +120,12 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
await page.selectType('kv'); await page.selectType('kv');
await page.next().path(path).submit(); await page.path(path).submit();
await page.secretList(); await page.secretList();
await settled(); await settled();
await page.enableEngine(); await page.enableEngine();
await page.selectType('kv'); await page.selectType('kv');
await page.next().path(path).submit(); await page.path(path).submit();
assert.dom('[data-test-message-error-description]').containsText(`path is already in use at ${path}`); assert.dom('[data-test-message-error-description]').containsText(`path is already in use at ${path}`);
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend'); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
@@ -174,7 +170,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
// create the engine // create the engine
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType('kv'); await mountSecrets.selectType('kv');
await mountSecrets.next().path(enginePath).setMaxVersion(101).submit(); await mountSecrets.path(enginePath).setMaxVersion(101).submit();
await settled(); await settled();
assert assert
.dom('[data-test-flash-message]') .dom('[data-test-flash-message]')
@@ -202,7 +198,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type).submit(); await mountSecrets.path(engine.type).submit();
assert.strictEqual( assert.strictEqual(
currentRouteName(), currentRouteName(),
`vault.cluster.secrets.backend.${engine.engineRoute}`, `vault.cluster.secrets.backend.${engine.engineRoute}`,
@@ -229,7 +225,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type); await mountSecrets.path(engine.type);
if (engine.type === 'kv') { if (engine.type === 'kv') {
await mountSecrets.toggleOptions().version(1); await mountSecrets.toggleOptions().version(1);
} }
@@ -258,7 +254,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType(engine.type); await mountSecrets.selectType(engine.type);
await mountSecrets.next().path(engine.type).submit(); await mountSecrets.path(engine.type).submit();
assert.strictEqual( assert.strictEqual(
currentRouteName(), currentRouteName(),
@@ -277,7 +273,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType('kv'); await mountSecrets.selectType('kv');
await mountSecrets.next().path(v2).submit(); await mountSecrets.path(v2).submit();
assert.strictEqual(currentURL(), `/vault/secrets/${v2}/kv/list`, `${v2} navigates to list url`); assert.strictEqual(currentURL(), `/vault/secrets/${v2}/kv/list`, `${v2} navigates to list url`);
assert.strictEqual( assert.strictEqual(
@@ -293,7 +289,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
]); ]);
await mountSecrets.visit(); await mountSecrets.visit();
await mountSecrets.selectType('kv'); await mountSecrets.selectType('kv');
await mountSecrets.next().path(v1).toggleOptions().version(1).submit(); await mountSecrets.path(v1).toggleOptions().version(1).submit();
assert.strictEqual(currentURL(), `/vault/secrets/${v1}/list`, `${v1} navigates to list url`); assert.strictEqual(currentURL(), `/vault/secrets/${v1}/list`, `${v1} navigates to list url`);
assert.strictEqual( assert.strictEqual(

View File

@@ -4,11 +4,11 @@
*/ */
export const SELECTORS = { export const SELECTORS = {
rolesCardTitle: '[data-test-overview-card="Roles"] .title', rolesCardTitle: '[data-test-overview-card="Roles"] h3',
rolesCardSubTitle: '[data-test-overview-card-container="Roles"] p', rolesCardSubTitle: '[data-test-overview-card-container="Roles"] p',
rolesCardLink: '[data-test-overview-card="Roles"] a', rolesCardLink: '[data-test-overview-card="Roles"] a',
rolesCardNumRoles: '[data-test-roles-card-overview-num]', rolesCardNumRoles: '[data-test-roles-card-overview-num]',
generateCredentialsCardTitle: '[data-test-overview-card="Generate credentials"] .title', generateCredentialsCardTitle: '[data-test-overview-card="Generate credentials"] h3',
generateCredentialsCardSubTitle: '[data-test-overview-card-container="Generate credentials"] p', generateCredentialsCardSubTitle: '[data-test-overview-card-container="Generate credentials"] p',
generateCredentialsCardButton: '[data-test-generate-credential-button]', generateCredentialsCardButton: '[data-test-generate-credential-button]',
emptyStateTitle: '.empty-state .empty-state-title', emptyStateTitle: '.empty-state .empty-state-title',

View File

@@ -7,11 +7,11 @@ export const SELECTORS = {
issuersCardTitle: '[data-test-overview-card-container="Issuers"] h3', issuersCardTitle: '[data-test-overview-card-container="Issuers"] h3',
issuersCardSubtitle: '[data-test-overview-card-container="Issuers"] p', issuersCardSubtitle: '[data-test-overview-card-container="Issuers"] p',
issuersCardLink: '[data-test-overview-card-container="Issuers"] a', issuersCardLink: '[data-test-overview-card-container="Issuers"] a',
issuersCardOverviewNum: '[data-test-overview-card-container="Issuers"] .title-number', issuersCardOverviewNum: '[data-test-overview-card-container="Issuers"] h2',
rolesCardTitle: '[data-test-overview-card-container="Roles"] h3', rolesCardTitle: '[data-test-overview-card-container="Roles"] h3',
rolesCardSubtitle: '[data-test-overview-card-container="Roles"] p', rolesCardSubtitle: '[data-test-overview-card-container="Roles"] p',
rolesCardLink: '[data-test-overview-card-container="Roles"] a', rolesCardLink: '[data-test-overview-card-container="Roles"] a',
rolesCardOverviewNum: '[data-test-overview-card-container="Roles"] .title-number', rolesCardOverviewNum: '[data-test-overview-card-container="Roles"] h2',
issueCertificate: '[data-test-overview-card-container="Issue certificate"] h3', issueCertificate: '[data-test-overview-card-container="Issue certificate"] h3',
issueCertificateInput: '[data-test-issue-certificate-input]', issueCertificateInput: '[data-test-issue-certificate-input]',
issueCertificatePowerSearch: '[data-test-issue-certificate-input] span', issueCertificatePowerSearch: '[data-test-issue-certificate-input] span',

View File

@@ -1,55 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import sinon from 'sinon';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | box-radio', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('type', 'aws');
this.set('displayName', 'An Option');
this.set('mountType', '');
this.set('disabled', false);
});
test('it renders', async function (assert) {
const spy = sinon.spy();
this.set('onRadioChange', spy);
await render(hbs`<BoxRadio
@type={{this.type}}
@glyph={{this.type}}
@displayName={{this.displayName}}
@onRadioChange={{this.onRadioChange}}
@disabled={{this.disabled}}
/>`);
assert.dom(this.element).hasText('An Option', 'shows the display name of the option');
assert.dom('.tooltip').doesNotExist('tooltip does not exist when disabled is false');
await click('[data-test-mount-type="aws"]');
assert.ok(spy.calledOnce, 'calls the radio change function when option clicked');
});
test('it renders correctly when disabled', async function (assert) {
const spy = sinon.spy();
this.set('onRadioChange', spy);
await render(hbs`<BoxRadio
@type={{this.type}}
@glyph={{this.type}}
@displayName={{this.displayName}}
@onRadioChange={{this.onRadioChange}}
@disabled={{true}}
/>`);
assert.dom(this.element).hasText('An Option', 'shows the display name of the option');
assert.dom('.ember-basic-dropdown-trigger').exists('tooltip exists');
await click('[data-test-mount-type="aws"]');
assert.ok(spy.notCalled, 'does not call the radio change function when option is clicked');
});
});

View File

@@ -27,7 +27,7 @@ module('Integration | Component | icon', function (hooks) {
assert.dom('.al').hasAttribute('aria-label', 'Testing', 'renders aria-label'); assert.dom('.al').hasAttribute('aria-label', 'Testing', 'renders aria-label');
await render(hbs`<Icon @name="vault-logo" @size="24"/>`); await render(hbs`<Icon @name="vault-logo" @size="24"/>`);
assert.dom('.hs-icon').hasClass('hs-icon-xl', 'adds the larger size class'); assert.dom('.hs-icon').hasClass('hs-icon-xlm', 'adds the larger size class');
const promise = waitForError(); const promise = waitForError();
render(hbs`<Icon @name="vault-logo" @size="12"/>`); render(hbs`<Icon @name="vault-logo" @size="12"/>`);

View File

@@ -60,11 +60,9 @@ module('Integration | Component | mount backend form', function (hooks) {
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await component.selectType('aws'); await component.selectType('aws');
await component.next();
assert.strictEqual(component.pathValue, 'aws', 'sets the value of the type'); assert.strictEqual(component.pathValue, 'aws', 'sets the value of the type');
await component.back(); await component.back();
await component.selectType('approle'); await component.selectType('approle');
await component.next();
assert.strictEqual(component.pathValue, 'approle', 'updates the value of the type'); assert.strictEqual(component.pathValue, 'approle', 'updates the value of the type');
}); });
@@ -73,7 +71,6 @@ module('Integration | Component | mount backend form', function (hooks) {
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await component.selectType('approle'); await component.selectType('approle');
await component.next();
assert.strictEqual(this.model.type, 'approle', 'Updates type on model'); assert.strictEqual(this.model.type, 'approle', 'Updates type on model');
assert.strictEqual(component.pathValue, 'approle', 'defaults to approle (first in the list)'); assert.strictEqual(component.pathValue, 'approle', 'defaults to approle (first in the list)');
await component.path('newpath'); await component.path('newpath');
@@ -82,7 +79,6 @@ module('Integration | Component | mount backend form', function (hooks) {
assert.strictEqual(this.model.type, '', 'Clears type on back'); assert.strictEqual(this.model.type, '', 'Clears type on back');
assert.strictEqual(this.model.path, 'newpath', 'Path is still newPath'); assert.strictEqual(this.model.path, 'newpath', 'Path is still newPath');
await component.selectType('aws'); await component.selectType('aws');
await component.next();
assert.strictEqual(this.model.type, 'aws', 'Updates type on model'); assert.strictEqual(this.model.type, 'aws', 'Updates type on model');
assert.strictEqual(component.pathValue, 'newpath', 'keeps custom path value'); assert.strictEqual(component.pathValue, 'newpath', 'keeps custom path value');
}); });
@@ -92,7 +88,6 @@ module('Integration | Component | mount backend form', function (hooks) {
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await component.selectType('github'); await component.selectType('github');
await component.next();
await component.toggleOptions(); await component.toggleOptions();
assert assert
.dom('[data-test-input="config.tokenType"]') .dom('[data-test-input="config.tokenType"]')
@@ -148,11 +143,9 @@ module('Integration | Component | mount backend form', function (hooks) {
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await component.selectType('azure'); await component.selectType('azure');
await component.next();
assert.strictEqual(component.pathValue, 'azure', 'sets the value of the type'); assert.strictEqual(component.pathValue, 'azure', 'sets the value of the type');
await component.back(); await component.back();
await component.selectType('nomad'); await component.selectType('nomad');
await component.next();
assert.strictEqual(component.pathValue, 'nomad', 'updates the value of the type'); assert.strictEqual(component.pathValue, 'nomad', 'updates the value of the type');
}); });
@@ -161,7 +154,6 @@ module('Integration | Component | mount backend form', function (hooks) {
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await component.selectType('kv'); await component.selectType('kv');
await component.next();
assert.strictEqual(this.model.type, 'kv', 'Updates type on model'); assert.strictEqual(this.model.type, 'kv', 'Updates type on model');
assert.strictEqual(component.pathValue, 'kv', 'path matches mount type'); assert.strictEqual(component.pathValue, 'kv', 'path matches mount type');
await component.path('newpath'); await component.path('newpath');
@@ -170,7 +162,6 @@ module('Integration | Component | mount backend form', function (hooks) {
assert.strictEqual(this.model.type, '', 'Clears type on back'); assert.strictEqual(this.model.type, '', 'Clears type on back');
assert.strictEqual(this.model.path, 'newpath', 'path is still newpath'); assert.strictEqual(this.model.path, 'newpath', 'path is still newpath');
await component.selectType('ssh'); await component.selectType('ssh');
await component.next();
assert.strictEqual(this.model.type, 'ssh', 'Updates type on model'); assert.strictEqual(this.model.type, 'ssh', 'Updates type on model');
assert.strictEqual(component.pathValue, 'newpath', 'path stays the same'); assert.strictEqual(component.pathValue, 'newpath', 'path stays the same');
}); });

View File

@@ -23,7 +23,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) {
this.setType = sinon.spy(); this.setType = sinon.spy();
}); });
test('it calls secrets setMountType only on next click', async function (assert) { test('it calls secrets setMountType when type is selected', async function (assert) {
const spy = sinon.spy(); const spy = sinon.spy();
this.set('setType', spy); this.set('setType', spy);
await render(hbs`<MountBackend::TypeForm @mountType="secret" @setMountType={{this.setType}} />`); await render(hbs`<MountBackend::TypeForm @mountType="secret" @setMountType={{this.setType}} />`);
@@ -31,17 +31,11 @@ module('Integration | Component | mount-backend/type-form', function (hooks) {
assert assert
.dom('[data-test-mount-type]') .dom('[data-test-mount-type]')
.exists({ count: secretTypes.length }, 'Renders all mountable engines'); .exists({ count: secretTypes.length }, 'Renders all mountable engines');
await click(`[data-test-mount-type="nomad"]`);
assert.dom(`[data-test-mount-type="nomad"] input`).isChecked(`ssh is checked`);
assert.ok(spy.notCalled, 'callback not called');
await click(`[data-test-mount-type="ssh"]`); await click(`[data-test-mount-type="ssh"]`);
assert.dom(`[data-test-mount-type="ssh"] input`).isChecked(`ssh is checked`);
assert.ok(spy.notCalled, 'callback not called');
await click('[data-test-mount-next]');
assert.ok(spy.calledOnceWith('ssh')); assert.ok(spy.calledOnceWith('ssh'));
}); });
test('it calls auth setMountType only on next click', async function (assert) { test('it calls auth setMountType when type is selected', async function (assert) {
const spy = sinon.spy(); const spy = sinon.spy();
this.set('setType', spy); this.set('setType', spy);
await render(hbs`<MountBackend::TypeForm @setMountType={{this.setType}} />`); await render(hbs`<MountBackend::TypeForm @setMountType={{this.setType}} />`);
@@ -50,13 +44,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) {
.dom('[data-test-mount-type]') .dom('[data-test-mount-type]')
.exists({ count: authTypes.length }, 'Renders all mountable auth methods'); .exists({ count: authTypes.length }, 'Renders all mountable auth methods');
await click(`[data-test-mount-type="okta"]`); await click(`[data-test-mount-type="okta"]`);
assert.dom(`[data-test-mount-type="okta"] input`).isChecked(`ssh is checked`); assert.ok(spy.calledOnceWith('okta'));
assert.ok(spy.notCalled, 'callback not called');
await click(`[data-test-mount-type="github"]`);
assert.dom(`[data-test-mount-type="github"] input`).isChecked(`ssh is checked`);
assert.ok(spy.notCalled, 'callback not called');
await click('[data-test-mount-next]');
assert.ok(spy.calledOnceWith('github'));
}); });
module('Enterprise', function (hooks) { module('Enterprise', function (hooks) {

View File

@@ -23,7 +23,7 @@ module('Integration | Component overview-card', function (hooks) {
test('it returns card title, ', async function (assert) { test('it returns card title, ', async function (assert) {
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}}/>`); await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}}/>`);
const titleText = this.element.querySelector('.title').innerText; const titleText = this.element.querySelector('h3').innerText;
assert.strictEqual(titleText, 'Card title'); assert.strictEqual(titleText, 'Card title');
}); });
test('it returns card subtext, ', async function (assert) { test('it returns card subtext, ', async function (assert) {
@@ -32,8 +32,10 @@ module('Integration | Component overview-card', function (hooks) {
assert.strictEqual(titleText, 'This is subtext for card'); assert.strictEqual(titleText, 'This is subtext for card');
}); });
test('it returns card action text', async function (assert) { test('it returns card action text', async function (assert) {
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}} @actionText={{this.actionText}}/>`); await render(
hbs`<OverviewCard @cardTitle={{this.cardTitle}} @actionText={{this.actionText}} @actionTo="route"/>`
);
const titleText = this.element.querySelector('a').innerText; const titleText = this.element.querySelector('a').innerText;
assert.strictEqual(titleText, 'View card '); assert.strictEqual(titleText, 'View card');
}); });
}); });

View File

@@ -35,7 +35,7 @@ module('Integration | Component | replication-primary-card', function (hooks) {
assert.dom('[data-test-hasError]').doesNotExist('shows no error for non-State cards'); assert.dom('[data-test-hasError]').doesNotExist('shows no error for non-State cards');
assert.dom('.last-wal').includesText(title); assert.dom('.card-title').includesText(title);
assert.dom('[data-test-description]').includesText(description); assert.dom('[data-test-description]').includesText(description);
assert.dom('[data-test-metric]').includesText(metric); assert.dom('[data-test-metric]').includesText(metric);
}); });

View File

@@ -5,30 +5,25 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers'; import { click, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
const TOTAL = 15;
const CARD_TITLE = 'Connections';
module('Integration | Component selectable-card', function (hooks) { module('Integration | Component selectable-card', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.set('total', TOTAL); this.onClick = sinon.spy();
this.set('cardTitle', CARD_TITLE);
}); });
test('it shows the card total', async function (assert) { test('it renders', async function (assert) {
await render(hbs`<SelectableCard @total={{this.total}} @cardTitle={{this.cardTitle}}/>`); await render(hbs`<SelectableCard @onClick={{this.onClick}}/>`);
const titleNumber = this.element.querySelector('.title-number').innerText; await click('.selectable-card');
assert.ok(this.onClick.calledOnce, 'calls on click');
assert.strictEqual(titleNumber, '15');
}); });
test('it returns card title, ', async function (assert) { test('it renders block content', async function (assert) {
await render(hbs`<SelectableCard @total={{1}} @cardTitle={{this.cardTitle}}/>`); await render(hbs`<SelectableCard @onClick={{this.onClick}}>hello</SelectableCard>`);
const titleText = this.element.querySelector('.title').innerText; assert.dom('.selectable-card').hasText('hello');
assert.strictEqual(titleText, 'Connections');
}); });
}); });

View File

@@ -10,12 +10,11 @@ export default {
...fields, ...fields,
header: text('[data-test-mount-form-header]'), header: text('[data-test-mount-form-header]'),
submit: clickable('[data-test-mount-submit]'), submit: clickable('[data-test-mount-submit]'),
next: clickable('[data-test-mount-next]'),
back: clickable('[data-test-mount-back]'), back: clickable('[data-test-mount-back]'),
path: fillable('[data-test-input="path"]'), path: fillable('[data-test-input="path"]'),
toggleOptions: clickable('[data-test-toggle-group="Method Options"]'), toggleOptions: clickable('[data-test-toggle-group="Method Options"]'),
pathValue: value('[data-test-input="path"]'), pathValue: value('[data-test-input="path"]'),
types: collection('[data-test-mount-type-radio] input', { types: collection('[data-test-mount-type]', {
select: clickable(), select: clickable(),
id: attribute('id'), id: attribute('id'),
}), }),
@@ -26,9 +25,9 @@ export default {
async mount(type, path) { async mount(type, path) {
await this.selectType(type); await this.selectType(type);
if (path) { if (path) {
await this.next().path(path).submit(); await this.path(path).submit();
} else { } else {
await this.next().submit(); await this.submit();
} }
}, },
}; };