UI: Use HDS::Toast for flash messages (#25459)

* Move global-flash to HDS-specified area

* Add flash-toast component

* use flash toast for flash messages

* Use spacing vars

* Remove unnecessary key

* Cleanup + tests

* Remove nondeterministic build warning

* add changelog

* I wish this was automatic
This commit is contained in:
Chelsea Shaw
2024-02-15 18:06:51 -06:00
committed by GitHub
parent 221cb24cdd
commit c5d39c816a
8 changed files with 113 additions and 22 deletions

3
changelog/25459.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:change
ui: flash messages render on right side of page
```

View File

@@ -0,0 +1,11 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<Hds::Toast @color={{this.color}} @onDismiss={{@close}} class="has-bottom-margin-m" data-test-flash-toast as |T|>
<T.Title data-test-flash-toast-title>{{this.title}}</T.Title>
<T.Description data-test-flash-message-body>
<p class="is-word-break">{{@flash.message}}</p>
</T.Description>
</Hds::Toast>

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { capitalize } from '@ember/string';
import Component from '@glimmer/component';
/**
* FlashToast components are used to translate flash messages into toast notifications.
* Flash object passed should have a `type` and `message` property at minimum.
*/
export default class FlashToastComponent extends Component {
get color() {
switch (this.args.flash.type) {
case 'info':
return 'highlight';
case 'danger':
return 'critical';
case 'warning':
case 'success':
return this.args.flash.type;
default:
return 'neutral';
}
}
get title() {
if (this.args.title) return this.args.title;
switch (this.args.flash.type) {
case 'danger':
return 'Error';
default:
return capitalize(this.args.flash.type);
}
}
}

View File

@@ -4,10 +4,10 @@
*/
.global-flash {
bottom: 0;
left: $spacing-12;
bottom: $spacing-32;
right: $spacing-24;
margin: 10px;
max-width: $drawer-width;
max-width: 360px;
position: fixed;
width: 95%;
z-index: 300;

View File

@@ -75,24 +75,7 @@
<div class="global-flash">
{{#each this.flashMessages.queue as |flash|}}
<FlashMessage data-test-flash-message={{true}} @flash={{flash}} as |customComponent flash close|>
{{#if flash.componentName}}
{{component flash.componentName content=flash.content}}
{{else}}
{{#let (hash info="highlight" success="success" danger="critical" warning="warning") as |color|}}
<Hds::Alert @type="inline" @color={{get color flash.type}} class="has-bottom-margin-s" @onDismiss={{close}} as |A|>
{{#let (hash info="Info" success="Success" danger="Error" warning="Warning") as |title|}}
<A.Title class="alert-title">{{get title flash.type}}</A.Title>
{{/let}}
<A.Description data-test-flash-message-body>
{{#if flash.preformatted}}
<p class="is-word-break">{{flash.message}}</p>
{{else}}
{{flash.message}}
{{/if}}
</A.Description>
</Hds::Alert>
{{/let}}
{{/if}}
<FlashToast @flash={{flash}} @close={{close}} />
</FlashMessage>
{{/each}}
</div>

View File

@@ -17,7 +17,6 @@ IF YOUR TOKEN HAS THE PROPER CAPABILITIES, THIS WILL CREATE AND DELETE ITEMS ON
Your token will also be shown on the screen in the example curl command output.`;
this.flashMessages.warning(warning, {
sticky: true,
preformatted: true,
});
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, find, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
module('Integration | Component | flash-toast', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.flash = {
type: 'info',
message: 'The bare minimum flash message',
};
this.closeSpy = sinon.spy();
});
test('it renders', async function (assert) {
await render(hbs`<FlashToast @flash={{this.flash}} @close={{this.closeSpy}} />`);
assert.dom('[data-test-flash-message-body]').hasText('The bare minimum flash message');
assert.dom('[data-test-flash-toast]').hasClass('hds-alert--color-highlight');
await click('button');
assert.ok(this.closeSpy.calledOnce, 'close action was called');
});
[
{ type: 'info', title: 'Info', color: 'hds-alert--color-highlight' },
{ type: 'success', title: 'Success', color: 'hds-alert--color-success' },
{ type: 'warning', title: 'Warning', color: 'hds-alert--color-warning' },
{ type: 'danger', title: 'Error', color: 'hds-alert--color-critical' },
{ type: 'foobar', title: 'Foobar', color: 'hds-alert--color-neutral' },
].forEach(({ type, title, color }) => {
test(`it has correct title and color for type: ${type}`, async function (assert) {
this.flash.type = type;
await render(hbs`<FlashToast @flash={{this.flash}} @close={{this.closeSpy}} />`);
assert.dom('[data-test-flash-toast-title]').hasText(title, 'title is correct');
assert.dom('[data-test-flash-toast]').hasClass(color, 'color is correct');
});
});
test('it renders messages with whitespaces correctly', async function (assert) {
this.flash.message = `multi-
line msg`;
await render(hbs`<FlashToast @flash={{this.flash}} @close={{this.closeSpy}} />`);
const dom = find('[data-test-flash-message-body]');
const lineHeight = 20;
assert.true(dom.clientHeight > lineHeight, 'renders message on multiple lines');
});
});