UI: HDS adoption replace <Modal> (#23382)

* UI: Part 1 - hds adoption replace <Modal> (#23363)

* replace policy-form modal

* replace clients/attribution modal

* clients/config modal

* scope form odal

* remove button type

* include toolbar to match other example templates

* rotate credentials modal

* add toolbar button class for hds buttons

* transformation-edit modal

* add back test selector

* add route arg to button!

* update link status

* fix link-status tests

* remove prevent default

* update db tests

* update tests

* use page alert for hcp link status banner

* fix scopy button selector

* fix sidebar test

* change to neutral banner

* UI: Part 2 - hds adoption replace <Modal>  (#23398)

* upgrade HDS library (adds support for snippet containers

* cleanup flight icons

* replace transit key action modals

* re-add deps as devDeps

* remove line

* address transit tests

* UI: Part 3 - hds adoption replace <Modal> (#23415)

* cleanup css

* cleanup extra type attr

* masked input download modal

* use Hds::Button in  download button"

* fix size of modal

* tiny icon fix

* refactor download button to always render download icon

* update tests

* UI: Part 3.5 - hds adoption replace <Modal> (#23448)

* replication-promote modal

* replication component modals

* replication add secondary modal

* move update text for diff

* UI: Part 4 - hds adoption replace <Modal>  (#23451)

* k8 configure modal

* kv delete modal

* ldap modals

* pki modals

* add trash icon

* move deps

* UI: Part 5 - hds adoption replace <Modal> (#23471)

* replace confirmation modals
---------

* UI: Part 6 - hds adoption replace <Modal>  (#23484)

* search select with modal

* policy search select modal

* replace date dropdown for client dashboard

* change padding to top

* update policy example args

* lolllll test typo wow

* update dropdown tests

* shamir flow modals!

* add one more container

* update test selectors

* UI: Final hds adoption replace <Modal> cleanup PR (#23522)

* search select with modal

* policy search select modal

* replace date dropdown for client dashboard

* change padding to top

* update policy example args

* lolllll test typo wow

* update dropdown tests

* shamir flow modals!

* add one more container

* update test selectors

* remove wormhole and modal component

* fix selectors

* uninstall wormhole

* remove shamir-modal-flow class

* fix confirm modal test

* fix pki and kv test

* fix toolbar selector kv

* client and download button test

* fix-confirmation-modal-padding

* fix replication modal tests so relevant modal opens (#23540)

* more confirmation modal tests

* adds changelog
This commit is contained in:
claire bontempo
2023-10-06 15:06:36 -07:00
committed by GitHub
parent 92fcfda8ad
commit 43258c28fa
120 changed files with 1555 additions and 2174 deletions

3
changelog/23382.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: Makes modals accessible by implementing Helios Design System modal component
```

View File

@@ -73,15 +73,15 @@ export default class DatabaseConnectionEdit extends Component {
} }
@action @action
continueWithoutRotate(evt) { continueWithoutRotate() {
evt.preventDefault(); this.showSaveModal = false;
const { name } = this.args.model; const { name } = this.args.model;
this.transitionToRoute(SHOW_ROUTE, name); this.transitionToRoute(SHOW_ROUTE, name);
} }
@action @action
continueWithRotate(evt) { continueWithRotate() {
evt.preventDefault(); this.showSaveModal = false;
const { backend, name } = this.args.model; const { backend, name } = this.args.model;
this.rotateCredentials(backend, name) this.rotateCredentials(backend, name)
.then(() => { .then(() => {

View File

@@ -15,10 +15,9 @@ import timestamp from 'core/utils/timestamp';
* *
* @example * @example
* ```js * ```js
* <DateDropdown @handleSubmit={{this.actionFromParent}} @name="startTime" @submitText="Save" @handleCancel={{this.onCancel}}/> * <DateDropdown @handleSubmit={{this.actionFromParent}} @name="startTime" @submitText="Save" />
* ``` * ```
* @param {function} handleSubmit - callback function from parent that the date picker triggers on submit * @param {function} handleSubmit - callback function from parent that the date picker triggers on submit
* @param {function} [handleCancel] - optional callback for cancel action, if exists then buttons appear modal style with a light gray background
* @param {string} [dateType] - optional argument to give the selected month/year a type * @param {string} [dateType] - optional argument to give the selected month/year a type
* @param {string} [submitText] - optional argument to change submit button text * @param {string} [submitText] - optional argument to change submit button text
* @param {function} [validateDate] - parent function to validate date selection, receives date object and returns an error message that's passed to the inline alert * @param {function} [validateDate] - parent function to validate date selection, receives date object and returns an error message that's passed to the inline alert
@@ -69,12 +68,6 @@ export default class DateDropdown extends Component {
this.resetDropdown(); this.resetDropdown();
} }
@action
handleCancel() {
this.args.handleCancel();
this.resetDropdown();
}
resetDropdown() { resetDropdown() {
this.maxMonthIdx = 11; this.maxMonthIdx = 11;
this.disabledYear = null; this.disabledYear = null;

View File

@@ -32,7 +32,7 @@
</nav> </nav>
{{/if}} {{/if}}
{{#if this.showExamplePolicy}} {{#if this.showExamplePolicy}}
<PolicyExample @policyType={{this.policy.policyType}} /> <PolicyExample @policyType={{this.policy.policyType}} @container="#search-select-modal" />
{{else}} {{else}}
<Select <Select
@name="policyType" @name="policyType"

View File

@@ -23,11 +23,24 @@
</div> </div>
{{/if}} {{/if}}
<div class="field"> <div class="field">
{{#if @model.isNew}} <Toolbar>
<Toolbar> <label class="has-text-weight-bold has-right-margin-xxs">Policy</label>
<label class="has-text-weight-bold">Policy</label> {{#if @renderPolicyExampleModal}}
<ToolbarActions> {{! only true in policy create and edit routes }}
<div class="toolbar-separator"></div> <ToolbarFilters>
<Hds::Button
@text="How to write a policy"
@icon="bulb"
@size="small"
@color="tertiary"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-policy-example-button
/>
</ToolbarFilters>
{{/if}}
<ToolbarActions>
<div class="toolbar-separator"></div>
{{#if @model.isNew}}
<div class="control is-flex"> <div class="control is-flex">
<Input <Input
id="fileUploadToggle" id="fileUploadToggle"
@@ -40,25 +53,24 @@
/> />
<label for="fileUploadToggle">Upload file</label> <label for="fileUploadToggle">Upload file</label>
</div> </div>
</ToolbarActions> {{else}}
</Toolbar> {{! EDITING - no file upload toggle}}
{{#if this.showFileUpload}} <Hds::Copy::Button
<TextFile @uploadOnly={{true}} @onChange={{this.setPolicyFromFile}} /> @text="Copy"
{{else}} @isIconOnly={{true}}
<JsonEditor @textToCopy={{@model.policy}}
@title="Policy" class="transparent"
@showToolbar={{false}} data-test-copy-button
@value={{@model.policy}} />
@valueUpdated={{action (mut @model.policy)}} {{/if}}
@mode="ruby" </ToolbarActions>
@extraKeys={{hash Shift-Enter=(perform this.save)}} </Toolbar>
data-test-policy-editor {{#if this.showFileUpload}}
/> <TextFile @uploadOnly={{true}} @onChange={{this.setPolicyFromFile}} />
{{/if}}
{{else}} {{else}}
{{! EDITING - no file upload toggle}}
<JsonEditor <JsonEditor
@title="Policy" @title="Policy"
@showToolbar={{false}}
@value={{@model.policy}} @value={{@model.policy}}
@valueUpdated={{action (mut @model.policy)}} @valueUpdated={{action (mut @model.policy)}}
@mode="ruby" @mode="ruby"
@@ -70,36 +82,6 @@
<span class="is-size-9 has-text-grey has-bottom-margin-l"> <span class="is-size-9 has-text-grey has-bottom-margin-l">
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field.
</span> </span>
{{! Only renders button (and modal) if not already in the "create policy" modal }}
{{#if @renderPolicyExampleModal}}
<span class="is-size-9 has-text-grey has-bottom-margin-l">
See
<button
type="button"
class="text-button has-text-info"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-policy-example-button
>
example template
</button>.
</span>
{{! Only renders more information if already in the "create policy" modal }}
{{else}}
<p class="has-top-margin-l">
More information about
{{uppercase @model.policyType}}
policies can be found
<DocLink
@path={{if
(eq @model.policyType "acl")
"/vault/docs/concepts/policies#capabilities"
"/vault/tutorials/policies/sentinel#role-governing-policies-rgps"
}}
>
here.
</DocLink>
</p>
{{/if}}
</div> </div>
</div> </div>
{{#each @model.additionalAttrs as |attr|}} {{#each @model.additionalAttrs as |attr|}}
@@ -128,26 +110,26 @@
</div> </div>
</div> </div>
</form> </form>
{{! SAMPLE POLICY MODAL. Only renders modal if not already in create policy modal }}
{{#if @renderPolicyExampleModal}} {{! SAMPLE POLICY MODAL. Only renders in policy create and edit routes }}
<Modal {{#if this.showTemplateModal}}
@title="Example {{uppercase @model.policyType}} Policy" <Hds::Modal
id="policy-example-modal"
@size="large"
@onClose={{fn (mut this.showTemplateModal) false}} @onClose={{fn (mut this.showTemplateModal) false}}
@isActive={{this.showTemplateModal}}
@showCloseButton={{true}}
data-test-policy-example-modal data-test-policy-example-modal
as |M|
> >
<section class="modal-card-body"> <M.Header data-test-modal-title>
{{! code-mirror modifier does not render value initially until focus event fires }} Example
{{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }} {{uppercase @model.policyType}}
{{#if this.showTemplateModal}} Policy
<PolicyExample @policyType={{@model.policyType}} /> </M.Header>
{{/if}} <M.Body>
</section> <PolicyExample @policyType={{@model.policyType}} @container="#policy-example-modal" />
<div class="modal-card-head has-border-top-light"> </M.Body>
<button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal> <M.Footer as |F|>
Close <Hds::Button @text="Close" {{on "click" F.close}} data-test-modal-close-button />
</button> </M.Footer>
</div> </Hds::Modal>
</Modal>
{{/if}} {{/if}}

View File

@@ -46,7 +46,6 @@
</Frame.Sidebar> </Frame.Sidebar>
<Frame.Main id="app-main-content" class={{if this.console.isOpen "main--console-open"}}> <Frame.Main id="app-main-content" class={{if this.console.isOpen "main--console-open"}}>
{{! outlet for app content }} {{! outlet for app content }}
<div id="modal-wormhole"></div>
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} /> <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
{{yield}} {{yield}}
<div data-test-console-panel class={{if this.console.isOpen "panel-open"}}> <div data-test-console-panel class={{if this.console.isOpen "panel-open"}}>

View File

@@ -48,25 +48,19 @@
background-color: transparent; background-color: transparent;
} }
.button.masked-input-toggle, .button.masked-input-toggle {
.button.download-button {
min-width: $spacing-xl; min-width: $spacing-xl;
border-left: 0; border-left: 0;
color: $grey; color: $grey;
box-shadow: 0 3px 1px 0px rgba(10, 10, 10, 0.12); box-shadow: 0 3px 1px 0px rgba(10, 10, 10, 0.12);
} }
.button.download-button {
border-radius: 0;
}
.button.masked-input-toggle { .button.masked-input-toggle {
border-radius: 0 $radius $radius 0; border-radius: 0 $radius $radius 0;
} }
.display-only { .display-only {
.button.masked-input-toggle, .button.masked-input-toggle {
.button.download-button {
background: transparent; background: transparent;
height: auto; height: auto;
line-height: 1rem; line-height: 1rem;

View File

@@ -1,157 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.modal {
align-items: center;
bottom: 0;
left: 0;
right: 0;
top: 0;
display: none;
justify-content: center;
overflow: hidden;
position: fixed;
z-index: 20;
&.is-active {
display: flex;
}
}
.modal-background {
background: $ui-gray-100;
bottom: 0;
left: 0;
opacity: 0.9;
position: absolute;
right: 0;
top: 0;
}
.modal-card {
box-shadow: $box-shadow-highest;
border: 1px solid $grey-light;
display: flex;
flex-direction: column;
max-height: calc(100vh - 70px);
margin-top: 60px;
min-width: calc(100vw * 0.3);
overflow: hidden;
position: relative;
&-head {
border-radius: 0;
background-color: $grey-lightest;
border-bottom: 1px solid $grey-light;
display: flex;
justify-content: flex-start;
padding: 20px;
}
&-foot {
border-radius: 0;
border: 0;
background-color: $white;
padding: 20px;
.button:not(:last-child) {
margin-right: 10px;
}
}
&-title.title {
margin: 0;
flex-grow: 1;
}
.copy-text {
background-color: $grey-lightest;
padding: $spacing-s;
margin-bottom: $spacing-s;
code {
overflow: scroll;
max-width: calc(100% - 36px);
background-color: inherit;
}
}
.copy-close {
margin-top: $spacing-s;
}
}
.modal-card-title.title {
display: flex;
align-items: center;
}
.modal-card-body {
background-color: $white;
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
padding: 20px;
}
pre {
background-color: inherit;
}
.is-highlight {
.modal-card-head {
background: $yellow-010;
border: 1px solid $yellow-100;
}
.modal-card-title {
color: $yellow-dark;
}
}
.is-danger {
.modal-card-head {
background: $red-010;
border: 1px solid $red-100;
}
.modal-card-title {
color: $red-dark;
}
}
.modal-confirm-section {
margin: $spacing-xl 0 $spacing-m;
}
.modal-card-foot-outlined {
background: $ui-gray-050;
border-top: $base-border;
}
// customize spacing (.modal-card-body is restricted to padding: 20px)
.modal-card-custom {
background-color: white;
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
&.has-padding-m {
padding: $spacing-m;
}
&.has-padding-btm-left {
padding-bottom: $spacing-m;
padding-left: $spacing-m;
}
}
// responsive css
@media screen and (min-width: 769px), print {
.modal-card,
.modal-content {
margin: 0 auto;
max-height: calc(100vh - 40px);
width: 640px;
}
}

View File

@@ -1,9 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.field-title {
font-weight: 700;
font-size: $size-7;
}

View File

@@ -24,29 +24,3 @@
color: $black; color: $black;
} }
} }
.link-status {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: $size-7;
font-weight: $font-weight-semibold;
&.connected {
background-color: var(--token-color-surface-action);
color: var(--token-color-foreground-action-active);
a {
color: var(--token-color-foreground-action-active);
}
}
&.warning {
background-color: var(--token-color-surface-warning);
color: var(--token-color-palette-amber-300);
a {
color: var(--token-color-palette-amber-300);
}
}
}

View File

@@ -65,6 +65,10 @@
display: flex; display: flex;
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
&:has(.hds-modal) {
// toolbar buttons that open/close a modal pass attrs to the modal content
white-space: wrap;
}
} }
.toolbar-filters + .toolbar-actions { .toolbar-filters + .toolbar-actions {

View File

@@ -80,7 +80,6 @@
@import './components/loader'; @import './components/loader';
@import './components/login-form'; @import './components/login-form';
@import './components/masked-input'; @import './components/masked-input';
@import './components/modal-component.scss';
@import './components/namespace-picker'; @import './components/namespace-picker';
@import './components/namespace-reminder'; @import './components/namespace-reminder';
@import './components/navigate-input'; @import './components/navigate-input';
@@ -104,7 +103,6 @@
@import './components/secrets-engines-card'; @import './components/secrets-engines-card';
// action-block extends selectable-card // action-block extends selectable-card
@import './components/action-block'; @import './components/action-block';
@import './components/shamir-modal-flow';
@import './components/shamir-progress'; @import './components/shamir-progress';
@import './components/sidebar'; @import './components/sidebar';
@import './components/splash-page'; @import './components/splash-page';

View File

@@ -91,12 +91,6 @@
color: $red-500; color: $red-500;
} }
&.is-warning-outlined {
background-color: $yellow-010;
border: 1px solid $yellow-700;
color: $yellow-700;
}
&.is-flat { &.is-flat {
min-width: auto; min-width: auto;
border: none; border: none;
@@ -332,11 +326,11 @@ a.button.disabled {
} }
} }
// TODO HDS adoption cleanup: audit styles with design and see what to keep/remove once buttons are fully HDS
// Existing class on <Hds::Copy::Button> component, modifying to match existing UI Structure buttons // Existing class on <Hds::Copy::Button> component, modifying to match existing UI Structure buttons
.hds-copy-button { .hds-copy-button {
font-weight: $font-weight-semibold; font-weight: $font-weight-semibold; // TODO delete
box-shadow: $box-shadow-low; box-shadow: $box-shadow-low; // TODO delete
border-radius: $radius;
&.white-icon svg { &.white-icon svg {
color: $white; color: $white;
@@ -350,15 +344,14 @@ a.button.disabled {
color: $ui-gray-500; color: $ui-gray-500;
} }
&.icon-only {
margin-right: $spacing-xxs;
margin-left: $spacing-xxs;
}
&.transparent { &.transparent {
background: none; background: none;
border: none;
box-shadow: none; box-shadow: none;
border: 1px solid transparent;
&:hover {
border: 1px solid $grey-light;
border-color: var(--token-color-border-strong);
}
} }
&.primary { &.primary {
@@ -386,3 +379,20 @@ a.button.disabled {
} }
} }
} }
// Existing class on <Hds::Button> component, modifying to match existing UI Structure buttons
.hds-button {
font-weight: $font-weight-semibold; // TODO consult design on font weight after button class audit
// for toolbar-button must pass arg @color="secondary"
&.toolbar-button {
color: $black;
background: none;
border: none;
box-shadow: none;
&:hover:not(.disabled) {
background-color: $ui-gray-100;
border: 0;
color: $blue;
}
}
}

View File

@@ -170,3 +170,27 @@ form {
label { label {
cursor: pointer; cursor: pointer;
} }
// HDS modifications and overrides
// * ONLY for universal changes (i.e. to address component functionality)
// <Hds::Modal>
.hds-modal {
&:has(.hds-dropdown) {
overflow: unset;
}
}
.hds-modal__body {
&:has(.hds-dropdown) {
overflow: unset;
}
}
// <Hds::Dropdown>
.hds-dropdown-list-item {
> button:disabled {
color: $black;
opacity: 0.5;
cursor: not-allowed;
}
}

View File

@@ -15,14 +15,12 @@
</div> </div>
<div class="header-right"> <div class="header-right">
{{#if this.hasCsvData}} {{#if this.hasCsvData}}
<button <Hds::Button
data-test-attribution-export-button data-test-attribution-export-button
type="button" @text="Export attribution data"
class="button is-secondary" @color="secondary"
{{on "click" (fn (mut this.showCSVDownloadModal) true)}} {{on "click" (fn (mut this.showCSVDownloadModal) true)}}
> />
Export attribution data
</button>
{{/if}} {{/if}}
</div> </div>
</div> </div>
@@ -91,41 +89,41 @@
</div> </div>
{{! MODAL FOR CSV DOWNLOAD }} {{! MODAL FOR CSV DOWNLOAD }}
<Modal {{#if this.showCSVDownloadModal}}
@title="Export attribution data" <Hds::Modal id="attribution-csv-download-modal" @onClose={{fn (mut this.showCSVDownloadModal) false}} as |M|>
@type="info" <M.Header @icon="info" data-test-export-modal-title>
@isActive={{this.showCSVDownloadModal}} Export attribution data
@showCloseButton={{true}} </M.Header>
@onClose={{action (mut this.showCSVDownloadModal) false}} <M.Body>
> <p class="has-bottom-margin-s">
<section class="modal-card-body"> This export will include the namespace path, authentication method path, and the associated total, entity, and
<p class="has-bottom-margin-s"> non-entity clients for the below
This export will include the namespace path, authentication method path, and the associated total, entity, and {{if this.formattedEndDate "date range" "month"}}.
non-entity clients for the below </p>
{{if this.formattedEndDate "date range" "month"}}. <p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if this.formattedEndDate " RANGE"}}</p>
</p> <p class="has-bottom-margin-s" data-test-export-date-range>
<p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if this.formattedEndDate " RANGE"}}</p> {{this.formattedStartDate}}
<p class="has-bottom-margin-s">{{this.formattedStartDate}} {{if this.formattedEndDate "-"}} {{this.formattedEndDate}}</p> {{if this.formattedEndDate "-"}}
</section> {{this.formattedEndDate}}</p>
<footer class="modal-card-foot modal-card-foot-outlined"> </M.Body>
<button type="button" class="button is-primary" {{on "click" (fn this.exportChartData this.formattedCsvFileName)}}> <M.Footer as |F|>
Export <Hds::ButtonSet>
</button> <Hds::Button @text="Export" {{on "click" (fn this.exportChartData this.formattedCsvFileName)}} />
<button type="button" class="button is-secondary" onclick={{action (mut this.showCSVDownloadModal) false}}> <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} />
Cancel </Hds::ButtonSet>
</button> {{#if @upgradeExplanation}}
{{#if @upgradeExplanation}} <div class="has-text-grey is-size-8 has-top-padding-m">
<div class="has-text-grey is-size-8"> <AlertInline @type="warning">
<AlertInline @type="warning" @isMarginless={{true}}> Your data contains an upgrade.
Your data contains an upgrade. <DocLink
<DocLink @path="/vault/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts"
@path="/vault/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts" >
> Learn more here.
Learn more here. </DocLink>
</DocLink> </AlertInline>
</AlertInline> <p class="has-left-padding-l">{{@upgradeExplanation}}</p>
<p class="has-left-padding-l">{{@upgradeExplanation}}</p> </div>
</div> {{/if}}
{{/if}} </M.Footer>
</footer> </Hds::Modal>
</Modal> {{/if}}

View File

@@ -47,47 +47,34 @@
</div> </div>
</form> </form>
<Modal {{#if this.modalOpen}}
@title={{this.modalTitle}} <Hds::Modal id="clients-config-modal" @color="warning" @onClose={{fn (mut this.modalOpen) false}} as |M|>
@onClose={{action (mut this.modalOpen) false}} <M.Header @icon="alert-triangle" data-test-clients-config-modal="title">
@isActive={{this.modalOpen}} {{this.modalTitle}}
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> {{#if (eq @model.enabled "On")}}
<section class="modal-card-body"> <p class="has-bottom-margin-s" data-test-clients-config-modal="on">
{{#if (eq @model.enabled "On")}} Vault will start tracking data starting from todays date,
<p class="has-bottom-margin-s" data-test-clients-config-modal="on"> {{date-format (now) "MMMM d, yyyy"}}. If youve previously enabled usage tracking, that historical data will
Vault will start tracking data starting from todays date, still be available to you.
{{date-format (now) "MMMM d, yyyy"}}. If youve previously enabled usage tracking, that historical data will still </p>
be available to you. {{else}}
</p> <p class="has-bottom-margin-s" data-test-clients-config-modal="off">
{{else}} Turning usage tracking off means that all data for the current month will be deleted. You will still be able to
<p class="has-bottom-margin-s" data-test-clients-config-modal="off"> query previous months.
Turning usage tracking off means that all data for the current month will be deleted. You will still be able to </p>
query previous months. <p>Are you sure?</p>
</p> {{/if}}
<p>Are you sure?</p> </M.Body>
{{/if}} <M.Footer as |F|>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button @text="Continue" {{on "click" (perform this.save)}} data-test-clients-config-modal="continue" />
<button <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-clients-config-modal="cancel" />
type="button" </Hds::ButtonSet>
class="button is-primary" </M.Footer>
data-test-clients-config-modal="continue" </Hds::Modal>
{{on "click" (perform this.save)}} {{/if}}
>
Continue
</button>
<button
type="button"
class="button is-secondary"
{{on "click" (fn (mut this.modalOpen) false)}}
data-test-clients-config-modal="cancel"
>
Cancel
</button>
</footer>
</Modal>
{{else}} {{else}}
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-clients-config-table> <div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-clients-config-table>
{{#each this.infoRows as |item|}} {{#each this.infoRows as |item|}}

View File

@@ -175,26 +175,28 @@
</EmptyState> </EmptyState>
{{/if}} {{/if}}
{{/if}} {{/if}}
</div>
{{! BILLING START DATE MODAL }} {{! BILLING START DATE MODAL }}
{{#if this.showBillingStartModal}}
<Modal <Hds::Modal id="clients-edit-date-modal" @onClose={{fn (mut this.showBillingStartModal) false}} as |M|>
@title="Edit start month" <M.Header>
@onClose={{action (mut this.showBillingStartModal) false}} Edit start month
@isActive={{this.showBillingStartModal}} </M.Header>
@showCloseButton={{true}} <M.Body>
>
<section class="modal-card-custom has-padding-m">
<p class="has-bottom-margin-s"> <p class="has-bottom-margin-s">
{{this.versionText.description}} {{this.versionText.description}}
</p> </p>
<p><strong>{{this.versionText.label}}</strong></p> <p><strong>{{this.versionText.label}}</strong></p>
</section> <DateDropdown
<DateDropdown class="has-top-padding-s"
@handleSubmit={{this.handleClientActivityQuery}} @handleSubmit={{this.handleClientActivityQuery}}
@dateType="startDate" @dateType="startDate"
@submitText="Save" @submitText="Save"
@handleCancel={{fn this.handleClientActivityQuery (hash dateType="cancel")}} />
/> </M.Body>
</Modal> <M.Footer as |F|>
</div> <Hds::Button data-test-date-dropdown-cancel @text="Cancel" @color="secondary" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -352,35 +352,32 @@
{{/each}} {{/each}}
{{/if}} {{/if}}
<Modal {{#if this.showSaveModal}}
@title="Rotate your root credentials?" <Hds::Modal id="rotate-credentials-modal" @onClose={{this.continueWithoutRotate}} as |M|>
@onClose={{this.continueWithoutRotate}} <M.Header @icon="info" data-test-db-connection-modal-title>
@isActive={{this.showSaveModal}} Rotate your root credentials?
@type="info" </M.Header>
@showCloseButton={{false}} <M.Body>
> <p class="has-bottom-margin-s">
<section class="modal-card-body"> Its best practice to rotate the root credential immediately after the initial configuration of each database. Once
<p class="has-bottom-margin-s"> rotated,
Its best practice to rotate the root credential immediately after the initial configuration of each database. Once <strong>only Vault knows the new root password</strong>.
rotated, </p>
<strong>only Vault knows the new root password</strong>. <p>Would you like to rotate your new credentials? You can also do this later.</p>
</p> </M.Body>
<p>Would you like to rotate your new credentials? You can also do this later.</p> <M.Footer>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button @text="Rotate and enable" {{on "click" this.continueWithRotate}} data-test-enable-rotate-connection />
<button <Hds::Button
type="button" @text="Enable without rotating"
class="button is-primary" @color="secondary"
{{on "click" this.continueWithRotate}} {{on "click" this.continueWithoutRotate}}
data-test-enable-rotate-connection data-test-enable-connection
> />
Rotate and enable </Hds::ButtonSet>
</button> </M.Footer>
<button type="button" class="button is-secondary" {{on "click" this.continueWithoutRotate}} data-test-enable-connection> </Hds::Modal>
Enable without rotating {{/if}}
</button>
</footer>
</Modal>
<ConfirmationModal <ConfirmationModal
@title="Delete connection?" @title="Delete connection?"

View File

@@ -3,89 +3,36 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div class="modal-card-custom has-padding-btm-left"> <Hds::SegmentedGroup ...attributes as |S|>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|> <S.Dropdown @height="200px" as |dd|>
<D.Trigger <dd.ToggleButton data-test-toggle-month @text={{or this.selectedMonth.name "Month"}} @color="secondary" />
data-test-popup-menu-trigger="month" {{#each this.dropdownMonths as |month|}}
class={{concat "toolbar-link" (if D.isOpen " is-active")}} <dd.Interactive
@htmlTag="button" data-test-dropdown-month={{month.name}}
> disabled={{if (gt month.index this.maxMonthIdx) true false}}
{{or this.selectedMonth.name "Month"}} {{on "click" (fn this.selectMonth month dd)}}
<Chevron @direction="down" @isButton={{true}} /> @text={{month.name}}
</D.Trigger> />
<D.Content @defaultClass="popup-menu-content is-wide"> {{/each}}
<nav class="box menu scroll" aria-label="months"> </S.Dropdown>
<ul data-test-month-list class="menu-list"> <S.Dropdown data-test-year-list @height="200px" as |dd|>
{{#each this.dropdownMonths as |month|}} <dd.ToggleButton data-test-toggle-year @text={{or this.selectedYear "Year"}} @color="secondary" />
<button {{#each this.dropdownYears as |year|}}
data-test-dropdown-month={{month.name}} <dd.Interactive
type="button" data-test-dropdown-year={{year}}
class="button link" disabled={{if (eq year this.disabledYear) true false}}
disabled={{if (gt month.index this.maxMonthIdx) true false}} {{on "click" (fn this.selectYear year dd)}}
{{on "click" (fn this.selectMonth month D.actions)}} @text={{year}}
> />
{{month.name}} {{/each}}
</button> </S.Dropdown>
{{/each}} <S.Button
</ul> data-test-date-dropdown-submit
</nav> disabled={{if (and this.selectedMonth this.selectedYear) false true}}
</D.Content> {{on "click" this.handleSubmit}}
</BasicDropdown> @text={{or @submitText "Submit"}}
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|> />
<D.Trigger </Hds::SegmentedGroup>
data-test-popup-menu-trigger="year" {{#if this.invalidDate}}
class={{concat "toolbar-link" (if D.isOpen " is-active")}} <AlertInline @type="danger" @message={{this.invalidDate}} @paddingTop={{true}} @mimicRefresh={{true}} />
@htmlTag="button"
>
{{or this.selectedYear "Year"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content @defaultClass="popup-menu-content is-wide">
<nav class="box menu" aria-label="years">
<ul data-test-year-list class="menu-list">
{{#each this.dropdownYears as |year|}}
<button
data-test-dropdown-year={{year}}
type="button"
class="button link"
disabled={{if (eq year this.disabledYear) true false}}
{{on "click" (fn this.selectYear year D.actions)}}
>
{{year}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
{{#unless @handleCancel}}
<button
data-test-date-dropdown-submit
type="button"
class="button is-primary"
disabled={{if (and this.selectedMonth this.selectedYear) false true}}
{{on "click" this.handleSubmit}}
>
{{or @submitText "Submit"}}
</button>
{{/unless}}
{{#if this.invalidDate}}
<AlertInline @type="danger" @message={{this.invalidDate}} @paddingTop={{true}} @mimicRefresh={{true}} />
{{/if}}
</div>
{{#if @handleCancel}}
<footer class="modal-card-foot modal-card-foot-outlined">
<button
data-test-date-dropdown-submit
type="button"
class="button is-primary"
disabled={{if (and this.selectedMonth this.selectedYear) false true}}
{{on "click" this.handleSubmit}}
>
{{or @submitText "Submit"}}
</button>
<button data-test-date-dropdown-cancel type="button" class="button is-secondary" {{on "click" this.handleCancel}}>
Cancel
</button>
</footer>
{{/if}} {{/if}}

View File

@@ -4,9 +4,8 @@
~}} ~}}
{{#if (and this.state this.version.isEnterprise)}} {{#if (and this.state this.version.isEnterprise)}}
<div class="link-status {{if (eq this.state 'connected') 'connected' 'warning'}}"> <Hds::Alert @type="page" @color={{if (eq this.state "connected") "neutral" "warning"}} as |A|>
<Icon @name="info" /> <A.Title data-test-link-status>
<p data-test-link-status>
{{#if (eq this.state "connected")}} {{#if (eq this.state "connected")}}
This self-managed Vault is linked to This self-managed Vault is linked to
<ExternalLink @href="https://portal.cloud.hashicorp.com/sign-in"> <ExternalLink @href="https://portal.cloud.hashicorp.com/sign-in">
@@ -19,49 +18,41 @@
</button> </button>
for more information. for more information.
{{/if}} {{/if}}
</p> </A.Title>
</div> </Hds::Alert>
{{/if}} {{/if}}
<Modal {{#if this.showModal}}
@title="HCP Link error" <Hds::Modal id="hcp-link-error-modal" @color="warning" @size="small" @onClose={{fn (mut this.showModal) false}} as |M|>
@onClose={{fn (mut this.showModal) false}} <M.Header @icon="alert-triangle">
@isActive={{this.showModal}} HCP Link error
@type="info" </M.Header>
@showCloseButton={{true}} <M.Body>
> <div>
<section class="modal-card-body"> <p class="has-text-weight-bold">Timestamp</p>
<div> <p data-test-link-status-timestamp>
<p class="has-text-weight-bold">Timestamp</p> {{or this.timestamp "Not available"}}
<p data-test-link-status-timestamp>
{{or this.timestamp "Not available"}}
</p>
</div>
<div class="has-top-bottom-margin">
<p class="has-text-weight-bold">Error</p>
{{#if this.error}}
<code class="tag has-text-danger" data-test-link-status-error>
{{this.error}}
</code>
{{else}}
<p data-test-link-status-error>
Not available
</p> </p>
{{/if}} </div>
</div> <div class="has-top-bottom-margin">
<div> <p class="has-text-weight-bold">Error</p>
<p class="has-text-weight-bold">Additional information</p> {{#if this.error}}
<p>Check the logs for more information</p> <code class="tag has-text-danger" data-test-link-status-error>
</div> {{this.error}}
</section> </code>
<footer class="modal-card-foot modal-card-foot-outlined"> {{else}}
<button <p data-test-link-status-error>
type="button" Not available
class="button is-primary" </p>
{{on "click" (fn (mut this.showModal) false)}} {{/if}}
data-test-link-status-close </div>
> <div>
Close <p class="has-text-weight-bold">Additional information</p>
</button> <p>Check the logs for more information</p>
</footer> </div>
</Modal> </M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} data-test-link-status-close />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -23,6 +23,7 @@
@id="entities" @id="entities"
@label="Entities" @label="Entities"
@placeholder="Search" @placeholder="Search"
@renderInPlace={{true}}
@models={{array "identity/entity"}} @models={{array "identity/entity"}}
@inputValue={{@model.entityIds}} @inputValue={{@model.entityIds}}
@shouldRenderName={{true}} @shouldRenderName={{true}}
@@ -35,6 +36,7 @@
@id="groups" @id="groups"
@label="Groups" @label="Groups"
@placeholder="Search" @placeholder="Search"
@renderInPlace={{true}}
@models={{array "identity/group"}} @models={{array "identity/group"}}
@inputValue={{@model.groupIds}} @inputValue={{@model.groupIds}}
@shouldRenderName={{true}} @shouldRenderName={{true}}

View File

@@ -29,6 +29,15 @@
Scope Scope
</h1> </h1>
</p.levelLeft> </p.levelLeft>
<p.levelRight>
<Hds::Button
@text="How to write JSON template for scopes"
@icon="bulb"
@color="tertiary"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-oidc-scope-example
/>
</p.levelRight>
</PageHeader> </PageHeader>
<form {{on "submit" (perform this.save)}}> <form {{on "submit" (perform this.save)}}>
@@ -41,15 +50,8 @@
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} /> <FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}} {{/each}}
<p class="is-size-9 has-text-grey has-bottom-margin-l"> <p class="is-size-9 has-text-grey has-bottom-margin-l">
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. See You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. Click 'How to write JSON
<button template for scopes' to view an example.
type="button"
class="text-button has-text-info"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-oidc-scope-example
>
example template
</button>.
</p> </p>
</div> </div>
<div class="has-top-margin-l has-bottom-margin-l"> <div class="has-top-margin-l has-bottom-margin-l">
@@ -78,40 +80,25 @@
{{/if}} {{/if}}
</form> </form>
<Modal {{#if this.showTemplateModal}}
@title="Scope template" <Hds::Modal id="scope-template-modal" @size="large" @onClose={{fn (mut this.showTemplateModal) false}} as |M|>
@onClose={{fn (mut this.showTemplateModal) false}} <M.Header data-test-scope-modal="title">
@isActive={{this.showTemplateModal}} Scope template
@showCloseButton={{true}} </M.Header>
> <M.Body>
<section class="modal-card-body"> <p data-test-scope-modal="text">
<div class="is-flex-between is-flex-center has-bottom-margin-s">
<p data-test-modal-text>
Example of a JSON template for scopes: Example of a JSON template for scopes:
</p> </p>
<Hds::Copy::Button <JsonEditor @value={{this.exampleTemplate}} @mode="ruby" @readOnly={{true}} @container=".hds-modal" />
@text="Copy" <p class="has-top-margin-m">
@isIconOnly={{true}} The full list of template parameters can be found
@textToCopy={{this.exampleTemplate}} <DocLink @path="/vault/docs/concepts/oidc-provider#scopes">
class="transparent" here.
data-test-copy-button </DocLink>
/> </p>
</div> </M.Body>
{{! code-mirror modifier does not render value initially in wormhole until focus event fires }} <M.Footer as |F|>
{{! wait until the Modal is rendered and then show the JsonEditor }} <Hds::Button @text="Close" {{on "click" F.close}} data-test-close-modal />
{{#if this.showTemplateModal}} </M.Footer>
<JsonEditor @value={{this.exampleTemplate}} @mode="ruby" @readOnly={{true}} @showToolbar={{false}} /> </Hds::Modal>
{{/if}} {{/if}}
<p class="has-top-margin-m">
The full list of template parameters can be found
<DocLink @path="/vault/docs/concepts/oidc-provider#scopes">
here.
</DocLink>
</p>
</section>
<div class="modal-card-head has-border-top-light">
<button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
Close
</button>
</div>
</Modal>

View File

@@ -53,14 +53,13 @@
{{/if}} {{/if}}
{{#if this.capabilities.canUpdate}} {{#if this.capabilities.canUpdate}}
{{#if (gt this.model.allowed_roles.length 0)}} {{#if (gt this.model.allowed_roles.length 0)}}
<button <Hds::Button
class="toolbar-link" @text="Edit transformation"
onclick={{action (mut this.isEditModalActive) true}} @color="secondary"
type="button" class="toolbar-button"
{{on "click" (fn (mut this.isEditModalActive) true)}}
data-test-edit-link data-test-edit-link
> />
Edit transformation
</button>
{{else}} {{else}}
<ToolbarSecretLink @secret={{this.model.id}} @mode="edit" data-test-edit-link={{true}} @replace={{true}}> <ToolbarSecretLink @secret={{this.model.id}} @mode="edit" data-test-edit-link={{true}} @replace={{true}}>
Edit transformation Edit transformation
@@ -96,30 +95,27 @@
<MessageError @model={{this.model}} @errorMessage={{this.error}} /> <MessageError @model={{this.model}} @errorMessage={{this.error}} />
</ConfirmationModal> </ConfirmationModal>
<Modal {{#if this.isEditModalActive}}
@title="Edit transformation" <Hds::Modal id="transformation-edit-modal" @color="warning" @onClose={{fn (mut this.isEditModalActive) false}} as |M|>
@onClose={{action (mut this.isEditModalActive) false}} <M.Header @icon="alert-triangle">
@isActive={{this.isEditModalActive}} Edit transformation
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> <p>
<section class="modal-card-body"> Youre editing a transformation that is in use by at least one role. Editing it may mean that encode and decode
<p> operations stop working. Are you sure?
Youre editing a transformation that is in use by at least one role. Editing it may mean that encode and decode </p>
operations stop working. Are you sure? </M.Body>
</p> <M.Footer as |F|>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button
<LinkTo @text="Confirm"
@route="vault.cluster.secrets.backend.edit" @route="vault.cluster.secrets.backend.edit"
@model={{this.model.id}} @model={{this.model.id}}
class="button is-primary" data-test-edit-confirm-button
data-test-edit-confirm-button={{true}} />
> <Hds::Button @color="secondary" @text="Cancel" {{on "click" F.close}} />
Confirm </Hds::ButtonSet>
</LinkTo> </M.Footer>
<button type="button" class="button is-secondary" onclick={{action (mut this.isEditModalActive) false}}> </Hds::Modal>
Cancel {{/if}}
</button>
</footer>
</Modal>

View File

@@ -74,19 +74,27 @@
</div> </div>
</div> </div>
</form> </form>
<Modal @title="Copy your generated key" @onClose={{action (mut @isModalActive) false}} @isActive={{@isModalActive}}> {{#if @isModalActive}}
<section class="modal-card-body"> <Hds::Modal id="transit-datakey-modal" @onClose={{fn (mut @isModalActive) false}} as |M|>
<div class="box is-shadowless is-fullwidth is-sideless"> <M.Header>
Copy your generated key
</M.Header>
<M.Body>
{{#if (eq @param "plaintext")}} {{#if (eq @param "plaintext")}}
<h2 class="has-text-weight-semibold is-6">Plaintext</h2> <h2 class="has-text-weight-semibold is-6">Plaintext</h2>
<p class="sub-text">Plaintext is base64 encoded</p> <p class="sub-text">Plaintext is base64 encoded</p>
<Hds::Copy::Snippet class="has-bottom-margin-m" @textToCopy={{@plaintext}} @color="secondary" /> <Hds::Copy::Snippet
class="has-bottom-margin-m"
@textToCopy={{@plaintext}}
@color="secondary"
@container="#transit-datakey-modal"
/>
{{/if}} {{/if}}
<h2 class="has-text-weight-semibold is-6">Ciphertext</h2> <h2 class="has-text-weight-semibold is-6">Ciphertext</h2>
<Hds::Copy::Snippet @textToCopy={{@ciphertext}} @color="secondary" /> <Hds::Copy::Snippet @textToCopy={{@ciphertext}} @color="secondary" @container="#transit-datakey-modal" />
</div> </M.Body>
</section> <M.Footer as |F|>
<footer class="modal-card-foot"> <Hds::Button @text="Close" {{on "click" F.close}} />
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> </M.Footer>
</footer> </Hds::Modal>
</Modal> {{/if}}

View File

@@ -56,16 +56,22 @@
</div> </div>
</form> </form>
{{#if @isModalActive}} {{#if @isModalActive}}
<Modal @title="Copy your unwrapped data" @onClose={{action (mut @isModalActive) false}} @isActive={{@isModalActive}}> <Hds::Modal id="transit-decrypt-modal" @onClose={{fn (mut @isModalActive) false}} data-test-decrypt-modal as |M|>
<section class="modal-card-body"> <M.Header>
<div class="box is-shadowless is-fullwidth is-sideless"> Copy your unwrapped data
<h2 class="has-text-weight-semibold is-6">Plaintext</h2> </M.Header>
<p class="sub-text">Plaintext is base64 encoded</p> <M.Body>
<Hds::Copy::Snippet @textToCopy={{@plaintext}} @color="secondary" data-test-encrypted-value="plaintext" /> <h2 class="has-text-weight-semibold is-6">Plaintext</h2>
</div> <p class="sub-text">Plaintext is base64 encoded</p>
</section> <Hds::Copy::Snippet
<footer class="modal-card-foot"> @textToCopy={{@plaintext}}
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> @color="secondary"
</footer> @container="#transit-decrypt-modal"
</Modal> data-test-encrypted-value="plaintext"
/>
</M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}} {{/if}}

View File

@@ -69,19 +69,22 @@
</div> </div>
</div> </div>
</form> </form>
<Modal {{#if @isModalActive}}
@title="Copy your token" <Hds::Modal id="transit-encrypt-modal" @onClose={{fn (mut @isModalActive) false}} data-test-encrypt-modal as |M|>
@onClose={{action (mut @isModalActive) false}} <M.Header>
@isActive={{@isModalActive}} Copy your token
data-test-encrypt-modal </M.Header>
> <M.Body>
<section class="modal-card-body">
<div class="box is-shadowless is-fullwidth is-sideless">
<h2 class="title is-6">Ciphertext</h2> <h2 class="title is-6">Ciphertext</h2>
<Hds::Copy::Snippet @textToCopy={{@ciphertext}} @color="secondary" data-test-encrypted-value="ciphertext" /> <Hds::Copy::Snippet
</div> @textToCopy={{@ciphertext}}
</section> @color="secondary"
<footer class="modal-card-foot"> @container="#transit-encrypt-modal"
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> data-test-encrypted-value="ciphertext"
</footer> />
</Modal> </M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -65,15 +65,19 @@
</div> </div>
</div> </div>
</form> </form>
<Modal @title="Copy your wrapped key" @onClose={{action (mut @isModalActive) false}} @isActive={{@isModalActive}}> {{#if @isModalActive}}
<section class="modal-card-body"> <Hds::Modal id="transit-export-modal" @onClose={{fn (mut @isModalActive) false}} as |M|>
<div class="box is-shadowless is-fullwidth is-sideless"> <M.Header>
<h2 class="title is-6">Wrapped Key</h2> Copy your wrapped key
</M.Header>
<M.Body>
<h2 class="title is-6">Wrapped key</h2>
{{#if this.wrapTTL}} {{#if this.wrapTTL}}
<Hds::Copy::Snippet <Hds::Copy::Snippet
class="has-bottom-margin-m" class="has-bottom-margin-m"
@textToCopy={{@wrappedToken}} @textToCopy={{@wrappedToken}}
@color="secondary" @color="secondary"
@container="#transit-export-modal"
data-test-encrypted-value="export" data-test-encrypted-value="export"
/> />
{{else}} {{else}}
@@ -85,13 +89,14 @@
@text="Copy" @text="Copy"
@isIconOnly={{true}} @isIconOnly={{true}}
@textToCopy={{stringify @keys}} @textToCopy={{stringify @keys}}
@container="#transit-export-modal"
class="transparent top-right-absolute" class="transparent top-right-absolute"
/> />
</div> </div>
{{/if}} {{/if}}
</div> </M.Body>
</section> <M.Footer as |F|>
<footer class="modal-card-foot"> <Hds::Button @text="Close" {{on "click" F.close}} />
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> </M.Footer>
</footer> </Hds::Modal>
</Modal> {{/if}}

View File

@@ -51,14 +51,22 @@
</div> </div>
</div> </div>
</form> </form>
<Modal @title="Copy your unwrapped data" @onClose={{action (mut @isModalActive) false}} @isActive={{@isModalActive}}> {{#if @isModalActive}}
<section class="modal-card-body"> <Hds::Modal id="transit-hmac-modal" @onClose={{fn (mut @isModalActive) false}} as |M|>
<div class="box is-shadowless is-fullwidth is-sideless"> <M.Header>
Copy your unwrapped data
</M.Header>
<M.Body>
<h2 class="title is-6">HMAC</h2> <h2 class="title is-6">HMAC</h2>
<Hds::Copy::Snippet @textToCopy={{@hmac}} @color="secondary" data-test-encrypted-value="hmac" /> <Hds::Copy::Snippet
</div> @textToCopy={{@hmac}}
</section> @color="secondary"
<footer class="modal-card-foot"> @container="#transit-hmac-modal"
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> data-test-encrypted-value="hmac"
</footer> />
</Modal> </M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -63,14 +63,17 @@
</div> </div>
</div> </div>
</form> </form>
<Modal @title="Copy your token" @onClose={{action (mut @isModalActive)}} @isActive={{@isModalActive}}> {{#if @isModalActive}}
<section class="modal-card-body"> <Hds::Modal id="transit-rewrap-modal" @onClose={{fn (mut @isModalActive) false}} as |M|>
<div class="box is-shadowless is-fullwidth is-sideless"> <M.Header>
Copy your token
</M.Header>
<M.Body>
<h2 class="title is-6">Ciphertext</h2> <h2 class="title is-6">Ciphertext</h2>
<Hds::Copy::Snippet @textToCopy={{@ciphertext}} @color="secondary" /> <Hds::Copy::Snippet @textToCopy={{@ciphertext}} @color="secondary" @container="#transit-rewrap-modal" />
</div> </M.Body>
</section> <M.Footer as |F|>
<footer class="modal-card-foot"> <Hds::Button @text="Close" {{on "click" F.close}} />
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> </M.Footer>
</footer> </Hds::Modal>
</Modal> {{/if}}

View File

@@ -120,19 +120,22 @@
</div> </div>
</div> </div>
</form> </form>
<Modal {{#if @isModalActive}}
@title="Copy your signature" <Hds::Modal id="transit-sign-modal" @onClose={{fn (mut @isModalActive) false}} data-test-sign-modal as |M|>
@onClose={{action (mut @isModalActive) false}} <M.Header>
@isActive={{@isModalActive}} Copy your signature
data-test-sign-modal </M.Header>
> <M.Body>
<section class="modal-card-body">
<div class="box is-shadowless is-fullwidth is-sideless">
<h2 class="title is-6">Signature</h2> <h2 class="title is-6">Signature</h2>
<Hds::Copy::Snippet @textToCopy={{@signature}} @color="secondary" data-test-encrypted-value="signature" /> <Hds::Copy::Snippet
</div> @textToCopy={{@signature}}
</section> @color="secondary"
<footer class="modal-card-foot"> @container="#transit-sign-modal"
<button type="button" class="button is-primary" {{on "click" (fn (mut @isModalActive) false)}}>Close</button> data-test-encrypted-value="signature"
</footer> />
</Modal> </M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -192,16 +192,25 @@
</div> </div>
</div> </div>
</form> </form>
<Modal @title="Results" @onClose={{action (mut @isModalActive) false}} @isActive={{@isModalActive}}> {{#if @isModalActive}}
<section class="modal-card-body"> <Hds::Modal id="transit-verify-modal" @size="small" @onClose={{fn (mut @isModalActive) false}} as |M|>
<Hds::Alert @type="inline" @color={{if @valid "success" "critical"}} as |A|> <M.Header>
<A.Title>{{if @valid "Valid" "Not Valid"}}</A.Title> Results
<A.Description> <Hds::Badge
The input is @text={{if @valid "Valid" "Not Valid"}}
{{if @valid "valid" "not valid"}} @size="large"
for the given @color={{if @valid "success" "critical"}}
{{if @signature "signature." "HMAC."}} @icon={{if @valid "check-circle" "x-circle"}}
</A.Description> />
</Hds::Alert> </M.Header>
</section> <M.Body>
</Modal> The input is
{{if @valid "valid" "not valid"}}
for the given
{{if @signature "signature." "HMAC."}}
</M.Body>
<M.Footer as |F|>
<Hds::Button @text="Close" {{on "click" F.close}} />
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -95,15 +95,13 @@
</div> </div>
{{/if}} {{/if}}
<DownloadButton <DownloadButton
class="button is-ghost" @color="tertiary"
@filename={{this.keyFilename}} @filename={{this.keyFilename}}
@data={{this.keyData}} @data={{this.keyData}}
@extension="json" @extension="json"
@stringify={{true}} @stringify={{true}}
> @text="Download keys"
<Icon @name="download" /> />
Download keys
</DownloadButton>
</div> </div>
</div> </div>
</Page.content> </Page.content>

View File

@@ -29,14 +29,13 @@
<Toolbar> <Toolbar>
<ToolbarActions> <ToolbarActions>
<DownloadButton <DownloadButton
class="toolbar-link" class="toolbar-button"
@color="secondary"
@filename={{this.model.name}} @filename={{this.model.name}}
@data={{this.model.policy}} @data={{this.model.policy}}
@extension={{if (eq this.policyType "acl") this.model.format "sentinel"}} @extension={{if (eq this.policyType "acl") this.model.format "sentinel"}}
> @text="Download policy"
Download policy />
<Chevron @isButton={{true}} />
</DownloadButton>
{{#if (and (not-eq this.model.id "root") (or this.capabilities.canUpdate this.capabilities.canDelete))}} {{#if (and (not-eq this.model.id "root") (or this.capabilities.canUpdate this.capabilities.canDelete))}}
<ToolbarLink @route="vault.cluster.policy.edit" @model={{this.model.id}} data-test-policy-edit-toggle> <ToolbarLink @route="vault.cluster.policy.edit" @model={{this.model.id}} data-test-policy-edit-toggle>
Edit policy Edit policy

View File

@@ -19,7 +19,7 @@
) )
}} }}
</p> </p>
<h4 class="field-title has-bottom-padding-m is-fullwidth"> <h4 class="has-text-weight-bold is-size-7 has-bottom-padding-m is-fullwidth">
{{concat "PGP Key " this.pgpKeyFile.filename}} {{concat "PGP Key " this.pgpKeyFile.filename}}
</h4> </h4>
<Hds::Copy::Snippet <Hds::Copy::Snippet
@@ -27,6 +27,7 @@
@textToCopy={{this.pgpKey}} @textToCopy={{this.pgpKey}}
@color="secondary" @color="secondary"
data-test-pgp-key-copy data-test-pgp-key-copy
@container="#shamir-flow-modal"
/> />
</div> </div>
<div class="field is-grouped"> <div class="field is-grouped">

View File

@@ -14,5 +14,6 @@
@text="Copy" @text="Copy"
@textToCopy={{or @clipboardCode @codeBlock}} @textToCopy={{or @clipboardCode @codeBlock}}
@isIconOnly={{@isIconOnly}} @isIconOnly={{@isIconOnly}}
@container={{@container}}
/> />
</div> </div>

View File

@@ -3,40 +3,45 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<Modal @title={{@title}} @onClose={{@onClose}} @isActive={{@isActive}} @type={{this.type}} @showCloseButton={{true}}> {{#if @isActive}}
<section class="modal-card-body"> <Hds::Modal id="confirmation-modal" @onClose={{@onClose}} @color="critical" as |M|>
{{yield}} <M.Header data-test-confirmation-modal-title @icon="alert-triangle">
<div class="modal-confirm-section"> {{@title}}
<p class="has-text-weight-semibold is-size-6"> </M.Header>
Confirm <M.Body>
</p> {{yield}}
<p class="sub-text has-top-bottom-margin-xxs">Type <div class="has-top-padding-m">
<strong>{{this.confirmText}}</strong> <p class="has-text-weight-semibold is-size-6">
to confirm Confirm
{{this.toConfirmMsg}}</p> </p>
<Input <p class="sub-text has-top-bottom-margin-xxs">Type
@type="text" <strong>{{this.confirmText}}</strong>
@value={{this.confirmationInput}} to confirm
name="confirmationInput" {{@toConfirmMsg}}
class="input has-margin-top" </p>
autocomplete="off" <Input
spellcheck="false" @type="text"
data-test-confirmation-modal-input={{or @title true}} @value={{this.confirmationInput}}
/> name="confirmationInput"
</div> class="input has-margin-top"
</section> autocomplete="off"
<footer class="modal-card-foot modal-card-foot-outlined"> spellcheck="false"
<button data-test-confirmation-modal-input={{@title}}
type="button" />
class="button {{this.buttonClass}}" </div>
disabled={{not-eq this.confirmationInput this.confirmText}} </M.Body>
{{on "click" @onConfirm}} <M.Footer>
data-test-confirm-button={{or @title true}} <Hds::ButtonSet>
> <Hds::Button
{{this.buttonText}} @color="critical"
</button> type="button"
<button type="button" class="button is-secondary" {{on "click" @onClose}} data-test-cancel-button> disabled={{not-eq this.confirmationInput this.confirmText}}
Cancel {{on "click" @onConfirm}}
</button> data-test-confirm-button={{@title}}
</footer> @text={{or @buttonText "Confirm"}}
</Modal> />
<Hds::Button @text="Cancel" @color="secondary" {{on "click" @onClose}} data-test-cancel-button />
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}

View File

@@ -2,11 +2,11 @@
* Copyright (c) HashiCorp, Inc. * Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import Component from '@glimmer/component'; import Component from '@glimmer/component';
/** /**
* @module ConfirmationModal * @module ConfirmationModal
* ConfirmationModal components are used to provide an alternative to ConfirmationButton that automatically prompts the user to fill in confirmation text before they can continue with a potentially destructive action. It is built off the Modal component * ConfirmationModal components wrap the <Hds::Modal> component to present a critical (red) type-to-confirm modal.
* They are used for extremely destructive actions that require extra consideration before confirming.
* *
* @example * @example
* ```js * ```js
@@ -15,6 +15,7 @@ import Component from '@glimmer/component';
* @title="Do Dangerous Thing?" * @title="Do Dangerous Thing?"
* @isActive={{isModalActive}} * @isActive={{isModalActive}}
* @onClose={{action (mut isModalActive) false}} * @onClose={{action (mut isModalActive) false}}
* @confirmText="yes"
* @onConfirmMsg="deleting this thing to delete." * @onConfirmMsg="deleting this thing to delete."
* /> * />
* ``` * ```
@@ -23,30 +24,12 @@ import Component from '@glimmer/component';
* @param {boolean} isActive - Controls whether the modal is "active" eg. visible or not. * @param {boolean} isActive - Controls whether the modal is "active" eg. visible or not.
* @param {string} title - Title of the modal * @param {string} title - Title of the modal
* @param {string} [confirmText=Yes] - The confirmation text that the user must type before continuing * @param {string} [confirmText=Yes] - The confirmation text that the user must type before continuing
* @param {string} [toConfirmMsg=''] - Finishes the sentence "Type <confirmText> to confirm <toConfirmMsg>", default is an empty string (ex. 'secret deletion') * @param {string} [toConfirmMsg] - Finishes the sentence "Type <confirmText> to confirm <toConfirmMsg>", default is an empty string (ex. 'secret deletion')
* @param {string} [buttonText=Confirm] - Button text on the confirm button * @param {string} [buttonText=Confirm] - Button text on the confirm button
* @param {string} [buttonClass=is-danger] - extra class to add to confirm button (eg. "is-danger")
* @param {string} [type=warning] - The header styling based on type, passed into the message-types helper (in the Modal component).
*/ */
export default class ConfirmationModal extends Component { export default class ConfirmationModal extends Component {
get buttonClass() {
return this.args.buttonClass || 'is-danger';
}
get buttonText() {
return this.args.buttonText || 'Confirm';
}
get confirmText() { get confirmText() {
return this.args.confirmText || 'Yes'; return this.args.confirmText || 'Yes';
} }
get type() {
return this.args.type || 'warning';
}
get toConfirmMsg() {
return this.args.toConfirmMsg || '';
}
} }

View File

@@ -3,6 +3,13 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<button data-test-download-button type="button" {{on "click" this.handleDownload}} ...attributes> <Hds::Button
{{yield}} data-test-download-button
</button> @icon={{if @hideIcon "" "download"}}
@text={{or @text "Download"}}
@color={{@color}}
@iconPosition={{or @iconPosition "leading"}}
@isIconOnly={{@isIconOnly}}
{{on "click" this.handleDownload}}
...attributes
/>

View File

@@ -12,22 +12,20 @@ import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug'; import { assert } from '@ember/debug';
/** /**
* @module DownloadButton * @module DownloadButton
* DownloadButton components are an action button used to download data. Both the action text and icon are yielded. * DownloadButton wraps an <Hds::Button> to perform a download action.
* * NOTE: when using in an engine, remember to add the 'download' service to its dependencies (in /engine.js) and map to it in /app.js * * NOTE: when using in an engine, remember to add the 'download' service to its dependencies (in /engine.js) and map to it in /app.js
* [ember-docs](https://ember-engines.com/docs/services) * [ember-docs](https://ember-engines.com/docs/services)
* @example * @example
* ```js * ```js
* <DownloadButton * <DownloadButton
* class="button" * @text="Download this stuff"
* @color="secondary"
* @data={{this.data}} * @data={{this.data}}
* @filename={{this.filename}} * @filename={{this.filename}}
* @mime={{this.mime}} * @mime={{this.mime}}
* @extension={{this.extension}} * @extension={{this.extension}}
* @stringify={{true}} * @stringify={{true}}
* > * />
* <Icon @name="download" />
* Download
* </DownloadButton>
* ``` * ```
* @param {string} [filename] - name of file that prefixes the ISO timestamp generated at download * @param {string} [filename] - name of file that prefixes the ISO timestamp generated at download
* @param {string} [data] - data to download * @param {string} [data] - data to download
@@ -35,6 +33,12 @@ import { assert } from '@ember/debug';
* @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype * @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype
* @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor * @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor
* @param {callback} [onSuccess] - callback from parent to invoke if download is successful * @param {callback} [onSuccess] - callback from parent to invoke if download is successful
* @param {boolean} [hideIcon=false] - renders the 'download' icon by default, pass true to hide (ex: when download button appears in a dropdown)
* * HDS ARGS https://helios.hashicorp.design/components/button?tab=code
* @param {string} [text="Download"] - button text, defaults to 'Download'
* @param {string} [color] - HDS default is primary, but there are four color options: primary, secondary, tertiary, and critical.
* @param {string} [iconPosition="leading"] - icon position, 'leading' (HDS default) or 'trailing'
* @param {boolean} [isIconOnly] - button only renders an icon, no text
*/ */
export default class DownloadButton extends Component { export default class DownloadButton extends Component {

View File

@@ -40,7 +40,7 @@
@text="Copy" @text="Copy"
@isIconOnly={{true}} @isIconOnly={{true}}
@textToCopy={{@value}} @textToCopy={{@value}}
class="transparent icon-only is-paddingless" class="transparent has-padding-xxs"
data-test-copy-button data-test-copy-button
/> />
{{/if}} {{/if}}

View File

@@ -39,14 +39,20 @@
@text="Copy" @text="Copy"
@isIconOnly={{true}} @isIconOnly={{true}}
@textToCopy={{@value}} @textToCopy={{@value}}
class="transparent icon-only is-paddingless" class="transparent has-padding-xxs"
data-test-copy-button data-test-copy-button
/> />
{{/if}} {{/if}}
{{#if @allowDownload}} {{#if @allowDownload}}
<button type="button" class="button download-button" {{on "click" (fn (mut this.modalOpen) true)}}> <Hds::Button
<Icon data-test-download-icon @name="download" /> @text="Download secret value"
</button> @icon="download"
@isIconOnly={{true}}
@color="tertiary"
class="has-padding-xxs"
data-test-download-icon
{{on "click" (fn (mut this.modalOpen) true)}}
/>
{{/if}} {{/if}}
<button <button
onclick={{this.toggleMask}} onclick={{this.toggleMask}}
@@ -61,31 +67,25 @@
</div> </div>
{{! CONFIRM DOWNLOAD MODAL }} {{! CONFIRM DOWNLOAD MODAL }}
{{#if @allowDownload}} {{#if this.modalOpen}}
<Modal <Hds::Modal @color="warning" id="confirm-download-modal" @onClose={{fn (mut this.modalOpen) false}} as |M|>
@title="Download secret value?" <M.Header @icon="alert-triangle">
@onClose={{action (mut this.modalOpen) false}} Download secret value?
@isActive={{this.modalOpen}} </M.Header>
@type="warning" <M.Body>
>
<section class="modal-card-body">
This download is This download is
<strong>unencrypted</strong>. Are you sure you want to download this secret data as plaintext? <strong>unencrypted</strong>. Are you sure you want to download this secret data as plaintext?
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer as |F|>
<DownloadButton <Hds::ButtonSet>
class="button is-primary" <DownloadButton
@filename={{or @name "secret-value"}} @filename={{or @name "secret-value"}}
@data={{@value}} @data={{@value}}
@stringify={{true}} @stringify={{true}}
aria-label="Download secret value" @onSuccess={{fn (mut this.modalOpen) false}}
@onSuccess={{fn (mut this.modalOpen) false}} />
> <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} />
Download </Hds::ButtonSet>
</DownloadButton> </M.Footer>
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.modalOpen) false)}}> </Hds::Modal>
Cancel
</button>
</footer>
</Modal>
{{/if}} {{/if}}

View File

@@ -1,37 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<EmberWormhole @to="modal-wormhole">
<div class="{{this.modalClass}} {{if this.isActive 'is-active'}}" data-test-modal-div ...attributes>
<div class="modal-background" role="button" {{on "click" @onClose}} data-test-modal-background={{@title}}></div>
<div class="modal-card">
<header class="modal-card-head">
<h2 class="modal-card-title title is-5" data-test-modal-title>
{{#if this.glyph}}
<Icon
class={{this.glyph.glyphClass}}
aria-hidden="true"
@name={{this.glyph.glyph}}
data-test-modal-glyph={{this.glyph.glyph}}
/>
{{/if}}
<span class={{if this.glyph "has-left-padding-xs"}}>{{capitalize @title}}</span>
</h2>
{{#if this.showCloseButton}}
<button
type="button"
class="icon-button has-text-grey"
aria-label="close"
{{on "click" @onClose}}
data-test-modal-close-button
>
<Icon @name="x-circle" class="close-icon" />
</button>
{{/if}}
</header>
{{yield}}
</div>
</div>
</EmberWormhole>

View File

@@ -1,55 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { messageTypes } from 'core/helpers/message-types';
/**
* @module Modal
* Modal components are used to overlay content on top of the page. Has a darkened background,
* a title, and in order to close it you must pass an onClose function.
*
* @example
* ```js
* <Modal
* @title="Export attribution data"
* @type="info"
* @isActive={{this.showModal}}
* @showCloseButton={{true}}
* @onClose={{action (mut this.showModal) false}}
* >
* Whatever content pops up when the modal isActive!
* </Modal>
* ```
* @callback onClose
* @param {onClose} onClose - onClose is the action taken when someone clicks the modal background or close button (if shown).
* @param {boolean} isActive=false - whether or not modal displays
* @param {string} [title] - This text shows up in the header section of the modal. Only the first word should be capitalized.
* @param {boolean} [showCloseButton=false] - controls whether the close button in the top right corner shows.
* @param {string} [type=null] - The header styling based on type passed into the message-types helper.
*/
export default class ModalComponent extends Component {
get isActive() {
return this.args.isActive || false;
}
get showCloseButton() {
return this.args.showCloseButton || false;
}
get glyph() {
if (!this.args.type) {
return null;
}
return messageTypes([this.args.type]);
}
get modalClass() {
if (!this.args.type) {
return 'modal';
}
return 'modal ' + messageTypes([this.args.type]).class;
}
}

View File

@@ -42,7 +42,14 @@
</p> </p>
{{/if}} {{/if}}
</div> </div>
<JsonEditor @value={{get this.policyTemplates @policyType}} @mode="ruby" @readOnly={{true}} @showToolbar={{true}} /> <JsonEditor
@value={{get this.policyTemplates @policyType}}
@mode="ruby"
@readOnly={{true}}
@showToolbar={{true}}
{{! Passed to copy button }}
@container={{@container}}
/>
<div class="has-bottom-margin-m has-top-padding-s"> <div class="has-bottom-margin-m has-top-padding-s">
<p> <p>
More information about More information about

View File

@@ -14,28 +14,11 @@ import Component from '@glimmer/component';
* @example * @example
* <PolicyExample * <PolicyExample
* @policyType={{@model.policyType}} * @policyType={{@model.policyType}}
* @container="#search-select-modal"
* /> * />
*
* @example (in modal)
* <Modal
* @onClose={{fn (mut this.showTemplateModal) false}}
* @isActive={{this.showTemplateModal}}
* >
* <section class="modal-card-body">
* {{! code-mirror modifier does not render value initially until focus event fires }}
* {{! wait until the Modal is rendered and then show the PolicyExample (contains JsonEditor) }}
* {{#if this.showTemplateModal}}
* <PolicyExample @policyType={{@model.policyType}}/>
* {{/if}}
* </section>
* <div class="modal-card-head has-border-top-light">
* <button type="button" class="button" {{on "click" (fn (mut this.showTemplateModal) false)}} data-test-close-modal>
* Close
* </button>
* </div>
* </Modal>
* ``` * ```
* @param {string} policyType - policy type to decide which template to render; can either be "acl" or "rgp" * @param {string} policyType - policy type to decide which template to render; can either be "acl" or "rgp"
* @param {string} container - selector for the container the example renders inside, passed to the copy button in JsonEditor
*/ */
export default class PolicyExampleComponent extends Component { export default class PolicyExampleComponent extends Component {

View File

@@ -83,32 +83,24 @@
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
{{/if}} {{/if}}
{{! wait until user has selected 'create a new item' before rendering modal}} {{#if this.showModal}}
{{#if this.nameInput}} <Hds::Modal id="search-select-modal" @onClose={{fn (mut this.showModal) false}} as |M|>
<Modal <M.Header data-test-modal-title>
@title="Create new {{singularize @id}}" Create new
@onClose={{action (mut this.showModal) false}} {{singularize @id}}
@isActive={{this.showModal}} </M.Header>
@type="info" <M.Body>
@showCloseButton={{false}}
>
<section class="modal-card-body">
{{#if @modalSubtext}} {{#if @modalSubtext}}
<p class="has-bottom-margin-s" data-test-modal-subtext> <p class="has-bottom-margin-s" data-test-modal-subtext>
{{@modalSubtext}} {{@modalSubtext}}
</p> </p>
{{/if}} {{/if}}
{{#if (has-block)}} {{! dynamically render template from modal-form/ folder}}
{{yield}} {{! form must receive an @onSave and @onCancel arg that executes the callback}}
{{else}} {{component @modalFormTemplate nameInput=this.nameInput onSave=this.resetModal onCancel=this.resetModal}}
{{! dynamically render template from modal-form/ folder}} </M.Body>
{{! form must receive an @onSave and @onCancel arg that executes the callback}} </Hds::Modal>
{{component @modalFormTemplate nameInput=this.nameInput onSave=this.resetModal onCancel=this.resetModal}}
{{/if}}
</section>
</Modal>
{{/if}} {{/if}}
</div> </div>

View File

@@ -43,6 +43,7 @@
@options={{this.dropdownOptions}} @options={{this.dropdownOptions}}
@onChange={{this.selectOrCreate}} @onChange={{this.selectOrCreate}}
@placeholderComponent={{component "search-select-placeholder"}} @placeholderComponent={{component "search-select-placeholder"}}
@renderInPlace={{@renderInPlace}}
@verticalPosition="below" @verticalPosition="below"
@disabled={{@disabled}} @disabled={{@disabled}}
as |option| as |option|

View File

@@ -53,6 +53,7 @@ import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-ut
* @param {string} [wildcardLabel] - string (singular) for rendering label tag beside a wildcard selection (i.e. 'role*'), for the number of items it includes, e.g. @wildcardLabel="role" -> "includes 4 roles" * @param {string} [wildcardLabel] - string (singular) for rendering label tag beside a wildcard selection (i.e. 'role*'), for the number of items it includes, e.g. @wildcardLabel="role" -> "includes 4 roles"
* @param {string} [placeholder] - text you wish to replace the default "search" with * @param {string} [placeholder] - text you wish to replace the default "search" with
* @param {boolean} [displayInherit=false] - if you need the search select component to display inherit instead of box. * @param {boolean} [displayInherit=false] - if you need the search select component to display inherit instead of box.
* @param {boolean} [renderInPlace] - pass `true` when power select renders in a modal
* @param {function} [renderInfoTooltip] - receives each inputValue string and list of dropdownOptions as args, so parent can determine when to render a tooltip beside a selectedOption and the tooltip text. see 'oidc/provider-form.js' * @param {function} [renderInfoTooltip] - receives each inputValue string and list of dropdownOptions as args, so parent can determine when to render a tooltip beside a selectedOption and the tooltip text. see 'oidc/provider-form.js'
* @param {boolean} [disabled] - if true sets the disabled property on the ember-power-select component and makes it unusable. * @param {boolean} [disabled] - if true sets the disabled property on the ember-power-select component and makes it unusable.
* *

View File

@@ -2,130 +2,131 @@
Copyright (c) HashiCorp, Inc. Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
{{! THIS COMPONENT RENDERS INSIDE A MODAL (must pass @container to copy buttons) }}
<section class="modal-card-body"> {{#if this.encodedToken}}
{{#if this.encodedToken}} <p class="has-bottom-margin-l" data-test-dr-token-flow-step="show-token">
<p class="has-bottom-margin-l" data-test-dr-token-flow-step="show-token"> Below is the process and the values necessary to generate your operation token. Read the instructions carefully!
Below is the process and the values necessary to generate your operation token. Read the instructions carefully! </p>
</p> <div class="has-bottom-margin-m">
<div class="has-bottom-margin-m"> <div class="has-bottom-margin-xl">
<div class="has-bottom-margin-xl"> <h4 class="has-text-weight-bold is-size-7">
<h4 class="field-title"> Encoded operation token
Encoded operation token </h4>
</h4> <p class="help has-text-grey has-bottom-margin-xs">
<p class="help has-text-grey has-bottom-margin-xs"> This is a one-time token that will be used to generate the operation token. Please save it.
This is a one-time token that will be used to generate the operation token. Please save it.
</p>
<Hds::Copy::Snippet @textToCopy={{this.encodedToken}} data-test-shamir-encoded-token />
</div>
{{#if this.otp}}
<div class="has-bottom-margin-xl">
<h4 class="field-title">
One time password (OTP)
</h4>
<p class="help has-text-grey has-bottom-margin-xs">
This OTP will be used to decode the generated operation token. Please save it.
</p>
<Hds::Copy::Snippet @textToCopy={{this.otp}} />
</div>
{{/if}}
<div class="has-bottom-margin-xl">
<h4 class="field-title">
DR operation token command
</h4>
<p class="help has-text-grey has-bottom-margin-xs">
{{#if this.otp}}
This command contains both the encoded token and the OTP. It should be executed on the secondary cluster in order
to generate the operation token.
{{else}}
This command requires the OTP saved earlier. It should be executed on the secondary cluster in order to generate
the operation token.
{{/if}}
</p>
{{! template-lint-disable quotes }}
{{#let
(if
this.otp
(concat 'vault operator generate-root -dr-token -otp="' this.otp '" -decode="' this.encodedToken '"')
(concat 'vault operator generate-root -dr-token -otp="<enter your otp here>" -decode="' this.encodedToken '"')
)
as |cmd|
}}
<CodeSnippet @codeBlock={{cmd}} />
{{/let}}
{{! template-lint-enable quotes }}
</div>
</div>
<div>
<button type="button" class="button is-primary" {{on "click" this.onCancelClose}}>
Clear &amp; Close
</button>
</div>
{{else if this.started}}
<Shamir::Form
@action={{@action}}
@progress={{this.progress}}
@threshold={{this.threshold}}
@errors={{this.errors}}
@inputLabel="Root key portion"
@buttonText="Generate token"
@onSubmit={{this.onSubmitKey}}
@otp={{this.otp}}
@alwaysShowProgress={{true}}
data-test-dr-token-flow-step="shamir"
>
<p>Generate an operation token by entering a portion of the
<strong>primary's root key</strong>. Once all portions are entered, the generated token may be used to manage your
secondary Disaster Recovery cluster.
</p> </p>
</Shamir::Form> <Hds::Copy::Snippet @textToCopy={{this.encodedToken}} @container="#shamir-flow-modal" data-test-shamir-encoded-token />
{{else if this.generateWithPGP}} </div>
<ChoosePgpKeyForm {{#if this.otp}}
@onCancel={{fn (mut this.generateWithPGP) false}} <div class="has-bottom-margin-xl">
@onSubmit={{this.usePgpKey}} <h4 class="has-text-weight-bold is-size-7">
@formText={{this.pgpText.form}} One time password (OTP)
@confirmText={{this.pgpText.confirm}} </h4>
@buttonText="Generate operation token" <p class="help has-text-grey has-bottom-margin-xs">
data-test-dr-token-flow-step="choose-pgp" This OTP will be used to decode the generated operation token. Please save it.
/>
{{else}}
{{! Generate token flow not started }}
<form
{{on "submit" this.startGenerate}}
id="shamir"
aria-label="shamir generate form"
data-test-dr-token-flow-step="begin"
>
<MessageError @errors={{this.errors}} />
<div class="has-bottom-margin-m" data-test-shamir-modal-body>
<p>
Updating or promoting this cluster requires an operation token, generated by inputting the root key shares. If
you'd like to first encrypt the token with a PGP Key, click "Encrypt with PGP key" below, otherwise we can begin
generation of the operation token.
</p> </p>
<Hds::Copy::Snippet @textToCopy={{this.otp}} @container="#shamir-flow-modal" />
</div> </div>
<div class="field is-grouped is-flex-center"> {{/if}}
<div class="control is-flex-row"> <div class="has-bottom-margin-xl">
<button type="button" class="link" {{on "click" (fn (mut this.generateWithPGP) true)}} data-test-use-pgp-key-cta> <h4 class="has-text-weight-bold is-size-7">
Provide PGP Key DR operation token command
</button> </h4>
</div> <p class="help has-text-grey has-bottom-margin-xs">
<div class="control"> {{#if this.otp}}
<span class="has-side-padding-s"> This command contains both the encoded token and the OTP. It should be executed on the secondary cluster in order
or to generate the operation token.
</span> {{else}}
</div> This command requires the OTP saved earlier. It should be executed on the secondary cluster in order to generate
<div class="control"> the operation token.
<button type="submit" class="button is-primary" data-test-generate-token-cta> {{/if}}
Generate operation token </p>
</button> {{! template-lint-disable quotes }}
</div> {{#let
(if
this.otp
(concat 'vault operator generate-root -dr-token -otp="' this.otp '" -decode="' this.encodedToken '"')
(concat 'vault operator generate-root -dr-token -otp="<enter your otp here>" -decode="' this.encodedToken '"')
)
as |cmd|
}}
<CodeSnippet @codeBlock={{cmd}} @container="#shamir-flow-modal" />
{{/let}}
{{! template-lint-enable quotes }}
</div>
</div>
<div>
<Hds::Button {{on "click" this.onCancelClose}} @text="Clear & Close" />
</div>
{{else if this.started}}
<Shamir::Form
@action={{@action}}
@progress={{this.progress}}
@threshold={{this.threshold}}
@errors={{this.errors}}
@inputLabel="Root key portion"
@buttonText="Generate token"
@onSubmit={{this.onSubmitKey}}
@otp={{this.otp}}
@alwaysShowProgress={{true}}
data-test-dr-token-flow-step="shamir"
>
<p>Generate an operation token by entering a portion of the
<strong>primary's root key</strong>. Once all portions are entered, the generated token may be used to manage your
secondary Disaster Recovery cluster.
</p>
</Shamir::Form>
{{else if this.generateWithPGP}}
<ChoosePgpKeyForm
@onCancel={{fn (mut this.generateWithPGP) false}}
@onSubmit={{this.usePgpKey}}
@formText={{this.pgpText.form}}
@confirmText={{this.pgpText.confirm}}
@buttonText="Generate operation token"
data-test-dr-token-flow-step="choose-pgp"
/>
{{else}}
{{! Generate token flow not started }}
<form
{{on "submit" this.startGenerate}}
id="shamir"
aria-label="shamir generate form"
data-test-dr-token-flow-step="begin"
>
<MessageError @errors={{this.errors}} />
<div class="has-bottom-margin-m">
<p>
Updating or promoting this cluster requires an operation token, generated by inputting the root key shares. If you'd
like to first encrypt the token with a PGP Key, click "Encrypt with PGP key" below, otherwise we can begin generation
of the operation token.
</p>
</div>
<div class="field is-grouped is-flex-center">
<div class="control is-flex-row">
<Hds::Button
@color="tertiary"
@icon="key"
{{on "click" (fn (mut this.generateWithPGP) true)}}
data-test-use-pgp-key-cta
@text="Provide PGP Key"
/>
</div> </div>
</form> <div class="control">
{{/if}} <span class="has-side-padding-s">
</section> or
<footer class="modal-card-foot modal-card-foot-outlined"> </span>
<button type="button" class="button is-secondary" {{on "click" this.onCancelClose}} data-test-shamir-modal-cancel-button> </div>
{{if this.encodedToken "Close" "Cancel"}} <div class="control">
</button> <Hds::Button type="submit" data-test-generate-token-cta @text="Generate operation token" />
</footer> </div>
</div>
</form>
{{/if}}
<hr class="has-background-gray-100" />
<Hds::Button
@color="secondary"
{{on "click" this.onCancelClose}}
data-test-shamir-modal-cancel-button
@text={{if this.encodedToken "Close" "Cancel"}}
/>

View File

@@ -21,7 +21,7 @@
<h4 class="hds-alert__title hds-font-weight-semibold"> <h4 class="hds-alert__title hds-font-weight-semibold">
One Time Password (otp) One Time Password (otp)
</h4> </h4>
<Hds::Copy::Snippet data-test-otp @textToCopy={{@otp}} @color="secondary" /> <Hds::Copy::Snippet data-test-otp @textToCopy={{@otp}} @color="secondary" @container="#shamir-flow-modal" />
</A.Description> </A.Description>
</Hds::Alert> </Hds::Alert>
{{/if}} {{/if}}

View File

@@ -37,11 +37,6 @@ export const MESSAGE_TYPES = {
glyph: 'loading', glyph: 'loading',
text: 'Loading', text: 'Loading',
}, },
rotation: {
class: 'is-info',
glyphClass: 'has-text-grey',
glyph: 'rotate-cw',
},
}; };
export function messageTypes([type]) { export function messageTypes([type]) {

View File

@@ -19,7 +19,7 @@
type="button" type="button"
class="button is-secondary" class="button is-secondary"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-replication-action-trigger data-test-replication-action-trigger="demote"
> >
Demote Demote
</button> </button>
@@ -30,7 +30,6 @@
@title="Demote to secondary?" @title="Demote to secondary?"
@onClose={{action (mut this.isModalActive) false}} @onClose={{action (mut this.isModalActive) false}}
@isActive={{this.isModalActive}} @isActive={{this.isModalActive}}
@buttonClass="is-primary"
@confirmText={{this.model.replicationModeForDisplay}} @confirmText={{this.model.replicationModeForDisplay}}
@toConfirmMsg="demoting this cluster" @toConfirmMsg="demoting this cluster"
@onConfirm={{action "onSubmit" "demote" this.model.replicationAttrs.modeForUrl}} @onConfirm={{action "onSubmit" "demote" this.model.replicationAttrs.modeForUrl}}

View File

@@ -20,7 +20,7 @@
type="button" type="button"
class="button is-danger" class="button is-danger"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-replication-action-trigger data-test-replication-action-trigger="disable"
> >
Disable Replication Disable Replication
</button> </button>

View File

@@ -25,16 +25,14 @@
</div> </div>
</div> </div>
<Modal {{#if this.isModalActive}}
@title="Generate operation token" <Hds::Modal id="shamir-flow-modal" @color="warning" @onClose={{action (mut this.isModalActive) false}} as |M|>
@onClose={{action (mut this.isModalActive) false}} <M.Header>
@isActive={{this.isModalActive}} Generate operation token
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> <Shamir::DrTokenFlow @action="generate-dr-operation-token" @onCancel={{action (mut this.isModalActive) false}} />
{{#if this.isModalActive}} </M.Body>
{{! Wrapped in if statement so the Shamir constructor fires on modal open }}
<Shamir::DrTokenFlow @action="generate-dr-operation-token" @onCancel={{action (mut this.isModalActive) false}} />
{{! Section & Footer is in child component since the form must do side effects on cancel }} {{! Section & Footer is in child component since the form must do side effects on cancel }}
{{/if}} </Hds::Modal>
</Modal> {{/if}}

View File

@@ -20,107 +20,106 @@
type="button" type="button"
class="button is-tertiary" class="button is-tertiary"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-replication-action-trigger data-test-replication-action-trigger="promote"
> >
Promote Promote
</button> </button>
</div> </div>
</div> </div>
<Modal {{#if this.isModalActive}}
@title="Promote cluster?" <Hds::Modal id="replication-promote-modal" @color="warning" @onClose={{fn (mut this.isModalActive) false}} as |M|>
@onClose={{action (mut this.isModalActive) false}} <M.Header @icon="alert-triangle">
@isActive={{this.isModalActive}} Promote cluster?
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
>
<section class="modal-card-body">
{{#if (eq this.replicationMode "dr")}}
<p class="has-bottom-margin-m">
To promote this DR Replication Secondary to a primary, enter the DR Operation token.
</p>
{{/if}}
<p class="has-bottom-margin-m">
Vault Replication is not designed for active-active usage. Enabling two primaries should never be done, as it can lead
to data loss if they or their secondaries are ever reconnected. If the cluster has a primary, be sure to demote it
before promoting a secondary.
</p>
<div data-test-promote-dr-inputs>
{{#if (eq this.replicationMode "dr")}} {{#if (eq this.replicationMode "dr")}}
<div class="field is-borderless"> <p class="has-bottom-margin-m">
<label for="dr_operation_token_promote" class="is-label is-size-6"> To promote this DR Replication Secondary to a primary, enter the DR Operation token.
DR Operation Token </p>
{{/if}}
<p class="has-bottom-margin-m">
Vault Replication is not designed for active-active usage. Enabling two primaries should never be done, as it can
lead to data loss if they or their secondaries are ever reconnected. If the cluster has a primary, be sure to demote
it before promoting a secondary.
</p>
<div data-test-promote-dr-inputs>
{{#if (eq this.replicationMode "dr")}}
<div class="field is-borderless">
<label for="dr_operation_token_promote" class="is-label is-size-6">
DR Operation Token
</label>
<div class="control">
<Input
class="input"
id="dr_operation_token_promote"
name="dr_operation_token_promote"
@value={{this.dr_operation_token_promote}}
/>
</div>
</div>
{{/if}}
<div class="field">
<label for="primary_cluster_addr" class="is-label is-size-6">
Primary cluster address
<em class="is-optional">(optional)</em>
</label> </label>
<div class="control"> <div class="control">
<Input <Input
class="input" class="input"
id="dr_operation_token_promote" id="primary_cluster_addr"
name="dr_operation_token_promote" name="primary_cluster_addr"
@value={{this.dr_operation_token_promote}} @value={{this.primary_cluster_addr}}
/> />
</div> </div>
</div> <p class="help">
{{/if}} Overrides the cluster address that the primary gives to secondary nodes.
<div class="field">
<label for="primary_cluster_addr" class="is-label is-size-6">
Primary cluster address
<em class="is-optional">(optional)</em>
</label>
<div class="control">
<Input class="input" id="primary_cluster_addr" name="primary_cluster_addr" @value={{this.primary_cluster_addr}} />
</div>
<p class="help">
Overrides the cluster address that the primary gives to secondary nodes.
</p>
</div>
<div class="field">
<div class="b-checkbox">
<input
type="checkbox"
id="forcePromote"
class="styled"
checked={{this.force}}
onchange={{action (mut this.force) value="target.checked"}}
/>
<label for="forcePromote" class="is-label is-size-6">
Force promotion of this cluster
</label>
<p>
Promote the cluster even if certain safety checks fail. This could result in data loss of data isn't fully
replicated
</p> </p>
</div> </div>
<div class="field">
<div class="b-checkbox">
<input
type="checkbox"
id="forcePromote"
class="styled"
checked={{this.force}}
onchange={{action (mut this.force) value="target.checked"}}
/>
<label for="forcePromote" class="is-label is-size-6">
Force promotion of this cluster
</label>
<p>
Promote the cluster even if certain safety checks fail. This could result in data loss of data isn't fully
replicated
</p>
</div>
</div>
</div> </div>
</div> </M.Body>
</section> <M.Footer as |F|>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::ButtonSet>
<button <Hds::Button
type="button" disabled={{if (and (eq this.replicationMode "dr") (not this.dr_operation_token_promote)) true}}
class="button is-primary" {{on
disabled={{if (and (eq this.replicationMode "dr") (not this.dr_operation_token_promote)) true}} "click"
onclick={{action (fn
"onSubmit" this.onSubmit
"promote" "promote"
this.model.replicationAttrs.modeForUrl this.model.replicationAttrs.modeForUrl
(hash (hash
dr_operation_token_promote=this.dr_operation_token_promote dr_operation_token_promote=this.dr_operation_token_promote
primary_cluster_addr=this.primary_cluster_addr primary_cluster_addr=this.primary_cluster_addr
force=this.force force=this.force
) )
}} )
data-test-promote-confirm-button }}
> data-test-confirm-button
Promote @text="Promote"
</button> />
<button <Hds::Button @color="secondary" @text="Cancel" {{on "click" F.close}} data-test-promote-cancel-button />
type="button" </Hds::ButtonSet>
class="button is-secondary" </M.Footer>
onclick={{action (mut this.isModalActive) false}} </Hds::Modal>
data-test-promote-cancel-button {{/if}}
>
Cancel
</button>
</footer>
</Modal>

View File

@@ -18,36 +18,28 @@
type="button" type="button"
class="button is-secondary" class="button is-secondary"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-replication-action-trigger data-test-replication-action-trigger="recover"
> >
Recover Recover
</button> </button>
</div> </div>
</div> </div>
<Modal {{#if this.isModalActive}}
@title="Begin recovery?" <Hds::Modal id="replication-recover-modal" @color="warning" @onClose={{fn (mut this.isModalActive) false}} as |M|>
@onClose={{action (mut this.isModalActive) false}} <M.Header @icon="alert-triangle">
@isActive={{this.isModalActive}} Begin recovery?
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> <p>
<section class="modal-card-body"> If replication is in an adverse state, we can begin recovery. This will attempt to recover to continue syncing.
<p> </p>
If replication is in an adverse state, we can begin recovery. This will attempt to recover to continue syncing. </M.Body>
</p> <M.Footer as |F|>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button @text="Recover" {{on "click" (fn this.onSubmit "recover")}} data-test-confirm-button />
<button type="button" class="button is-primary" onclick={{action "onSubmit" "recover"}} data-test-recover-confirm-button> <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-recover-cancel-button />
Recover </Hds::ButtonSet>
</button> </M.Footer>
<button </Hds::Modal>
type="button" {{/if}}
class="button is-secondary"
onclick={{action (mut this.isModalActive) false}}
data-test-recover-cancel-button
>
Cancel
</button>
</footer>
</Modal>

View File

@@ -17,41 +17,33 @@
type="button" type="button"
class="button is-secondary" class="button is-secondary"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-replication-action-trigger data-test-replication-action-trigger="reindex"
> >
Reindex Reindex
</button> </button>
</div> </div>
</div> </div>
<Modal {{#if this.isModalActive}}
@title="Begin reindex?" <Hds::Modal id="replication-reindex-modal" @color="warning" @onClose={{fn (mut this.isModalActive) false}} as |M|>
@onClose={{action (mut this.isModalActive) false}} <M.Header @icon="alert-triangle">
@isActive={{this.isModalActive}} Begin reindex?
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> <p class="has-bottom-margin-m">
<section class="modal-card-body"> Reindexing can cause a very long delay depending on the number and size of objects in the data store.
<p class="has-bottom-margin-m"> {{if this.model.replicationAttrs.isPrimary "You should always re-index your secondary first."}}
Reindexing can cause a very long delay depending on the number and size of objects in the data store. </p>
{{if this.model.replicationAttrs.isPrimary "You should always re-index your secondary first."}} <p>
</p> Progress will be shown, and you will
<p> {{if this.model.replicationAttrs.isPrimary "not"}}
Progress will be shown, and you will be able to use Vault during this time.
{{if this.model.replicationAttrs.isPrimary "not"}} </p>
be able to use Vault during this time. </M.Body>
</p> <M.Footer as |F|>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button @text="Reindex" {{on "click" (fn this.onSubmit "reindex")}} data-test-confirm-button />
<button type="button" class="button is-primary" onclick={{action "onSubmit" "reindex"}} data-test-reindex-confirm-button> <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-reindex-cancel-button />
Reindex </Hds::ButtonSet>
</button> </M.Footer>
<button </Hds::Modal>
type="button" {{/if}}
class="button is-secondary"
onclick={{action (mut this.isModalActive) false}}
data-test-reindex-cancel-button
>
Cancel
</button>
</footer>
</Modal>

View File

@@ -18,117 +18,111 @@
type="button" type="button"
class="button is-secondary" class="button is-secondary"
onclick={{action (mut this.isModalActive) true}} onclick={{action (mut this.isModalActive) true}}
data-test-update-primary-action-trigger data-test-replication-action-trigger="update-primary"
> >
Update Update
</button> </button>
</div> </div>
</div> </div>
<Modal {{#if this.isModalActive}}
@title="Update primary" <Hds::Modal id="replication-update-primary-modal" @color="warning" @onClose={{fn (mut this.isModalActive) false}} as |M|>
@onClose={{action (mut this.isModalActive) false}} <M.Header @icon="alert-triangle">
@isActive={{this.isModalActive}} Update primary
@type="warning" </M.Header>
@showCloseButton={{true}} <M.Body>
> <p class="has-bottom-margin-m">
<section class="modal-card-body"> Use a secondary activation token to change this secondarys assigned primary. This does not wipe all data in the
<p class="has-bottom-margin-m"> cluster.
Use a secondary activation token to change this secondarys assigned primary. This does not wipe all data in the </p>
cluster.
</p>
<div data-test-update-primary-inputs> <div data-test-update-primary-inputs>
{{#if (eq this.replicationMode "dr")}} {{#if (eq this.replicationMode "dr")}}
<div class="field">
<label for="dr_operation_token_primary" class="is-label">
DR operation token
</label>
<div class="control">
<Input
class="input"
id="dr_operation_token_primary"
name="dr_operation_token_primary"
@value={{this.dr_operation_token_primary}}
/>
</div>
</div>
{{/if}}
<div class="field"> <div class="field">
<label for="dr_operation_token_primary" class="is-label"> <label for="secondary-token" class="is-label">
DR operation token Secondary activation token
</label> </label>
<div class="control"> <div class="control">
<Input <Textarea @value={{this.token}} id="secondary-token" name="secondary-token" class="textarea" />
class="input"
id="dr_operation_token_primary"
name="dr_operation_token_primary"
@value={{this.dr_operation_token_primary}}
/>
</div> </div>
</div> </div>
{{/if}} <div class="field">
<div class="field"> <label for="primary_api_addr" class="is-label">
<label for="secondary-token" class="is-label"> Primary API address
Secondary activation token <em class="is-optional">(optional)</em>
</label> </label>
<div class="control"> <div class="control">
<Textarea @value={{this.token}} id="secondary-token" name="secondary-token" class="textarea" /> <Input class="input" @value={{this.primary_api_addr}} id="primary_api_addr" name="primary_api_addr" />
</div>
<p class="help">
Set this to the API address (normal Vault address) to override the value embedded in the token.
</p>
</div>
<div class="field">
<label for="ca_file" class="is-label">
CA file
<em class="is-optional">(optional)</em>
</label>
<div class="control">
<Input @value={{this.ca_file}} id="ca_file" name="ca_file" class="input" />
</div>
<p class="help">
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the
primary.
</p>
</div>
<div class="field">
<label for="ca_path" class="is-label">
CA path
<em class="is-optional">(optional)</em>
</label>
<div class="control">
<Input @value={{this.ca_path}} id="ca_path" name="ca_file" class="input" />
</div>
<p class="help">
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping
the token from the primary.
</p>
</div> </div>
</div> </div>
<div class="field"> </M.Body>
<label for="primary_api_addr" class="is-label"> <M.Footer as |F|>
Primary API address <Hds::ButtonSet>
<em class="is-optional">(optional)</em> <Hds::Button
</label> {{on
<div class="control"> "click"
<Input class="input" @value={{this.primary_api_addr}} id="primary_api_addr" name="primary_api_addr" /> (fn
</div> this.onSubmit
<p class="help"> "update-primary"
Set this to the API address (normal Vault address) to override the value embedded in the token. this.model.replicationAttrs.modeForUrl
</p> (hash
</div> token=this.token
<div class="field"> dr_operation_token_primary=this.dr_operation_token_primary
<label for="ca_file" class="is-label"> primary_api_addr=this.primary_api_addr
CA file ca_path=this.ca_path
<em class="is-optional">(optional)</em> ca_file=this.ca_file
</label> )
<div class="control"> )
<Input @value={{this.ca_file}} id="ca_file" name="ca_file" class="input" /> }}
</div> data-test-confirm-button
<p class="help"> @text="Update"
Specifies the path to a CA root file (PEM format) that the secondary can use when unwrapping the token from the />
primary. <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-update-primary-cancel-button />
</p> </Hds::ButtonSet>
</div> </M.Footer>
<div class="field"> </Hds::Modal>
<label for="ca_path" class="is-label"> {{/if}}
CA path
<em class="is-optional">(optional)</em>
</label>
<div class="control">
<Input @value={{this.ca_path}} id="ca_path" name="ca_file" class="input" />
</div>
<p class="help">
Specifies the path to a CA root directory containing PEM-format files that the secondary can use when unwrapping
the token from the primary.
</p>
</div>
</div>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<button
type="button"
class="button is-primary"
onclick={{action
"onSubmit"
"update-primary"
this.model.replicationAttrs.modeForUrl
(hash
token=this.token
dr_operation_token_primary=this.dr_operation_token_primary
primary_api_addr=this.primary_api_addr
ca_path=this.ca_path
ca_file=this.ca_file
)
}}
data-test-confirm-action-trigger
>
Update
</button>
<button
type="button"
class="button is-secondary"
onclick={{action (mut this.isModalActive) false}}
data-test-update-primary-cancel-button
>
Cancel
</button>
</footer>
</Modal>

View File

@@ -1,6 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/modal';

View File

@@ -24,7 +24,6 @@
"ember-router-helpers": "*", "ember-router-helpers": "*",
"ember-svg-jar": "*", "ember-svg-jar": "*",
"ember-truth-helpers": "*", "ember-truth-helpers": "*",
"ember-wormhole": "*",
"escape-string-regexp": "*", "escape-string-regexp": "*",
"@hashicorp/ember-flight-icons": "*", "@hashicorp/ember-flight-icons": "*",
"@hashicorp/flight-icons": "*", "@hashicorp/flight-icons": "*",

View File

@@ -8,14 +8,13 @@
<ToolbarActions> <ToolbarActions>
{{#if this.model}} {{#if this.model}}
<DownloadButton <DownloadButton
class="toolbar-link" class="toolbar-button"
@color="secondary"
@filename={{concat this.model.ca.id "-ca"}} @filename={{concat this.model.ca.id "-ca"}}
@data={{this.model.ca.caPem}} @data={{this.model.ca.caPem}}
@extension="pem" @extension="pem"
> @text="Download CA cert"
Download CA cert />
<Chevron @isButton={{true}} />
</DownloadButton>
{{/if}} {{/if}}
<ToolbarLink @route="configure" data-test-kmip-link-configure> <ToolbarLink @route="configure" data-test-kmip-link-configure>
Configure Configure

View File

@@ -103,26 +103,21 @@
</div> </div>
{{#if this.showConfirm}} {{#if this.showConfirm}}
<Modal <Hds::Modal id="kubernetes-edit-config-modal" @onClose={{fn (mut this.showConfirm) false}} @color="warning" as |M|>
@title="Edit configuration" <M.Header @icon="alert-triangle">
@type="warning" Edit configuration
@isActive={{this.showConfirm}} </M.Header>
@showCloseButton={{true}} <M.Body data-test-edit-config-body>
@onClose={{fn (mut this.showConfirm) false}}
>
<section class="modal-card-body">
<p> <p>
Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are
you sure? you sure?
</p> </p>
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer as |F|>
<button data-test-config-confirm type="button" class="button is-primary" {{on "click" (perform this.save)}}> <Hds::ButtonSet>
Confirm <Hds::Button data-test-config-confirm {{on "click" (perform this.save)}} @text="Confirm" />
</button> <Hds::Button {{on "click" F.close}} @color="secondary" @text="Cancel" />
<button type="button" class="button" onclick={{fn (mut this.showConfirm) false}}> </Hds::ButtonSet>
Cancel </M.Footer>
</button> </Hds::Modal>
</footer>
</Modal>
{{/if}} {{/if}}

View File

@@ -1,16 +1,23 @@
<button type="button" class="toolbar-link" {{on "click" (fn (mut this.modalOpen) true)}} data-test-kv-delete={{@mode}}> <Hds::Button
{{yield}} @text={{or @text (capitalize @mode)}}
</button> @color="secondary"
class="toolbar-button"
{{on "click" (fn (mut this.modalOpen) true)}}
data-test-kv-delete={{@mode}}
/>
{{#if this.modalOpen}} {{#if this.modalOpen}}
<Modal <Hds::Modal
@title={{this.modalDisplay.title}} id="kv-delete-modal-{{@mode}}"
@color={{this.modalDisplay.color}}
@onClose={{fn (mut this.modalOpen) false}} @onClose={{fn (mut this.modalOpen) false}}
@isActive={{this.modalOpen}}
@type={{this.modalDisplay.type}}
@showCloseButton={{true}}
data-test-delete-modal data-test-delete-modal
as |M|
> >
<section class="modal-card-body"> <M.Header data-test-modal-title @icon="trash">
{{this.modalDisplay.title}}
</M.Header>
<M.Body>
<p class="has-bottom-margin-s"> <p class="has-bottom-margin-s">
{{this.modalDisplay.intro}} {{this.modalDisplay.intro}}
</p> </p>
@@ -45,19 +52,19 @@
{{/each}} {{/each}}
</div> </div>
{{/if}} {{/if}}
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer as |F|>
<button <Hds::ButtonSet>
type="button" <Hds::Button
class="button {{if (eq this.modalDisplay.type 'danger') 'is-danger-outlined' 'is-warning-outlined'}}" @text="Confirm"
{{on "click" this.onDelete}} {{on "click" this.onDelete}}
data-test-delete-modal-confirm @color={{if (eq this.modalDisplay.color "critical") this.modalDisplay.color}}
> data-test-delete-modal-confirm
Confirm />
</button>
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.modalOpen) false)}}> <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} />
Cancel
</button> </Hds::ButtonSet>
</footer> </M.Footer>
</Modal> </Hds::Modal>
{{/if}} {{/if}}

View File

@@ -21,6 +21,7 @@ import { assert } from '@ember/debug';
* @param {string} mode - delete, delete-metadata, or destroy. * @param {string} mode - delete, delete-metadata, or destroy.
* @param {object} secret - The kv/data model. * @param {object} secret - The kv/data model.
* @param {object} [metadata] - The kv/metadata model. It is only required when mode is "delete" or "metadata-delete". * @param {object} [metadata] - The kv/metadata model. It is only required when mode is "delete" or "metadata-delete".
* @param {string} [text] - Button text that renders in KV v2 toolbar, defaults to capitalize @mode
* @param {callback} onDelete - callback function fired to handle delete event. * @param {callback} onDelete - callback function fired to handle delete event.
*/ */
@@ -34,20 +35,20 @@ export default class KvDeleteModal extends Component {
case 'delete': case 'delete':
return { return {
title: 'Delete version?', title: 'Delete version?',
type: 'warning', color: 'warning',
intro: intro:
'There are two ways to delete a version of a secret. Both delete actions can be undeleted later. How would you like to proceed?', 'There are two ways to delete a version of a secret. Both delete actions can be undeleted later. How would you like to proceed?',
}; };
case 'destroy': case 'destroy':
return { return {
title: 'Destroy version?', title: 'Destroy version?',
type: 'danger', color: 'critical',
intro: `This action will permanently destroy Version ${this.args.version} of the secret, and the secret data cannot be read or recovered later.`, intro: `This action will permanently destroy Version ${this.args.version} of the secret, and the secret data cannot be read or recovered later.`,
}; };
case 'delete-metadata': case 'delete-metadata':
return { return {
title: 'Delete metadata and secret data?', title: 'Delete metadata and secret data?',
type: 'danger', color: 'critical',
intro: intro:
'This will permanently delete the metadata and versions of the secret. All version history will be removed. This cannot be undone.', 'This will permanently delete the metadata and versions of the secret. All version history will be removed. This cannot be undone.',
}; };

View File

@@ -34,14 +34,10 @@
@metadata={{@metadata}} @metadata={{@metadata}}
@onDelete={{this.handleDestruction}} @onDelete={{this.handleDestruction}}
@version={{this.version}} @version={{this.version}}
> />
Delete
</KvDeleteModal>
{{/if}} {{/if}}
{{#if this.showDestroy}} {{#if this.showDestroy}}
<KvDeleteModal @mode="destroy" @secret={{@secret}} @onDelete={{this.handleDestruction}} @version={{this.version}}> <KvDeleteModal @mode="destroy" @secret={{@secret}} @onDelete={{this.handleDestruction}} @version={{this.version}} />
Destroy
</KvDeleteModal>
{{/if}} {{/if}}
{{#if (or @secret.canReadData @secret.canReadMetadata @secret.canEditData)}} {{#if (or @secret.canReadData @secret.canReadMetadata @secret.canEditData)}}
<div class="toolbar-separator"></div> <div class="toolbar-separator"></div>

View File

@@ -10,9 +10,12 @@
<:toolbarActions> <:toolbarActions>
{{#if @secret.canDeleteMetadata}} {{#if @secret.canDeleteMetadata}}
<KvDeleteModal @mode="delete-metadata" @metadata={{@metadata}} @onDelete={{this.onDelete}}> <KvDeleteModal
Permanently delete @mode="delete-metadata"
</KvDeleteModal> @metadata={{@metadata}}
@onDelete={{this.onDelete}}
@text="Permanently delete"
/>
{{/if}} {{/if}}
{{#if @secret.canUpdateMetadata}} {{#if @secret.canUpdateMetadata}}
<ToolbarLink @route="secret.metadata.edit" data-test-edit-metadata>Edit metadata</ToolbarLink> <ToolbarLink @route="secret.metadata.edit" data-test-edit-metadata>Edit metadata</ToolbarLink>

View File

@@ -39,37 +39,35 @@
</OverviewCard> </OverviewCard>
{{#if this.selectedStatus}} {{#if this.selectedStatus}}
<Modal <Hds::Modal id="account-check-in-modal" @onClose={{fn (mut this.selectedStatus) undefined}} as |M|>
@title="Account Check-in" <M.Header>
@isActive={{this.selectedStatus}} Account Check-in
@showCloseButton={{true}} </M.Header>
@onClose={{fn (mut this.selectedStatus) undefined}} <M.Body>
>
<section class="modal-card-body">
<p> <p>
This action will check-in account This action will check-in account
{{this.selectedStatus.account}} {{this.selectedStatus.account}}
back to the library. Do you want to proceed? back to the library. Do you want to proceed?
</p> </p>
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer>
<button <Hds::ButtonSet>
type="button" <Hds::Button
class="button is-primary {{if this.save.isRunning 'is-loading'}}" @icon={{if this.save.isRunning "is-loading"}}
disabled={{this.checkIn.isRunning}} disabled={{this.checkIn.isRunning}}
data-test-check-in-confirm data-test-check-in-confirm
{{on "click" (perform this.checkIn)}} {{on "click" (perform this.checkIn)}}
> @text="Confirm"
Confirm />
</button> <Hds::Button
<button @icon={{if this.save.isRunning "is-loading"}}
type="button" @color="secondary"
class="button" disabled={{this.checkIn.isRunning}}
disabled={{this.checkIn.isRunning}} {{on "click" (fn (mut this.selectedStatus) "")}}
{{on "click" (fn (mut this.selectedStatus) "")}} @text="Cancel"
> />
Cancel
</button> </Hds::ButtonSet>
</footer> </M.Footer>
</Modal> </Hds::Modal>
{{/if}} {{/if}}

View File

@@ -78,14 +78,11 @@
</form> </form>
{{#if this.showRotatePrompt}} {{#if this.showRotatePrompt}}
<Modal <Hds::Modal id="ldap-rotate-password-modal" @onClose={{fn (mut this.showRotatePrompt) false}} as |M|>
@title="Rotate your root password?" <M.Header @icon="info">
@type="info" Rotate your root password?
@isActive={{this.showRotatePrompt}} </M.Header>
@showCloseButton={{true}} <M.Body>
@onClose={{fn (mut this.showRotatePrompt) false}}
>
<section class="modal-card-body">
<p> <p>
Its best practice to rotate the administrator (root) password immediately after the initial configuration of the Its best practice to rotate the administrator (root) password immediately after the initial configuration of the
LDAP engine. The rotation will update the password both in Vault and your directory server. Once rotated, LDAP engine. The rotation will update the password both in Vault and your directory server. Once rotated,
@@ -95,19 +92,17 @@
<p> <p>
Would you like to rotate your new credentials? You can also do this later. Would you like to rotate your new credentials? You can also do this later.
</p> </p>
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer>
<button <Hds::ButtonSet>
data-test-save-with-rotate <Hds::Button data-test-save-with-rotate @text="Save and rotate" {{on "click" (fn (perform this.save) null true)}} />
type="button" <Hds::Button
class="button is-primary" data-test-save-without-rotate
{{on "click" (fn (perform this.save) null true)}} @text="Save without rotating"
> @color="secondary"
Save and rotate {{on "click" (fn (perform this.save) null false)}}
</button> />
<button data-test-save-without-rotate type="button" class="button" {{on "click" (fn (perform this.save) null false)}}> </Hds::ButtonSet>
Save without rotating </M.Footer>
</button> </Hds::Modal>
</footer>
</Modal>
{{/if}} {{/if}}

View File

@@ -54,32 +54,23 @@
</div> </div>
{{#if this.showCheckOutPrompt}} {{#if this.showCheckOutPrompt}}
<Modal <Hds::Modal id="account-check-out-modal" @onClose={{fn (mut this.showCheckOutPrompt) false}} as |M|>
@title="Account Check-out" <M.Header>
@isActive={{this.showCheckOutPrompt}} Account Check-out
@showCloseButton={{true}} </M.Header>
@onClose={{fn (mut this.showCheckOutPrompt) false}} <M.Body>
>
<section class="modal-card-body">
<p> <p>
Current generated credentials time-to-live is set at Current generated credentials time-to-live is set at
{{format-duration @library.ttl}}. You can set a different limit if youd like: {{format-duration @library.ttl}}. You can set a different limit if youd like:
</p> </p>
<br /> <br />
<TtlPicker @label="TTL" @hideToggle={{true}} @initialValue={{@library.ttl}} @onChange={{this.setTtl}} /> <TtlPicker @label="TTL" @hideToggle={{true}} @initialValue={{@library.ttl}} @onChange={{this.setTtl}} />
</section> </M.Body>
<footer class="modal-card-foot modal-card-foot-outlined"> <M.Footer as |F|>
<button data-test-check-out="save" type="button" class="button is-primary" {{on "click" this.checkOut}}> <Hds::ButtonSet>
Check-out <Hds::Button data-test-check-out="save" @text="Check-out" {{on "click" this.checkOut}} />
</button> <Hds::Button data-test-check-out="cancel" @text="Cancel" @color="secondary" {{on "click" F.close}} />
<button </Hds::ButtonSet>
data-test-check-out="cancel" </M.Footer>
type="button" </Hds::Modal>
class="button"
{{on "click" (fn (mut this.showCheckOutPrompt) false)}}
>
Cancel
</button>
</footer>
</Modal>
{{/if}} {{/if}}

View File

@@ -111,19 +111,18 @@
{{#if this.showDeleteAllIssuers}} {{#if this.showDeleteAllIssuers}}
<ConfirmationModal <ConfirmationModal
@title="Delete All Issuers?" @title="Delete all issuers?"
@toConfirmMsg="deleting all issuers and keys." @toConfirmMsg="deleting all issuers and keys."
@buttonText="Confirm"
@confirmText="delete-all" @confirmText="delete-all"
@isActive={{this.showDeleteAllIssuers}} @isActive={{this.showDeleteAllIssuers}}
@onClose={{action (mut this.showDeleteAllIssuers) false}} @onClose={{action (mut this.showDeleteAllIssuers) false}}
@onConfirm={{this.deleteAllIssuers}} @onConfirm={{this.deleteAllIssuers}}
> >
<section class="modal-card-custom"> <p>
This endpoint deletes This endpoint deletes
<strong>all</strong> <strong>all</strong>
issuers and keys within the mount. It is highly recommended to use the individual delete operations instead. This mount issuers and keys within the mount. It is highly recommended to use the individual delete operations instead. This mount
will be unusable until new issuers and keys are provisioned. will be unusable until new issuers and keys are provisioned.
</section> </p>
</ConfirmationModal> </ConfirmationModal>
{{/if}} {{/if}}

View File

@@ -48,9 +48,9 @@
@data={{@pem}} @data={{@pem}}
@extension="pem" @extension="pem"
data-test-issuer-download-type="pem" data-test-issuer-download-type="pem"
> @text="PEM format"
PEM format @hideIcon={{true}}
</DownloadButton> />
</li> </li>
{{/if}} {{/if}}
{{#if @der}} {{#if @der}}
@@ -62,9 +62,9 @@
@data={{@der}} @data={{@der}}
@extension="der" @extension="der"
data-test-issuer-download-type="der" data-test-issuer-download-type="der"
> @text="DER format"
DER format @hideIcon={{true}}
</DownloadButton> />
</li> </li>
{{/if}} {{/if}}
</ul> </ul>
@@ -148,45 +148,37 @@
{{/if}} {{/if}}
{{! ROOT ROTATION MODAL }} {{! ROOT ROTATION MODAL }}
<Modal {{#if this.showRotationModal}}
@type="rotation" <Hds::Modal id="pki-rotate-root-modal" @size="large" @onClose={{fn (mut this.showRotationModal) false}} as |M|>
@title="Rotate this root" <M.Header @icon="rotate-cw">
@onClose={{fn (mut this.showRotationModal) false}} Rotate this root
@isActive={{this.showRotationModal}} </M.Header>
@showCloseButton={{true}} <M.Body>
> <h3 class="title is-5">Root rotation</h3>
<section class="modal-card-body"> <p class="has-text-grey has-bottom-padding-s">
<h3 class="title is-5">Root rotation</h3> Root rotation is an impactful process. Please be ready to ensure that the new root is properly distributed to
<p class="has-text-grey has-bottom-padding-s"> end-users trust stores. You can also do this manually by
Root rotation is an impactful process. Please be ready to ensure that the new root is properly distributed to <DocLink @path="/vault/docs/secrets/pki/rotation-primitives#suggested-root-rotation-procedure">
end-users trust stores. You can also do this manually by following our documentation.
<DocLink @path="/vault/docs/secrets/pki/rotation-primitives#suggested-root-rotation-procedure"> </DocLink>
following our documentation. </p>
</DocLink> <h3 class="title is-5 has-top-bottom-margin">How root rotation will work</h3>
</p> <p class="has-text-grey">
<h3 class="title is-5 has-top-bottom-margin">How root rotation will work</h3> <ol class="has-left-margin-m has-bottom-margin-s">
<p class="has-text-grey"> <li>The new root will be generated using defaults from the old one that you can customize.</li>
<ol class="has-left-margin-m has-bottom-margin-s"> <li>You will identify intermediates, which Vault will then cross-sign.</li>
<li>The new root will be generated using defaults from the old one that you can customize.</li> </ol>
<li>You will identify intermediates, which Vault will then cross-sign.</li> Then, you can begin re-issuing leaf certs and phase out the old root.
</ol> </p>
Then, you can begin re-issuing leaf certs and phase out the old root. <div class="has-top-margin-l has-tall-padding">
</p> <img src={{img-path "~/pki-rotate-root.png"}} alt="pki root rotation diagram" />
<div class="has-top-margin-l has-tall-padding"> </div>
<img src={{img-path "~/pki-rotate-root.png"}} alt="pki root rotation diagram" /> </M.Body>
</div> <M.Footer as |F|>
</section> <Hds::ButtonSet>
<footer class="modal-card-foot modal-card-foot-outlined"> <Hds::Button @text="Generate new root" @route="issuers.issuer.rotate-root" data-test-root-rotate-step-one />
<button <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} />
type="button" </Hds::ButtonSet>
class="button is-primary" </M.Footer>
{{on "click" (transition-to "vault.cluster.secrets.backend.pki.issuers.issuer.rotate-root")}} </Hds::Modal>
data-test-root-rotate-step-one {{/if}}
>
Generate new root
</button>
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.showRotationModal) false)}}>
Cancel
</button>
</footer>
</Modal>

View File

@@ -53,9 +53,9 @@
@extension="pem" @extension="pem"
@fetchData={{fn this.fetchDataForDownload "pem"}} @fetchData={{fn this.fetchDataForDownload "pem"}}
data-test-issuer-download-type="pem" data-test-issuer-download-type="pem"
> @text="PEM format"
PEM format @hideIcon={{true}}
</DownloadButton> />
</li> </li>
<li class="action"> <li class="action">
<DownloadButton <DownloadButton
@@ -64,9 +64,9 @@
@extension="der" @extension="der"
@fetchData={{fn this.fetchDataForDownload "der"}} @fetchData={{fn this.fetchDataForDownload "der"}}
data-test-issuer-download-type="der" data-test-issuer-download-type="der"
> @text="DER format"
DER format @hideIcon={{true}}
</DownloadButton> />
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@@ -19,14 +19,13 @@
{{/if}} {{/if}}
{{#if @key.privateKey}} {{#if @key.privateKey}}
<DownloadButton <DownloadButton
class="toolbar-link" class="toolbar-button"
@color="secondary"
@filename="{{@key.backend}}-{{or @key.keyName 'private-key'}}" @filename="{{@key.backend}}-{{or @key.keyName 'private-key'}}"
@data={{@key.privateKey}} @data={{@key.privateKey}}
@extension="pem" @extension="pem"
> @text="Download private key"
Download private key />
<Chevron @isButton={{true}} />
</DownloadButton>
{{/if}} {{/if}}
{{#if @canEdit}} {{#if @canEdit}}
<ToolbarLink @route="keys.key.edit" @model={{@key.keyId}} data-test-pki-key-edit> <ToolbarLink @route="keys.key.edit" @model={{@key.keyId}} data-test-pki-key-edit>

View File

@@ -6,7 +6,7 @@
<PkiPaginatedList @listRoute="keys.index" @list={{@keyModels}} @hasConfig={{@hasConfig}}> <PkiPaginatedList @listRoute="keys.index" @list={{@keyModels}} @hasConfig={{@hasConfig}}>
<:actions> <:actions>
{{#if @canImportKey}} {{#if @canImportKey}}
<ToolbarLink @route="keys.import" @type="download" data-test-pki-key-import> <ToolbarLink @route="keys.import" @type="upload" data-test-pki-key-import>
Import Import
</ToolbarLink> </ToolbarLink>
{{/if}} {{/if}}

View File

@@ -14,15 +14,15 @@
Perform manual tidy Perform manual tidy
</ToolbarLink> </ToolbarLink>
{{else}} {{else}}
<button <Hds::Button
type="button" class="toolbar-button"
class="toolbar-link" @color="secondary"
@icon="chevron-right"
@iconPosition="trailing"
{{on "click" (fn (mut this.tidyOptionsModal) true)}} {{on "click" (fn (mut this.tidyOptionsModal) true)}}
data-test-pki-tidy-options-modal data-test-pki-tidy-options-modal
> @text="Tidy"
Tidy />
<Icon @name="chevron-right" />
</button>
{{/if}} {{/if}}
</ToolbarActions> </ToolbarActions>
</Toolbar> </Toolbar>
@@ -101,104 +101,75 @@
@title="Tidy status unavailable" @title="Tidy status unavailable"
@message="After the next tidy operation has been performed, information about the current or most recent tidy operation will display here." @message="After the next tidy operation has been performed, information about the current or most recent tidy operation will display here."
> >
<button <Hds::Button
type="button" @color="tertiary"
class="link" @icon="chevron-right"
@iconPosition="trailing"
{{on "click" (fn (mut this.tidyOptionsModal) true)}} {{on "click" (fn (mut this.tidyOptionsModal) true)}}
data-test-tidy-empty-state-configure data-test-tidy-empty-state-configure
> @text="Tidy"
Tidy />
</button>
</EmptyState> </EmptyState>
{{/if}} {{/if}}
{{! TIDY OPTIONS MODAL }} {{! TIDY OPTIONS MODAL }}
<Modal {{#if this.tidyOptionsModal}}
@title="Tidy this mount" <Hds::Modal id="pki-tidy-modal" @size="large" @onClose={{fn (mut this.tidyOptionsModal) false}} as |M|>
@onClose={{fn (mut this.tidyOptionsModal) false}} <M.Header>
@isActive={{this.tidyOptionsModal}} Tidy this mount
@showCloseButton={{true}} </M.Header>
> <M.Body>
<section aria-label="tidy-options-modal-content" class="modal-card-body"> <h3 class="title is-5">How tidying will work</h3>
<h3 class="title is-5">How tidying will work</h3> <p class="has-text-grey has-bottom-padding-s">
<p class="has-text-grey has-bottom-padding-s"> Tidying cleans up the storage backend and/or CRL by removing certificates that have expired and are past a certain
Tidying cleans up the storage backend and/or CRL by removing certificates that have expired and are past a certain buffer period beyond their expiration time.
buffer period beyond their expiration time. <DocLink @path="/vault/docs/secrets/pki/considerations#automate-crl-building-and-tidying">
<DocLink @path="/vault/docs/secrets/pki/considerations#automate-crl-building-and-tidying"> Documentation.
Documentation. </DocLink>
</DocLink> </p>
</p> <p class="has-text-grey">
<p class="has-text-grey"> <ol class="has-left-margin-m has-bottom-margin-s">
<ol class="has-left-margin-m has-bottom-margin-s"> <li>Select a tidy operation:</li>
<li>Select a tidy operation:</li> <ul class="bullet has-bottom-margin-xs">
<ul class="bullet has-bottom-margin-xs"> <li><strong>Automatic tidy</strong>
<li><strong>Automatic tidy</strong> periodically runs a tidy operation with saved configuration settings after waiting the specified interval
periodically runs a tidy operation with saved configuration settings after waiting the specified interval duration between tidies
duration between tidies </li>
</li> <li><strong>Manual tidy</strong> runs a tidy operation once</li>
<li><strong>Manual tidy</strong> runs a tidy operation once</li> </ul>
</ul> <li>Configure the parameters that determine how to tidy and run the operation.</li>
<li>Configure the parameters that determine how to tidy and run the operation.</li> </ol>
</ol> </p>
</p> <div class="has-top-margin-l has-padding">
<div class="has-top-margin-l has-padding"> <img src={{img-path "~/pki-tidy.png"}} alt="tidy operation diagram" />
<img src={{img-path "~/pki-tidy.png"}} alt="tidy operation diagram" /> </div>
</div> </M.Body>
</section> <M.Footer as |F|>
<footer aria-label="tidy-option-buttons" class="modal-card-foot modal-card-foot-outlined"> <Hds::ButtonSet>
<button <Hds::Button @text="Automatic tidy" @route="tidy.auto.configure" data-test-tidy-modal-auto-button />
type="button" <Hds::Button @text="Manual tidy" @route="tidy.manual" data-test-tidy-modal-manual-button />
class="button is-primary" <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-tidy-modal-cancel-button />
{{on "click" (transition-to "vault.cluster.secrets.backend.pki.tidy.auto.configure")}} </Hds::ButtonSet>
data-test-tidy-modal-auto-button </M.Footer>
> </Hds::Modal>
Automatic tidy {{/if}}
</button>
<button
type="button"
class="button is-primary"
{{on "click" (transition-to "vault.cluster.secrets.backend.pki.tidy.manual")}}
data-test-tidy-modal-manual-button
>
Manual tidy
</button>
<button
type="button"
class="button is-secondary"
{{on "click" (fn (mut this.tidyOptionsModal) false)}}
data-test-tidy-modal-cancel-button
>
Cancel
</button>
</footer>
</Modal>
{{! CANCEL TIDY CONFIRMATION MODAL }} {{! CANCEL TIDY CONFIRMATION MODAL }}
{{#if this.confirmCancelTidy}} {{#if this.confirmCancelTidy}}
<Modal <Hds::Modal id="pki-cancel-tidy-modal" @color="warning" @onClose={{fn (mut this.confirmCancelTidy) false}} as |M|>
@type="warning" <M.Header @icon="alert-triangle">
@title="Cancel tidy?" Cancel tidy?
@onClose={{fn (mut this.confirmCancelTidy) false}} </M.Header>
@isActive={{this.confirmCancelTidy}} <M.Body>
@showCloseButton={{true}}
>
<section aria-label="confirm-cancel-modal-content" class="modal-card-body">
This will cancel the tidy at the next available checkpoint, which may process additional certificates between when the This will cancel the tidy at the next available checkpoint, which may process additional certificates between when the
operation was marked as cancelled and when the operation stopped. operation was marked as cancelled and when the operation stopped.
<p class="has-top-margin-s">Click “Confirm” to cancel the running tidy operation.</p> <p class="has-top-margin-s">Click “Confirm” to cancel the running tidy operation.</p>
</section> </M.Body>
<footer aria-label="confirm-cancel-buttons" class="modal-card-foot modal-card-foot-outlined"> <M.Footer as |F|>
<button <Hds::ButtonSet>
type="button" <Hds::Button @text="Confirm" {{on "click" (perform this.cancelTidy)}} data-test-tidy-modal-cancel-button />
class="button is-primary" <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} />
{{on "click" (perform this.cancelTidy)}} </Hds::ButtonSet>
data-test-tidy-modal-cancel-button </M.Footer>
> </Hds::Modal>
Confirm
</button>
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.confirmCancelTidy) false)}}>
Cancel
</button>
</footer>
</Modal>
{{/if}} {{/if}}

View File

@@ -26,11 +26,11 @@ const DEFAULTS = {
export default Controller.extend(copy(DEFAULTS, true), { export default Controller.extend(copy(DEFAULTS, true), {
isModalActive: false, isModalActive: false,
isTokenCopied: false,
expirationDate: null, expirationDate: null,
store: service(), store: service(),
rm: service('replication-mode'), rm: service('replication-mode'),
replicationMode: alias('rm.mode'), replicationMode: alias('rm.mode'),
flashMessages: service(),
submitError(e) { submitError(e) {
if (e.errors) { if (e.errors) {
@@ -121,25 +121,13 @@ export default Controller.extend(copy(DEFAULTS, true), {
onSubmit(/*action, mode, data, event*/) { onSubmit(/*action, mode, data, event*/) {
return this.submitHandler(...arguments); return this.submitHandler(...arguments);
}, },
copyClose(successMessage) { closeTokenModal() {
// separate action for copy & close button so it does not try and use execCommand to copy token to clipboard
if (!!successMessage && typeof successMessage === 'string') {
this.flashMessages.success(successMessage);
}
this.toggleProperty('isModalActive'); this.toggleProperty('isModalActive');
this.transitionToRoute('mode.secondaries'); this.transitionToRoute('mode.secondaries');
this.set('isTokenCopied', false);
}, },
toggleModal(successMessage) { onCopy() {
if (!!successMessage && typeof successMessage === 'string') { this.set('isTokenCopied', true);
this.flashMessages.success(successMessage);
}
// use copy browser extension to copy token if you close the modal by clicking outside of it.
const htmlSelectedToken = document.querySelector('#token-textarea');
htmlSelectedToken.select();
document.execCommand('copy');
this.toggleProperty('isModalActive');
this.transitionToRoute('mode.secondaries');
}, },
clear() { clear() {
this.reset(); this.reset();

View File

@@ -60,8 +60,16 @@
</form> </form>
{{#if this.isModalActive}} {{#if this.isModalActive}}
<Modal @title="Copy your token" @onClose={{action "toggleModal" "Token copied!"}} @isActive={{this.isModalActive}}> <Hds::Modal
<section class="modal-card-body"> id="replication-copy-token-modal"
@onClose={{action "closeTokenModal"}}
@isDismissDisabled={{not this.isTokenCopied}}
as |M|
>
<M.Header>
Copy your token
</M.Header>
<M.Body>
<p> <p>
This token can be used to enable This token can be used to enable
{{this.model.replicationModeForDisplay}} {{this.model.replicationModeForDisplay}}
@@ -79,14 +87,28 @@
<InfoTableRow @label="Expires" @value={{date-format this.expirationDate "MMM dd, yyyy hh:mm:ss a"}} /> <InfoTableRow @label="Expires" @value={{date-format this.expirationDate "MMM dd, yyyy hh:mm:ss a"}} />
</div> </div>
</div> </div>
</section> </M.Body>
<footer class="modal-card-foot"> <M.Footer>
<Hds::Copy::Button <Hds::ButtonSet>
@text="Copy & Close" <Hds::Copy::Button
@textToCopy={{this.token}} data-test-modal-copy
class="primary" @text="Copy token"
{{on "click" (action "copyClose" "Token copied!")}} @textToCopy={{this.token}}
/> class="primary"
</footer> @container=".hds-modal"
</Modal> {{on "click" (action "onCopy")}}
/>
<Hds::Button
data-test-modal-close
disabled={{not this.isTokenCopied}}
@text="Close"
@color="secondary"
{{on "click" (action "closeTokenModal")}}
/>
{{#unless this.isTokenCopied}}
<AlertInline @type="warning" @message="Copy token to dismiss modal" />
{{/unless}}
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}} {{/if}}

View File

@@ -65,7 +65,6 @@
"@ember/test-waiters": "^3.0.0", "@ember/test-waiters": "^3.0.0",
"@glimmer/component": "^1.1.2", "@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"@hashicorp/ember-flight-icons": "^3.0.9",
"@hashicorp/structure-icons": "^1.3.0", "@hashicorp/structure-icons": "^1.3.0",
"@icholy/duration": "^5.1.0", "@icholy/duration": "^5.1.0",
"@tsconfig/ember": "^1.0.1", "@tsconfig/ember": "^1.0.1",
@@ -166,7 +165,6 @@
"ember-test-selectors": "6.0.0", "ember-test-selectors": "6.0.0",
"ember-tether": "^2.0.1", "ember-tether": "^2.0.1",
"ember-truth-helpers": "3.0.0", "ember-truth-helpers": "3.0.0",
"ember-wormhole": "0.6.0",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^2.0.0",
"eslint": "^8.37.0", "eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
@@ -248,7 +246,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hashicorp/design-system-components": "^2.9.0", "@hashicorp/design-system-components": "^2.12.2",
"@hashicorp/ember-flight-icons": "^3.1.3",
"handlebars": "4.7.7", "handlebars": "4.7.7",
"highlight.js": "^10.4.1", "highlight.js": "^10.4.1",
"node-notifier": "^8.0.1", "node-notifier": "^8.0.1",

View File

@@ -390,9 +390,9 @@ module('Acceptance | client counts dashboard tab', function (hooks) {
.dom(SELECTORS.emptyStateTitle) .dom(SELECTORS.emptyStateTitle)
.includesText('start date found', 'Empty state shows no billing start date'); .includesText('start date found', 'Empty state shows no billing start date');
await click(SELECTORS.monthDropdown); await click(SELECTORS.monthDropdown);
await click(this.element.querySelector('[data-test-month-list] button:not([disabled])')); await click(this.element.querySelector('[data-test-dropdown-month]:not([disabled])'));
await click(SELECTORS.yearDropdown); await click(SELECTORS.yearDropdown);
await click(this.element.querySelector('[data-test-year-list] button:not([disabled])')); await click(this.element.querySelector('[data-test-dropdown-year]:not([disabled])'));
await click(SELECTORS.dateDropdownSubmit); await click(SELECTORS.dateDropdownSubmit);
assert assert
.dom(SELECTORS.emptyStateTitle) .dom(SELECTORS.emptyStateTitle)

View File

@@ -250,11 +250,16 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await pollCluster(this.owner); await pollCluster(this.owner);
await settled(); await settled();
const modalDefaultTtl = document.querySelector('[data-test-row-value="TTL"]').innerText; const modalDefaultTtl = document.querySelector('[data-test-row-value="TTL"]').innerText;
// checks on secondary token modal // checks on secondary token modal
assert.dom('#modal-wormhole').exists(); assert.dom('.hds-modal#replication-copy-token-modal').exists();
assert.dom('[data-test-inline-error-message]').hasText('Copy token to dismiss modal');
assert.strictEqual(modalDefaultTtl, '1800s', 'shows the correct TTL of 1800s'); assert.strictEqual(modalDefaultTtl, '1800s', 'shows the correct TTL of 1800s');
// click off the modal to make sure you don't just have to click on the copy-close button to copy the token // click off the modal to make sure you don't just have to click on the copy-close button to copy the token
await click('[data-test-modal-background="Copy your token"]'); assert.dom('[data-test-modal-close]').isDisabled('cancel is disabled');
await click('[data-test-modal-copy]');
assert.dom('[data-test-modal-close]').isEnabled('cancel is enabled after token is copied');
await click('[data-test-modal-close]');
// add another secondary not using the default ttl // add another secondary not using the default ttl
await click('[data-test-secondary-add]'); await click('[data-test-secondary-add]');
@@ -269,7 +274,8 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await settled(); await settled();
const modalTtl = document.querySelector('[data-test-row-value="TTL"]').innerText; const modalTtl = document.querySelector('[data-test-row-value="TTL"]').innerText;
assert.strictEqual(modalTtl, '180s', 'shows the correct TTL of 180s'); assert.strictEqual(modalTtl, '180s', 'shows the correct TTL of 180s');
await click('[data-test-modal-background="Copy your token"]'); await click('[data-test-modal-copy]');
await click('[data-test-modal-close]');
// confirm you were redirected to the secondaries page // confirm you were redirected to the secondaries page
assert.strictEqual( assert.strictEqual(

View File

@@ -432,7 +432,7 @@ module('Acceptance | pki workflow', function (hooks) {
.dom(SELECTORS.issuerDetails.configure) .dom(SELECTORS.issuerDetails.configure)
.hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/edit`); .hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/edit`);
await click(SELECTORS.issuerDetails.rotateRoot); await click(SELECTORS.issuerDetails.rotateRoot);
assert.dom(find(SELECTORS.issuerDetails.rotateModal).parentElement).hasClass('is-active'); assert.dom(SELECTORS.issuerDetails.rotateModal).exists('rotate root modal opens');
await click(SELECTORS.issuerDetails.rotateModalGenerate); await click(SELECTORS.issuerDetails.rotateModalGenerate);
assert.strictEqual( assert.strictEqual(
currentURL(), currentURL(),

View File

@@ -163,7 +163,6 @@ module('Acceptance | pki tidy', function (hooks) {
.exists('Configure tidy modal options button exists'); .exists('Configure tidy modal options button exists');
await click(SELECTORS.tidyConfigureModal.tidyOptionsModal); await click(SELECTORS.tidyConfigureModal.tidyOptionsModal);
assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists'); assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
await click(SELECTORS.tidyConfigureModal.tidyOptionsModal);
await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton); await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton);
await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled')); await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled'));
await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore')); await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore'));

View File

@@ -281,7 +281,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
await connectionPage.save(); await connectionPage.save();
await settled(); await settled();
assert assert
.dom('.modal.is-active .title') .dom('[data-test-db-connection-modal-title]')
.hasText('Rotate your root credentials?', 'Modal appears asking to rotate root credentials'); .hasText('Rotate your root credentials?', 'Modal appears asking to rotate root credentials');
assert.dom('[data-test-enable-connection]').exists('Enable button exists'); assert.dom('[data-test-enable-connection]').exists('Enable button exists');
await click('[data-test-enable-connection]'); await click('[data-test-enable-connection]');
@@ -397,7 +397,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
await connectionPage.save(); await connectionPage.save();
await settled(); await settled();
assert assert
.dom('.modal.is-active .title') .dom('[data-test-db-connection-modal-title]')
.hasText('Rotate your root credentials?', 'Modal appears asking to '); .hasText('Rotate your root credentials?', 'Modal appears asking to ');
await connectionPage.enable(); await connectionPage.enable();
assert.strictEqual( assert.strictEqual(
@@ -418,7 +418,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
}); });
await connectionPage.delete(); await connectionPage.delete();
assert assert
.dom('.modal.is-active .title') .dom('[data-test-confirmation-modal-title]')
.hasText('Delete connection?', 'Modal appears asking to confirm delete action'); .hasText('Delete connection?', 'Modal appears asking to confirm delete action');
await fillIn('[data-test-confirmation-modal-input="Delete connection?"]', connectionDetails.id); await fillIn('[data-test-confirmation-modal-input="Delete connection?"]', connectionDetails.id);
await click('[data-test-confirm-button]'); await click('[data-test-confirm-button]');

View File

@@ -37,14 +37,14 @@ const testConvergentEncryption = async function (assert, keyName) {
encodePlaintext: false, encodePlaintext: false,
encodeContext: false, encodeContext: false,
assertAfterEncrypt: (key) => { assertAfterEncrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); assert.dom('[data-test-encrypt-modal]').exists(`${key}: Modal opens after encrypt`);
assert.ok( assert.ok(
/vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText),
`${key}: ciphertext shows a vault-prefixed ciphertext` `${key}: ciphertext shows a vault-prefixed ciphertext`
); );
}, },
assertBeforeDecrypt: (key) => { assertBeforeDecrypt: (key) => {
assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); assert.dom('[data-test-decrypt-modal]').doesNotExist(`${key}: Modal not open before decrypt`);
assert assert
.dom('[data-test-transit-input="context"]') .dom('[data-test-transit-input="context"]')
.hasValue( .hasValue(
@@ -52,9 +52,8 @@ const testConvergentEncryption = async function (assert, keyName) {
`${key}: the ui shows the base64-encoded context` `${key}: the ui shows the base64-encoded context`
); );
}, },
assertAfterDecrypt: (key) => { assertAfterDecrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); assert.dom('[data-test-decrypt-modal]').exists(`${key}: Modal opens after decrypt`);
assert.strictEqual( assert.strictEqual(
find('[data-test-encrypted-value="plaintext"]').innerText, find('[data-test-encrypted-value="plaintext"]').innerText,
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
@@ -69,20 +68,20 @@ const testConvergentEncryption = async function (assert, keyName) {
encodePlaintext: false, encodePlaintext: false,
encodeContext: false, encodeContext: false,
assertAfterEncrypt: (key) => { assertAfterEncrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); assert.dom('[data-test-encrypt-modal]').exists(`${key}: Modal opens after encrypt`);
assert.ok( assert.ok(
/vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText),
`${key}: ciphertext shows a vault-prefixed ciphertext` `${key}: ciphertext shows a vault-prefixed ciphertext`
); );
}, },
assertBeforeDecrypt: (key) => { assertBeforeDecrypt: (key) => {
assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); assert.dom('[data-test-decrypt-modal]').doesNotExist(`${key}: Modal not open before decrypt`);
assert assert
.dom('[data-test-transit-input="context"]') .dom('[data-test-transit-input="context"]')
.hasValue(encodeString('context'), `${key}: the ui shows the input context`); .hasValue(encodeString('context'), `${key}: the ui shows the input context`);
}, },
assertAfterDecrypt: (key) => { assertAfterDecrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); assert.dom('[data-test-decrypt-modal]').exists(`${key}: Modal opens after decrypt`);
assert.strictEqual( assert.strictEqual(
find('[data-test-encrypted-value="plaintext"]').innerText, find('[data-test-encrypted-value="plaintext"]').innerText,
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
@@ -97,20 +96,20 @@ const testConvergentEncryption = async function (assert, keyName) {
encodePlaintext: false, encodePlaintext: false,
encodeContext: false, encodeContext: false,
assertAfterEncrypt: (key) => { assertAfterEncrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); assert.dom('[data-test-encrypt-modal]').exists(`${key}: Modal opens after encrypt`);
assert.ok( assert.ok(
/vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText),
`${key}: ciphertext shows a vault-prefixed ciphertext` `${key}: ciphertext shows a vault-prefixed ciphertext`
); );
}, },
assertBeforeDecrypt: (key) => { assertBeforeDecrypt: (key) => {
assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); assert.dom('[data-test-decrypt-modal]').doesNotExist(`${key}: Modal not open before decrypt`);
assert assert
.dom('[data-test-transit-input="context"]') .dom('[data-test-transit-input="context"]')
.hasValue(encodeString('context'), `${key}: the ui shows the input context`); .hasValue(encodeString('context'), `${key}: the ui shows the input context`);
}, },
assertAfterDecrypt: (key) => { assertAfterDecrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); assert.dom('[data-test-decrypt-modal]').exists(`${key}: Modal opens after decrypt`);
assert.strictEqual( assert.strictEqual(
find('[data-test-encrypted-value="plaintext"]').innerText, find('[data-test-encrypted-value="plaintext"]').innerText,
encodeString('This is the secret'), encodeString('This is the secret'),
@@ -125,20 +124,20 @@ const testConvergentEncryption = async function (assert, keyName) {
encodePlaintext: true, encodePlaintext: true,
encodeContext: true, encodeContext: true,
assertAfterEncrypt: (key) => { assertAfterEncrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after encrypt`); assert.dom('[data-test-encrypt-modal]').exists(`${key}: Modal opens after encrypt`);
assert.ok( assert.ok(
/vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText), /vault:/.test(find('[data-test-encrypted-value="ciphertext"]').innerText),
`${key}: ciphertext shows a vault-prefixed ciphertext` `${key}: ciphertext shows a vault-prefixed ciphertext`
); );
}, },
assertBeforeDecrypt: (key) => { assertBeforeDecrypt: (key) => {
assert.dom('.modal.is-active').doesNotExist(`${key}: Modal not open before decrypt`); assert.dom('[data-test-decrypt-modal]').doesNotExist(`${key}: Modal not open before decrypt`);
assert assert
.dom('[data-test-transit-input="context"]') .dom('[data-test-transit-input="context"]')
.hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); .hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`);
}, },
assertAfterDecrypt: (key) => { assertAfterDecrypt: (key) => {
assert.dom('.modal.is-active').exists(`${key}: Modal opens after decrypt`); assert.dom('[data-test-decrypt-modal]').exists(`${key}: Modal opens after decrypt`);
assert.strictEqual( assert.strictEqual(
find('[data-test-encrypted-value="plaintext"]').innerText, find('[data-test-encrypted-value="plaintext"]').innerText,
encodeString('There are many secrets 🤐'), encodeString('There are many secrets 🤐'),
@@ -161,7 +160,7 @@ const testConvergentEncryption = async function (assert, keyName) {
if (testCase.encodeContext) { if (testCase.encodeContext) {
await click('[data-test-transit-b64-toggle="context"]'); await click('[data-test-transit-b64-toggle="context"]');
} }
assert.dom('.modal.is-active').doesNotExist(`${name}: is not open before encrypt`); assert.dom('[data-test-encrypt-modal]').doesNotExist(`${name}: is not open before encrypt`);
await click('[data-test-button-encrypt]'); await click('[data-test-button-encrypt]');
if (testCase.assertAfterEncrypt) { if (testCase.assertAfterEncrypt) {
@@ -170,9 +169,9 @@ const testConvergentEncryption = async function (assert, keyName) {
} }
// store ciphertext for decryption step // store ciphertext for decryption step
const copiedCiphertext = find('[data-test-encrypted-value="ciphertext"]').innerText; const copiedCiphertext = find('[data-test-encrypted-value="ciphertext"]').innerText;
await click('.modal.is-active [data-test-modal-background]'); await click('dialog button');
assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`); assert.dom('dialog.hds-modal').doesNotExist(`${name}: Modal closes after background clicked`);
await click('[data-test-transit-action-link="decrypt"]'); await click('[data-test-transit-action-link="decrypt"]');
if (testCase.assertBeforeDecrypt) { if (testCase.assertBeforeDecrypt) {
@@ -187,9 +186,9 @@ const testConvergentEncryption = async function (assert, keyName) {
testCase.assertAfterDecrypt(keyName); testCase.assertAfterDecrypt(keyName);
} }
await click('.modal.is-active [data-test-modal-background]'); await click('dialog button');
assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`); assert.dom('dialog.hds-modal').doesNotExist(`${name}: Modal closes after background clicked`);
} }
}; };

View File

@@ -29,8 +29,8 @@ export const SELECTORS = {
attributionBlock: '[data-test-clients-attribution]', attributionBlock: '[data-test-clients-attribution]',
filterBar: '[data-test-clients-filter-bar]', filterBar: '[data-test-clients-filter-bar]',
rangeDropdown: '[data-test-calendar-widget-trigger]', rangeDropdown: '[data-test-calendar-widget-trigger]',
monthDropdown: '[data-test-popup-menu-trigger="month"]', monthDropdown: '[data-test-toggle-month]',
yearDropdown: '[data-test-popup-menu-trigger="year"]', yearDropdown: '[data-test-toggle-year]',
dateDropdownSubmit: '[data-test-date-dropdown-submit]', dateDropdownSubmit: '[data-test-date-dropdown-submit]',
runningTotalMonthStats: '[data-test-running-total="single-month-stats"]', runningTotalMonthStats: '[data-test-running-total="single-month-stats"]',
runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]', runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]',

View File

@@ -22,7 +22,7 @@ export const PAGE = {
message: '[data-test-page-error] p', message: '[data-test-page-error] p',
}, },
toolbar: 'nav.toolbar', toolbar: 'nav.toolbar',
toolbarAction: 'nav.toolbar-actions .toolbar-link', toolbarAction: 'nav.toolbar-actions .toolbar-link, nav.toolbar-actions .toolbar-button',
secretRow: '[data-test-component="info-table-row"]', // replace with infoRow secretRow: '[data-test-component="info-table-row"]', // replace with infoRow
// specific page selectors // specific page selectors
backends: { backends: {

View File

@@ -12,12 +12,12 @@ export const SELECTORS = {
hdsAlertButtonText: '[data-test-cancel-tidy-action] .hds-button__text', hdsAlertButtonText: '[data-test-cancel-tidy-action] .hds-button__text',
timeStartedRow: '[data-test-value-div="Time started"]', timeStartedRow: '[data-test-value-div="Time started"]',
timeFinishedRow: '[data-test-value-div="Time finished"]', timeFinishedRow: '[data-test-value-div="Time finished"]',
cancelTidyModalBackground: '[data-test-modal-background="Cancel tidy?"]', cancelTidyModalBackground: '.hds-modal#pki-cancel-tidy-modal',
tidyEmptyStateConfigure: '[data-test-tidy-empty-state-configure]', tidyEmptyStateConfigure: '[data-test-tidy-empty-state-configure]',
manualTidyToolbar: '[data-test-pki-manual-tidy-config]', manualTidyToolbar: '[data-test-pki-manual-tidy-config]',
autoTidyToolbar: '[data-test-pki-auto-tidy-config]', autoTidyToolbar: '[data-test-pki-auto-tidy-config]',
tidyConfigureModal: { tidyConfigureModal: {
configureTidyModal: '[data-test-modal-background="Tidy this mount"]', configureTidyModal: '.hds-modal#pki-tidy-modal',
tidyModalAutoButton: '[data-test-tidy-modal-auto-button]', tidyModalAutoButton: '[data-test-tidy-modal-auto-button]',
tidyModalManualButton: '[data-test-tidy-modal-manual-button]', tidyModalManualButton: '[data-test-tidy-modal-manual-button]',
tidyModalCancelButton: '[data-test-tidy-modal-cancel-button]', tidyModalCancelButton: '[data-test-tidy-modal-cancel-button]',

View File

@@ -5,7 +5,7 @@
export const SELECTORS = { export const SELECTORS = {
issuerLink: '[data-test-delete-all-issuers-link]', issuerLink: '[data-test-delete-all-issuers-link]',
deleteAllIssuerModal: '[data-test-modal-background="Delete All Issuers?"]', deleteAllIssuerModal: '.hds-modal#confirmation-modal',
deleteAllIssuerInput: '[data-test-confirmation-modal-input="Delete All Issuers?"]', deleteAllIssuerInput: '[data-test-confirmation-modal-input="Delete all issuers?"]',
deleteAllIssuerButton: '[data-test-confirm-button="Delete All Issuers?"]', deleteAllIssuerButton: '[data-test-confirm-button="Delete all issuers?"]',
}; };

View File

@@ -11,7 +11,7 @@ export const SELECTORS = {
download: '[data-test-issuer-download]', download: '[data-test-issuer-download]',
groupTitle: '[data-test-group-title]', groupTitle: '[data-test-group-title]',
parsingAlertBanner: '[data-test-parsing-error-alert-banner]', parsingAlertBanner: '[data-test-parsing-error-alert-banner]',
rotateModal: '[data-test-modal-background="Rotate this root"]', rotateModal: '.hds-modal#pki-rotate-root-modal',
rotateModalGenerate: '[data-test-root-rotate-step-one]', rotateModalGenerate: '[data-test-root-rotate-step-one]',
rotateRoot: '[data-test-pki-issuer-rotate-root]', rotateRoot: '[data-test-pki-issuer-rotate-root]',
row: '[data-test-component="info-table-row"]', row: '[data-test-component="info-table-row"]',

View File

@@ -26,7 +26,6 @@
<div id="qunit-fixture"> <div id="qunit-fixture">
<div id="ember-testing-container"> <div id="ember-testing-container">
<div id="ember-testing"></div> <div id="ember-testing"></div>
<div id="modal-wormhole"></div>
</div> </div>
</div> </div>

View File

@@ -46,7 +46,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders empty state with no data', async function (assert) { test('it renders empty state with no data', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution @chartLegend={{this.chartLegend}} /> <Clients::Attribution @chartLegend={{this.chartLegend}} />
`); `);
@@ -59,7 +58,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders with data for namespaces', async function (assert) { test('it renders with data for namespaces', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.totalClientAttribution}} @totalClientAttribution={{this.totalClientAttribution}}
@@ -93,7 +91,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
this.start = formatRFC3339(subMonths(this.mockNow, 1)); this.start = formatRFC3339(subMonths(this.mockNow, 1));
this.end = formatRFC3339(subMonths(endOfMonth(this.mockNow), 1)); this.end = formatRFC3339(subMonths(endOfMonth(this.mockNow), 1));
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.totalClientAttribution}} @totalClientAttribution={{this.totalClientAttribution}}
@@ -147,7 +144,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders single chart for current month', async function (assert) { test('it renders single chart for current month', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.totalClientAttribution}} @totalClientAttribution={{this.totalClientAttribution}}
@@ -169,7 +165,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders single chart and correct text for for date range', async function (assert) { test('it renders single chart and correct text for for date range', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.totalClientAttribution}} @totalClientAttribution={{this.totalClientAttribution}}
@@ -193,7 +188,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders with data for selected namespace auth methods for a date range', async function (assert) { test('it renders with data for selected namespace auth methods for a date range', async function (assert) {
this.set('selectedNamespace', 'second'); this.set('selectedNamespace', 'second');
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.namespaceMountsData}} @totalClientAttribution={{this.namespaceMountsData}}
@@ -225,7 +219,6 @@ module('Integration | Component | clients/attribution', function (hooks) {
test('it renders modal', async function (assert) { test('it renders modal', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution <Clients::Attribution
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@totalClientAttribution={{this.namespaceMountsData}} @totalClientAttribution={{this.namespaceMountsData}}
@@ -235,7 +228,9 @@ module('Integration | Component | clients/attribution', function (hooks) {
/> />
`); `);
await click('[data-test-attribution-export-button]'); await click('[data-test-attribution-export-button]');
assert.dom('.modal.is-active .title').hasText('Export attribution data', 'modal appears to export csv'); assert
assert.dom('.modal.is-active').includesText('June 2022 - December 2022'); .dom('[data-test-export-modal-title]')
.hasText('Export attribution data', 'modal appears to export csv');
assert.dom('[ data-test-export-date-range]').includesText('June 2022 - December 2022');
}); });
}); });

View File

@@ -52,7 +52,7 @@ module('Integration | Component | client count config', function (hooks) {
}); });
test('it should function in edit mode when reporting is disabled', async function (assert) { test('it should function in edit mode when reporting is disabled', async function (assert) {
assert.expect(13); assert.expect(12);
this.server.put('/sys/internal/counters/config', (schema, req) => { this.server.put('/sys/internal/counters/config', (schema, req) => {
const { enabled, retention_months } = JSON.parse(req.requestBody); const { enabled, retention_months } = JSON.parse(req.requestBody);
@@ -64,7 +64,6 @@ module('Integration | Component | client count config', function (hooks) {
this.createModel('disable'); this.createModel('disable');
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Config @model={{this.model}} @mode="edit" /> <Clients::Config @model={{this.model}} @mode="edit" />
`); `);
@@ -86,9 +85,8 @@ module('Integration | Component | client count config', function (hooks) {
await fillIn('[data-test-input="retentionMonths"]', 24); await fillIn('[data-test-input="retentionMonths"]', 24);
await click('[data-test-clients-config-save]'); await click('[data-test-clients-config-save]');
assert.dom('.modal.is-active').exists('Modal renders');
assert assert
.dom('[data-test-modal-title] span') .dom('[data-test-clients-config-modal="title"]')
.hasText('Turn usage tracking on?', 'Correct modal title renders'); .hasText('Turn usage tracking on?', 'Correct modal title renders');
assert.dom('[data-test-clients-config-modal="on"]').exists('Correct modal description block renders'); assert.dom('[data-test-clients-config-modal="on"]').exists('Correct modal description block renders');
@@ -100,14 +98,14 @@ module('Integration | Component | client count config', function (hooks) {
await click('[data-test-input="enabled"]'); await click('[data-test-input="enabled"]');
await click('[data-test-clients-config-save]'); await click('[data-test-clients-config-save]');
assert.dom('.modal.is-active').exists('Modal renders'); assert.dom('[data-test-clients-config-modal]').exists('Modal renders');
assert assert
.dom('[data-test-modal-title] span') .dom('[data-test-clients-config-modal="title"]')
.hasText('Turn usage tracking off?', 'Correct modal title renders'); .hasText('Turn usage tracking off?', 'Correct modal title renders');
assert.dom('[data-test-clients-config-modal="off"]').exists('Correct modal description block renders'); assert.dom('[data-test-clients-config-modal="off"]').exists('Correct modal description block renders');
await click('[data-test-clients-config-modal="cancel"]'); await click('[data-test-clients-config-modal="cancel"]');
assert.dom('.modal.is-active').doesNotExist('Modal is hidden on cancel'); assert.dom('[data-test-clients-config-modal]').doesNotExist('Modal is hidden on cancel');
}); });
test('it should function in edit mode when reporting is enabled', async function (assert) { test('it should function in edit mode when reporting is enabled', async function (assert) {
@@ -123,7 +121,6 @@ module('Integration | Component | client count config', function (hooks) {
this.createModel('enable', true, 24); this.createModel('enable', true, 24);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Config @model={{this.model}} @mode="edit" /> <Clients::Config @model={{this.model}} @mode="edit" />
`); `);
@@ -162,7 +159,6 @@ module('Integration | Component | client count config', function (hooks) {
this.createModel(); this.createModel();
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Config @model={{this.model}} @mode="edit" /> <Clients::Config @model={{this.model}} @mode="edit" />
`); `);
await fillIn('[data-test-input="retentionMonths"]', 24); await fillIn('[data-test-input="retentionMonths"]', 24);

View File

@@ -1433,8 +1433,7 @@ module('Integration | Component | clients/monthly-usage', function (hooks) {
test('it renders empty state with no data', async function (assert) { test('it renders empty state with no data', async function (assert) {
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <Clients::MonthlyUsage @chartLegend={{this.chartLegend}} @responseTimestamp={{this.timestamp}}/>
<Clients::MonthlyUsage @chartLegend={{this.chartLegend}} @responseTimestamp={{this.timestamp}}/>
`); `);
assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders'); assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders');
assert.dom('[data-test-component="empty-state"]').exists(); assert.dom('[data-test-component="empty-state"]').exists();
@@ -1455,8 +1454,7 @@ module('Integration | Component | clients/monthly-usage', function (hooks) {
), ),
]); ]);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <Clients::MonthlyUsage
<Clients::MonthlyUsage
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@verticalBarChartData={{this.byMonthActivityData}} @verticalBarChartData={{this.byMonthActivityData}}
@responseTimestamp={{this.timestamp}} @responseTimestamp={{this.timestamp}}

View File

@@ -1444,8 +1444,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
const expectedNewNonEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'non_entity_clients')]); const expectedNewNonEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'non_entity_clients')]);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <Clients::RunningTotal
<Clients::RunningTotal
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@selectedAuthMethod={{this.selectedAuthMethod}} @selectedAuthMethod={{this.selectedAuthMethod}}
@byMonthActivityData={{this.byMonthActivityData}} @byMonthActivityData={{this.byMonthActivityData}}
@@ -1515,8 +1514,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]); const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <Clients::RunningTotal
<Clients::RunningTotal
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@selectedAuthMethod={{this.selectedAuthMethod}} @selectedAuthMethod={{this.selectedAuthMethod}}
@byMonthActivityData={{this.byMonthActivityData}} @byMonthActivityData={{this.byMonthActivityData}}
@@ -1558,8 +1556,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
const expectedNewNonEntity = formatNumber([singleMonthNew.non_entity_clients]); const expectedNewNonEntity = formatNumber([singleMonthNew.non_entity_clients]);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <Clients::RunningTotal
<Clients::RunningTotal
@chartLegend={{this.chartLegend}} @chartLegend={{this.chartLegend}}
@selectedAuthMethod={{this.selectedAuthMethod}} @selectedAuthMethod={{this.selectedAuthMethod}}
@byMonthActivityData={{this.singleMonth}} @byMonthActivityData={{this.singleMonth}}

View File

@@ -18,8 +18,7 @@ module('Integration | Component | confirmation-modal', function (hooks) {
this.set('onConfirm', confirmAction); this.set('onConfirm', confirmAction);
this.set('onClose', closeAction); this.set('onClose', closeAction);
await render(hbs` await render(hbs`
<div id="modal-wormhole"></div> <ConfirmationModal
<ConfirmationModal
@title="Confirmation Modal" @title="Confirmation Modal"
@isActive={{true}} @isActive={{true}}
@onConfirm={{this.onConfirm}} @onConfirm={{this.onConfirm}}
@@ -30,11 +29,11 @@ module('Integration | Component | confirmation-modal', function (hooks) {
`); `);
assert.dom('[data-test-confirm-button]').isDisabled(); assert.dom('[data-test-confirm-button]').isDisabled();
assert.dom('[data-test-modal-div]').hasAttribute('class', 'modal is-highlight is-active'); assert.dom('.hds-modal#confirmation-modal').exists('modal is active');
assert.dom('[data-test-confirm-button]').hasText('Plz Continue', 'Confirm button has specified value'); assert.dom('[data-test-confirm-button]').hasText('Plz Continue', 'Confirm button has specified value');
assert assert
.dom('[data-test-modal-title]') .dom('[data-test-confirmation-modal-title] [data-test-icon="alert-triangle"]')
.hasStyle({ color: 'rgb(160, 125, 2)' }, 'title exists with warning header'); .exists('title has with warning icon');
await fillIn('[data-test-confirmation-modal-input="Confirmation Modal"]', 'Destructive Thing'); await fillIn('[data-test-confirmation-modal-input="Confirmation Modal"]', 'Destructive Thing');
assert.dom('[data-test-confirm-button="Confirmation Modal"]').isNotDisabled(); assert.dom('[data-test-confirm-button="Confirmation Modal"]').isNotDisabled();

View File

@@ -12,14 +12,12 @@ import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import timestamp from 'core/utils/timestamp'; import timestamp from 'core/utils/timestamp';
const SELECTORS = { const SELECTORS = {
monthDropdown: '[data-test-popup-menu-trigger="month"]', monthDropdown: '[data-test-toggle-month]',
specificMonth: (m) => `[data-test-dropdown-month="${m}"]`, specificMonth: (m) => `[data-test-dropdown-month="${m}"]`,
yearDropdown: '[data-test-popup-menu-trigger="year"]', yearDropdown: '[data-test-toggle-year]',
specificYear: (y) => `[data-test-dropdown-year="${y}"]`, specificYear: (y) => `[data-test-dropdown-year="${y}"]`,
submitButton: '[data-test-date-dropdown-submit]', submitButton: '[data-test-date-dropdown-submit]',
cancelButton: '[data-test-date-dropdown-cancel]', monthOptions: '[data-test-dropdown-month]',
monthOptions: '[data-test-month-list] button',
}; };
module('Integration | Component | date-dropdown', function (hooks) { module('Integration | Component | date-dropdown', function (hooks) {
@@ -34,27 +32,11 @@ module('Integration | Component | date-dropdown', function (hooks) {
test('it renders dropdown', async function (assert) { test('it renders dropdown', async function (assert) {
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown/> <DateDropdown/>
</div> </div>
`); `);
assert.dom(SELECTORS.submitButton).hasText('Submit', 'button renders default text'); assert.dom(SELECTORS.submitButton).hasText('Submit', 'button renders default text');
assert.dom(SELECTORS.cancelButton).doesNotExist('it does not render cancel button by default');
});
test('it fires off cancel callback', async function (assert) {
assert.expect(2);
const onCancel = () => {
assert.ok('fires onCancel callback');
};
this.set('onCancel', onCancel);
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown @handleCancel={{this.onCancel}} @submitText="Save"/>
</div>
`);
assert.dom(SELECTORS.submitButton).hasText('Save', 'button renders passed in text');
await click(SELECTORS.cancelButton);
}); });
test('it renders dropdown and selects month and year', async function (assert) { test('it renders dropdown and selects month and year', async function (assert) {
@@ -74,7 +56,7 @@ module('Integration | Component | date-dropdown', function (hooks) {
this.set('parentAction', parentAction); this.set('parentAction', parentAction);
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown <DateDropdown
@handleSubmit={{this.parentAction}} @handleSubmit={{this.parentAction}}
@dateType="start" @dateType="start"
@@ -112,7 +94,7 @@ module('Integration | Component | date-dropdown', function (hooks) {
test('selecting month first: current year enabled when current month selected', async function (assert) { test('selecting month first: current year enabled when current month selected', async function (assert) {
assert.expect(5); assert.expect(5);
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown/> <DateDropdown/>
</div> </div>
`); `);
@@ -129,7 +111,7 @@ module('Integration | Component | date-dropdown', function (hooks) {
test('selecting month first: it disables current year when future months selected', async function (assert) { test('selecting month first: it disables current year when future months selected', async function (assert) {
assert.expect(5); assert.expect(5);
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown/> <DateDropdown/>
</div> </div>
`); `);
@@ -149,7 +131,7 @@ module('Integration | Component | date-dropdown', function (hooks) {
test('selecting year first: it disables future months when current year selected', async function (assert) { test('selecting year first: it disables future months when current year selected', async function (assert) {
assert.expect(12); assert.expect(12);
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown/> <DateDropdown/>
</div> </div>
`); `);
@@ -170,7 +152,7 @@ module('Integration | Component | date-dropdown', function (hooks) {
test('selecting year first: it enables all months when past year is selected', async function (assert) { test('selecting year first: it enables all months when past year is selected', async function (assert) {
assert.expect(12); assert.expect(12);
await render(hbs` await render(hbs`
<div class="is-flex-align-baseline"> <div class="has-padding-l">
<DateDropdown/> <DateDropdown/>
</div> </div>
`); `);

View File

@@ -6,7 +6,6 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers'; import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers';
import { isPresent } from 'ember-cli-page-object';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon'; import sinon from 'sinon';
@@ -28,25 +27,23 @@ module('Integration | Component | download button', function (hooks) {
test('it renders', async function (assert) { test('it renders', async function (assert) {
await render(hbs` await render(hbs`
<DownloadButton class="button"> <DownloadButton /> `);
<Icon @name="download" /> assert.dom(SELECTORS.icon).exists('renders download icon');
Download assert.dom(SELECTORS.button).hasText('Download', 'renders default text');
</DownloadButton> });
`);
assert.dom(SELECTORS.button).hasClass('button'); test('it renders passed args', async function (assert) {
assert.ok(isPresent(SELECTORS.icon), 'renders yielded icon'); await render(hbs`
assert.dom(SELECTORS.button).hasTextContaining('Download', 'renders yielded text'); <DownloadButton @text="I do something" @hideIcon={{true}} /> `);
assert.dom(SELECTORS.icon).doesNotExist('hides icon');
assert.dom(SELECTORS.button).hasText('I do something', 'renders passed text');
}); });
test('it downloads with defaults when only passed @data arg', async function (assert) { test('it downloads with defaults when only passed @data arg', async function (assert) {
assert.expect(3); assert.expect(3);
await render(hbs` await render(hbs`
<DownloadButton class="button" <DownloadButton @data={{this.data}} />
@data={{this.data}}
>
Download
</DownloadButton>
`); `);
await click(SELECTORS.button); await click(SELECTORS.button);
const [filename, content, extension] = this.downloadSpy.getCall(0).args; const [filename, content, extension] = this.downloadSpy.getCall(0).args;
@@ -59,16 +56,13 @@ module('Integration | Component | download button', function (hooks) {
assert.expect(3); assert.expect(3);
await render(hbs` await render(hbs`
<DownloadButton class="button" <DownloadButton
@data={{this.data}} @data={{this.data}}
@filename={{this.filename}} @filename={{this.filename}}
@mime={{this.mime}} @mime={{this.mime}}
@extension={{this.extension}} @extension={{this.extension}}
> />
Download
</DownloadButton>
`); `);
await click(SELECTORS.button); await click(SELECTORS.button);
const [filename, content, extension] = this.downloadSpy.getCall(0).args; const [filename, content, extension] = this.downloadSpy.getCall(0).args;
assert.ok(filename.includes(`${this.filename}-`), 'filename added to ISO string'); assert.ok(filename.includes(`${this.filename}-`), 'filename added to ISO string');
@@ -80,9 +74,7 @@ module('Integration | Component | download button', function (hooks) {
assert.expect(3); assert.expect(3);
this.fetchData = () => 'this is fetched data from a parent function'; this.fetchData = () => 'this is fetched data from a parent function';
await render(hbs` await render(hbs`
<DownloadButton class="button" @fetchData={{this.fetchData}} > <DownloadButton @fetchData={{this.fetchData}} />
Download
</DownloadButton>
`); `);
await click(SELECTORS.button); await click(SELECTORS.button);
@@ -103,7 +95,7 @@ module('Integration | Component | download button', function (hooks) {
}); });
this.fetchData = () => 'this is fetched data from a parent function'; this.fetchData = () => 'this is fetched data from a parent function';
await render(hbs` await render(hbs`
<DownloadButton class="button" @data={{this.data}} @fetchData={{this.fetchData}} /> <DownloadButton @data={{this.data}} @fetchData={{this.fetchData}} />
`); `);
resetOnerror(); resetOnerror();
}); });

View File

@@ -45,9 +45,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
// TODO: Add capabilities tests // TODO: Add capabilities tests
test('it renders show view as default', async function (assert) { test('it renders show view as default', async function (assert) {
assert.expect(8); assert.expect(8);
await render( await render(hbs`<Keymgmt::KeyEdit @model={{this.model}} @tab={{this.tab}} />`);
hbs`<Keymgmt::KeyEdit @model={{this.model}} @tab={{this.tab}} /><div id="modal-wormhole" />`
);
assert.dom('[data-test-secret-header]').hasText('Unicorns', 'Shows key name'); assert.dom('[data-test-secret-header]').hasText('Unicorns', 'Shows key name');
assert.dom('[data-test-keymgmt-key-toolbar]').exists('Subnav toolbar exists'); assert.dom('[data-test-keymgmt-key-toolbar]').exists('Subnav toolbar exists');
assert.dom('[data-test-tab="Details"]').exists('Details tab exists'); assert.dom('[data-test-tab="Details"]').exists('Details tab exists');
@@ -71,9 +69,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
this.set('mode', 'edit'); this.set('mode', 'edit');
this.set('model', model); this.set('model', model);
await render( await render(hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} />`);
hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} /><div id="modal-wormhole" />`
);
assert.dom('[data-test-secret-header]').hasText('Edit Key', 'Shows edit header'); assert.dom('[data-test-secret-header]').hasText('Edit Key', 'Shows edit header');
assert.dom('[data-test-keymgmt-key-toolbar]').doesNotExist('Subnav toolbar does not exist'); assert.dom('[data-test-keymgmt-key-toolbar]').doesNotExist('Subnav toolbar does not exist');
assert.dom('[data-test-tab="Details"]').doesNotExist('Details tab does not exist'); assert.dom('[data-test-tab="Details"]').doesNotExist('Details tab does not exist');
@@ -86,9 +82,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
this.set('mode', 'create'); this.set('mode', 'create');
this.set('model', model); this.set('model', model);
await render( await render(hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} />`);
hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} /><div id="modal-wormhole" />`
);
assert.dom('[data-test-secret-header]').hasText('Create Key', 'Shows edit header'); assert.dom('[data-test-secret-header]').hasText('Create Key', 'Shows edit header');
assert.dom('[data-test-keymgmt-key-toolbar]').doesNotExist('Subnav toolbar does not exist'); assert.dom('[data-test-keymgmt-key-toolbar]').doesNotExist('Subnav toolbar does not exist');
assert.dom('[data-test-tab="Details"]').doesNotExist('Details tab does not exist'); assert.dom('[data-test-tab="Details"]').doesNotExist('Details tab does not exist');
@@ -100,9 +94,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
const store = this.owner.lookup('service:store'); const store = this.owner.lookup('service:store');
this.model = store.createRecord('keymgmt/key'); this.model = store.createRecord('keymgmt/key');
this.set('mode', 'create'); this.set('mode', 'create');
await render( await render(hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} />`);
hbs`<Keymgmt::KeyEdit @model={{this.model}} @mode={{this.mode}} /><div id="modal-wormhole" />`
);
assert.dom('[data-test-input="type"]').hasValue('rsa-2048', 'Has type rsa-2048 by default'); assert.dom('[data-test-input="type"]').hasValue('rsa-2048', 'Has type rsa-2048 by default');
}); });
}); });

View File

@@ -199,14 +199,13 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
await render( await render(
hbs` hbs`
<div id="modal-wormhole"></div> <Page::Configure @model={{this.editModel}} @breadcrumbs={{this.breadcrumbs}} />
<Page::Configure @model={{this.editModel}} @breadcrumbs={{this.breadcrumbs}} />
`, `,
{ owner: this.engine } { owner: this.engine }
); );
await click('[data-test-config-save]'); await click('[data-test-config-save]');
assert assert
.dom('.modal-card-body') .dom('[data-test-edit-config-body]')
.hasText( .hasText(
'Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are you sure?', 'Making changes to your configuration may affect how Vault will reach the Kubernetes API and authenticate with it. Are you sure?',
'Confirm modal renders' 'Confirm modal renders'

View File

@@ -44,8 +44,7 @@ module('Integration | Component | ldap | AccountsCheckedOut', function (hooks) {
this.renderComponent = () => { this.renderComponent = () => {
return render( return render(
hbs` hbs`
<div id="modal-wormhole"></div> <AccountsCheckedOut
<AccountsCheckedOut
@libraries={{array this.library}} @libraries={{array this.library}}
@statuses={{this.statuses}} @statuses={{this.statuses}}
@showLibraryColumn={{this.showLibraryColumn}} @showLibraryColumn={{this.showLibraryColumn}}

Some files were not shown because too many files have changed in this diff Show More