diff --git a/changelog/25459.txt b/changelog/25459.txt
new file mode 100644
index 0000000000..67787b67a5
--- /dev/null
+++ b/changelog/25459.txt
@@ -0,0 +1,3 @@
+```release-note:change
+ui: flash messages render on right side of page
+```
diff --git a/ui/app/components/flash-toast.hbs b/ui/app/components/flash-toast.hbs
new file mode 100644
index 0000000000..a25c338e9d
--- /dev/null
+++ b/ui/app/components/flash-toast.hbs
@@ -0,0 +1,11 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ {{this.title}}
+
+ {{@flash.message}}
+
+
\ No newline at end of file
diff --git a/ui/app/components/flash-toast.js b/ui/app/components/flash-toast.js
new file mode 100644
index 0000000000..fd971694ce
--- /dev/null
+++ b/ui/app/components/flash-toast.js
@@ -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);
+ }
+ }
+}
diff --git a/ui/app/services/flash-messages.ts b/ui/app/services/flash-messages.js
similarity index 100%
rename from ui/app/services/flash-messages.ts
rename to ui/app/services/flash-messages.js
diff --git a/ui/app/styles/components/global-flash.scss b/ui/app/styles/components/global-flash.scss
index c1dc06117a..933ebe61a1 100644
--- a/ui/app/styles/components/global-flash.scss
+++ b/ui/app/styles/components/global-flash.scss
@@ -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;
diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs
index f0ee1b39c5..a2391b0ad9 100644
--- a/ui/app/templates/vault/cluster.hbs
+++ b/ui/app/templates/vault/cluster.hbs
@@ -75,24 +75,7 @@
{{#each this.flashMessages.queue as |flash|}}
- {{#if flash.componentName}}
- {{component flash.componentName content=flash.content}}
- {{else}}
- {{#let (hash info="highlight" success="success" danger="critical" warning="warning") as |color|}}
-
- {{#let (hash info="Info" success="Success" danger="Error" warning="Warning") as |title|}}
- {{get title flash.type}}
- {{/let}}
-
- {{#if flash.preformatted}}
- {{flash.message}}
- {{else}}
- {{flash.message}}
- {{/if}}
-
-
- {{/let}}
- {{/if}}
+
{{/each}}
diff --git a/ui/lib/open-api-explorer/addon/routes/index.js b/ui/lib/open-api-explorer/addon/routes/index.js
index 08c5ea970e..57408807b9 100644
--- a/ui/lib/open-api-explorer/addon/routes/index.js
+++ b/ui/lib/open-api-explorer/addon/routes/index.js
@@ -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,
});
}
}
diff --git a/ui/tests/integration/components/flash-toast-test.js b/ui/tests/integration/components/flash-toast-test.js
new file mode 100644
index 0000000000..ee648f7d3f
--- /dev/null
+++ b/ui/tests/integration/components/flash-toast-test.js
@@ -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``);
+
+ 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``);
+
+ 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``);
+ const dom = find('[data-test-flash-message-body]');
+ const lineHeight = 20;
+ assert.true(dom.clientHeight > lineHeight, 'renders message on multiple lines');
+ });
+});