mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
* add general wizard component * add copyright headers * remove header, improve submit block conditional logic, add integration tests Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
parent
7bf7bf39fe
commit
c5b3edc0e4
30
ui/app/components/wizard/index.hbs
Normal file
30
ui/app/components/wizard/index.hbs
Normal file
@ -0,0 +1,30 @@
|
||||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if @showWelcome}}
|
||||
<div data-test-welcome-content>
|
||||
{{yield to="welcome"}}
|
||||
</div>
|
||||
{{else}}
|
||||
<Wizard::Quickstart
|
||||
@currentStep={{@currentStep}}
|
||||
@steps={{@steps}}
|
||||
@onStepChange={{@onStepChange}}
|
||||
@onDismiss={{@onDismiss}}
|
||||
@hasSubmitBlock={{has-block "submit"}}
|
||||
>
|
||||
<:content>
|
||||
{{yield to="quickstart"}}
|
||||
</:content>
|
||||
|
||||
<:submit>
|
||||
{{#if (has-block "submit")}}
|
||||
{{yield to="submit"}}
|
||||
{{else}}
|
||||
<Hds::Button @text="Done" {{on "click" @onDismiss}} data-test-submit />
|
||||
{{/if}}
|
||||
</:submit>
|
||||
</Wizard::Quickstart>
|
||||
{{/if}}
|
||||
40
ui/app/components/wizard/quickstart.hbs
Normal file
40
ui/app/components/wizard/quickstart.hbs
Normal file
@ -0,0 +1,40 @@
|
||||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<div class="wizard" data-test-quickstart-content>
|
||||
<Hds::Stepper::Nav
|
||||
class="has-top-margin-xl has-bottom-margin-xl"
|
||||
@isInteractive={{false}}
|
||||
@currentStep={{@currentStep}}
|
||||
@steps={{@steps}}
|
||||
/>
|
||||
|
||||
<div class="content" tabindex="0">
|
||||
{{yield to="content"}}
|
||||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
{{#if (gt @currentStep 0)}}
|
||||
<Hds::Button
|
||||
@text="Back"
|
||||
@color="tertiary"
|
||||
@icon="chevron-left"
|
||||
{{on "click" (fn this.onStepChange -1)}}
|
||||
data-test-back-button
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Exit" @color="secondary" {{on "click" @onDismiss}} data-test-cancel />
|
||||
|
||||
{{#if this.isFinalStep}}
|
||||
{{yield to="submit"}}
|
||||
{{else}}
|
||||
<Hds::Button @text="Next" {{on "click" (fn this.onStepChange 1)}} data-test-next-button />
|
||||
{{/if}}
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
62
ui/app/components/wizard/quickstart.ts
Normal file
62
ui/app/components/wizard/quickstart.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* @module QuickStart
|
||||
* QuickStart component holds the wizard content pages and navigation controls.
|
||||
*
|
||||
* @example
|
||||
* <QuickStart @currentStep={{@currentStep}} @steps={{@steps}} @onStepChange={{@onStepChange}} @onDismiss={{@onDismiss}} @hasSubmitBlock={{has-block "submit"}} />
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
/**
|
||||
* The active step
|
||||
*/
|
||||
currentStep: number;
|
||||
/**
|
||||
* Define step information to be shown in the Stepper Nav
|
||||
*/
|
||||
steps: { title: string; description?: string }[];
|
||||
/**
|
||||
* Callback to update viewing state when the wizard is exited.
|
||||
*/
|
||||
onDismiss: CallableFunction;
|
||||
/**
|
||||
* Callback to update the current step when navigating backwards or
|
||||
* forwards through the wizard
|
||||
*/
|
||||
onStepChange: CallableFunction;
|
||||
/**
|
||||
* Helper arg to conditionally render a custom submit button upon
|
||||
* completion of the wizard. Necessary to avoid a nested block error.
|
||||
*/
|
||||
hasSubmitBlock: boolean;
|
||||
}
|
||||
|
||||
export default class QuickStart extends Component<Args> {
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
}
|
||||
|
||||
get isFinalStep() {
|
||||
return this.args.currentStep === this.args.steps.length - 1;
|
||||
}
|
||||
|
||||
@action
|
||||
onStepChange(change: number) {
|
||||
const { currentStep, steps, onStepChange } = this.args;
|
||||
const target = currentStep + change;
|
||||
|
||||
if (target < 0 || target > steps.length - 1) {
|
||||
onStepChange(currentStep);
|
||||
} else {
|
||||
onStepChange(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
ui/app/styles/components/wizard.scss
Normal file
34
ui/app/styles/components/wizard.scss
Normal file
@ -0,0 +1,34 @@
|
||||
@use '../helper-classes/flexbox-and-grid';
|
||||
@use '../helper-classes/spacing';
|
||||
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
.wizard {
|
||||
@extend .is-flex-column;
|
||||
@extend .is-flex-grow-1;
|
||||
// a fixed height is needed for content to stretch properly
|
||||
height: 1px;
|
||||
|
||||
.content {
|
||||
// ensures button bar is always at the bottom even if content is not filled
|
||||
@extend .is-flex-1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.button-bar {
|
||||
@extend .has-padding-m;
|
||||
@extend .is-flex-between;
|
||||
|
||||
background: var(--token-color-surface-primary);
|
||||
box-shadow:
|
||||
0 2px 3px 0 rgba(59, 61, 69, 0.25),
|
||||
0 12px 24px 0 rgba(59, 61, 69, 0.35);
|
||||
|
||||
.hds-button-set {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,3 +103,4 @@
|
||||
@use 'components/unseal-warning';
|
||||
@use 'components/usage-page';
|
||||
@use 'components/vault-loading';
|
||||
@use 'components/wizard';
|
||||
|
||||
@ -34,6 +34,7 @@ export const GENERAL = {
|
||||
confirmButton: '[data-test-confirm-button]', // used most often on modal or confirm popups
|
||||
confirmTrigger: '[data-test-confirm-action-trigger]',
|
||||
copyButton: '[data-test-copy-button]',
|
||||
nextButton: '[data-test-next-button]',
|
||||
revealButton: (label: string) => `[data-test-reveal="${label}"] button`, // intended for Hds::Reveal components
|
||||
// there should only be one submit button per view (e.g. one per form) so this does not need to be dynamic
|
||||
// this button should be used for any kind of "submit" on a form or "save" action.
|
||||
|
||||
182
ui/tests/integration/components/wizard-test.js
Normal file
182
ui/tests/integration/components/wizard-test.js
Normal file
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import sinon from 'sinon';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const SELECTORS = {
|
||||
welcome: '[data-test-welcome-content]',
|
||||
quickstart: '[data-test-quickstart-content]',
|
||||
};
|
||||
|
||||
module('Integration | Component | Wizard', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.steps = [
|
||||
{ title: 'First step' },
|
||||
{ title: 'Another stage' },
|
||||
{ title: 'Almost done' },
|
||||
{ title: 'Finale' },
|
||||
];
|
||||
this.step = 0;
|
||||
this.showWelcome = false;
|
||||
this.onDismiss = sinon.spy();
|
||||
this.onStepChange = sinon.spy();
|
||||
});
|
||||
|
||||
test('it shows welcome content initially, then hides it when entering wizard', async function (assert) {
|
||||
this.set('showWelcome', true);
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:welcome>
|
||||
<div>Some welcome content</div>
|
||||
{{!-- TODO: This will change once the welcome page structure is defined in a follow up PR --}}
|
||||
<Hds::Button @text="Dismiss" {{on "click" this.onDismiss}} />
|
||||
<Hds::Button @text="Enter wizard" {{on "click" (fn (mut this.showWelcome) false)}} data-test-enter-wizard-button />
|
||||
</:welcome>
|
||||
<:quickstart>
|
||||
<div data-test-quickstart-content>Quickstart content</div>
|
||||
</:quickstart>
|
||||
</Wizard>`);
|
||||
|
||||
// Assert welcome content is rendered and quickstart content is not
|
||||
assert.dom(SELECTORS.welcome).exists('Welcome content is rendered initially');
|
||||
assert.dom(SELECTORS.welcome).hasTextContaining('Some welcome content');
|
||||
assert
|
||||
.dom(SELECTORS.quickstart)
|
||||
.doesNotExist('Quickstart content is not rendered when welcome is displayed');
|
||||
|
||||
await click('[data-test-enter-wizard-button]');
|
||||
|
||||
// Assert welcome content is no longer rendered and that quickstart content is rendered
|
||||
assert.dom(SELECTORS.welcome).doesNotExist('Welcome content is hidden after entering wizard');
|
||||
assert.dom(SELECTORS.quickstart).exists('Quickstart content is now rendered');
|
||||
assert.dom(SELECTORS.quickstart).hasTextContaining('Quickstart content');
|
||||
});
|
||||
|
||||
test('it shows custom submit block when provided', async function (assert) {
|
||||
// Go to final step
|
||||
this.set('step', 3);
|
||||
this.onCustomSubmit = sinon.spy();
|
||||
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:quickstart>
|
||||
<div data-test-quickstart-content>Quickstart content</div>
|
||||
</:quickstart>
|
||||
<:submit>
|
||||
<Hds::Button @text="Custom Submit" {{on "click" this.onCustomSubmit}} data-test-custom-submit />
|
||||
</:submit>
|
||||
</Wizard>`);
|
||||
|
||||
assert.dom('[data-test-custom-submit]').exists('Custom submit button is rendered');
|
||||
assert.dom(GENERAL.submitButton).doesNotExist('Default submit button is not rendered');
|
||||
await click('[data-test-custom-submit]');
|
||||
assert.true(this.onCustomSubmit.calledOnce, 'Custom submit handler is called');
|
||||
});
|
||||
|
||||
test('it shows default submit button when custom submit block is not provided', async function (assert) {
|
||||
// Go to final step
|
||||
this.set('step', 3);
|
||||
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:quickstart>
|
||||
<div>Quickstart content</div>
|
||||
</:quickstart>
|
||||
</Wizard>`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.submitButton)
|
||||
.exists('Default submit button is rendered when no custom submit provided');
|
||||
});
|
||||
|
||||
test('it renders next button when not on final step', async function (assert) {
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:quickstart>
|
||||
<div>Quickstart content</div>
|
||||
</:quickstart>
|
||||
</Wizard>`);
|
||||
|
||||
assert.dom(GENERAL.nextButton).exists('Next button is rendered when not on final step');
|
||||
assert.dom(GENERAL.submitButton).doesNotExist('Submit button is not rendered when not on final step');
|
||||
await click(GENERAL.nextButton);
|
||||
assert.true(this.onStepChange.calledOnce, 'onStepChange is called');
|
||||
// Go to final step
|
||||
this.set('step', 3);
|
||||
assert.dom(GENERAL.nextButton).doesNotExist('Next button is not rendered when on the final step');
|
||||
assert.dom(GENERAL.submitButton).exists('Submit button is rendered on final step');
|
||||
});
|
||||
|
||||
test('it renders back button when not on first step', async function (assert) {
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:quickstart>
|
||||
<div>Quickstart content</div>
|
||||
</:quickstart>
|
||||
</Wizard>`);
|
||||
|
||||
assert.dom(GENERAL.backButton).doesNotExist('Back button is not rendered on the first step');
|
||||
this.set('step', 2);
|
||||
assert.dom(GENERAL.backButton).exists('Back button is shown when not on first step');
|
||||
await click(GENERAL.backButton);
|
||||
assert.true(this.onStepChange.calledOnce, 'onStepChange is called');
|
||||
});
|
||||
|
||||
test('it dismisses wizard when exit button is clicked within quickstart', async function (assert) {
|
||||
await render(hbs`<Wizard
|
||||
@title="Example Wizard"
|
||||
@showWelcome={{this.showWelcome}}
|
||||
@currentStep={{this.step}}
|
||||
@steps={{this.steps}}
|
||||
@onStepChange={{this.onStepChange}}
|
||||
@onDismiss={{this.onDismiss}}
|
||||
>
|
||||
<:quickstart>
|
||||
<div>Quickstart content</div>
|
||||
</:quickstart>
|
||||
</Wizard>`);
|
||||
|
||||
assert.dom(GENERAL.cancelButton).exists('Exit button is shown within quickstart');
|
||||
await click(GENERAL.cancelButton);
|
||||
assert.true(this.onDismiss.calledOnce, 'onDismiss is called when exit button is clicked');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user