diff --git a/addons/rose/addon/components/rose/message.js b/addons/rose/addon/components/rose/message.js
new file mode 100644
index 000000000..89ac42ee5
--- /dev/null
+++ b/addons/rose/addon/components/rose/message.js
@@ -0,0 +1,7 @@
+import Component from '@ember/component';
+import layout from '../../templates/components/rose/message';
+
+export default Component.extend({
+ layout,
+ tagName: '',
+});
diff --git a/addons/rose/addon/components/rose/message.stories.mdx b/addons/rose/addon/components/rose/message.stories.mdx
new file mode 100644
index 000000000..2c3e062d9
--- /dev/null
+++ b/addons/rose/addon/components/rose/message.stories.mdx
@@ -0,0 +1,65 @@
+import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
+import { hbs } from 'ember-cli-htmlbars';
+import { action } from '@storybook/addon-actions';
+import { text, boolean, select } from '@storybook/addon-knobs';
+
+
+
+# Message
+
+A message component containing a title, description, and nav links. Title can be configured with optional icon and subtitle.
+
+```html
+
+ {{inline-svg "chevron-left"}} Go back
+ Need help?
+
+```
+
+
+ {{
+ template: hbs`
+
+ {{inline-svg "chevron-left"}} Go back
+ Need help?
+
+ `,
+ context: {
+ title: text('title', 'You are not authorized'),
+ subtitle: text('subtitle', 'Error 403'),
+ description: text('description', 'You must be granted permissions to view this data. Ask your administrator if you think you should have access.'),
+ icon: select(
+ 'icon',
+ {
+ 'alert-circle-outline': 'alert-circle-outline',
+ 'help-circle-outline': 'help-circle-outline',
+ 'disabled': 'disabled',
+ },
+ 'disabled'
+ ),
+ },
+ }}
+
+
+Message can be configured without icons and navigation links.
+```html
+
+```
+
+ {{
+ template: hbs`
+
+ `,
+ context: {
+ title: text('title', 'No logs received yet'),
+ description: text('description', 'Your logs will appear here when we start receiving them. If your server is currently outputting logs, check your configuration.'),
+ },
+ }}
+
+
+Message can be configured with a single left aligned link.
+```html
+
+ Add a namespace
+
+```
diff --git a/addons/rose/addon/components/rose/message/link.js b/addons/rose/addon/components/rose/message/link.js
new file mode 100644
index 000000000..3291d255d
--- /dev/null
+++ b/addons/rose/addon/components/rose/message/link.js
@@ -0,0 +1,7 @@
+import Component from '@ember/component';
+import layout from '../../../templates/components/rose/message/link';
+
+export default Component.extend({
+ layout,
+ tagName: '',
+});
diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss
index f63f9bdf0..1c86e7a83 100644
--- a/addons/rose/addon/styles/rose/components/_index.scss
+++ b/addons/rose/addon/styles/rose/components/_index.scss
@@ -9,3 +9,4 @@
@import 'separator';
@import 'nav';
@import 'table';
+@import 'message';
diff --git a/addons/rose/addon/styles/rose/components/_message.scss b/addons/rose/addon/styles/rose/components/_message.scss
new file mode 100644
index 000000000..c00e2e1fd
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/_message.scss
@@ -0,0 +1,83 @@
+@use '../utilities/type';
+@use '../variables/sizing';
+@use '../variables/media';
+
+.rose-message {
+ @include type.type(s, normal);
+ display: grid;
+ grid-template-areas:
+ 'title title title'
+ 'description description description'
+ 'left-link . right-link';
+ grid-auto-columns: auto 1fr;
+ color: var(--ui-gray);
+ width: sizing.rems(m) * 16; //16 rems/320 px
+
+ .rose-message-title {
+ @include type.type(m, semibold);
+ grid-area: title;
+
+ .rose-message-title-text {
+ display: inline-block;
+ }
+
+ .rose-message-icon {
+ display: inline;
+ svg {
+ vertical-align: top;
+ width: sizing.rems(m) * 2; // 40px
+ }
+ }
+
+ .rose-message-subtitle {
+ @include type.type(xs, normal);
+ $lineHeight: 16 / 15;
+ line-height: $lineHeight; // 16px
+ vertical-align: bottom;
+ }
+ }
+
+ .rose-message-description {
+ grid-area: description;
+ margin: sizing.rems(xs) 0;
+ }
+
+ .rose-message-link {
+ @include type.type(xs, semibold);
+ text-decoration: none;
+ cursor: pointer;
+ --color: var(--ui-gray);
+ color: var(--color);
+ margin: sizing.rems(xs) 0;
+
+ svg {
+ vertical-align: top;
+ width: sizing.rems(m);
+ height: sizing.rems(m);
+ }
+
+ &.rose-message-link-left-align {
+ grid-area: left-link;
+ --color: var(--action);
+ }
+
+ &.rose-message-link-right-align {
+ grid-area: right-link;
+ }
+
+ &:hover {
+ --color: var(--action);
+ }
+ }
+}
+
+@media (max-width: media.width(small)) {
+ .rose-message {
+ grid-template-areas:
+ 'title'
+ 'description'
+ 'right-link'
+ 'left-link';
+ grid-auto-columns: auto 1fr;
+ }
+}
diff --git a/addons/rose/addon/templates/components/rose/message.hbs b/addons/rose/addon/templates/components/rose/message.hbs
new file mode 100644
index 000000000..d15efb596
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/message.hbs
@@ -0,0 +1,25 @@
+
+ {{#if @title}}
+
+ {{#if @icon}}
+
{{inline-svg @icon}}
+ {{/if}}
+
+
+ {{@title}}
+
+ {{#if @subtitle}}
+
{{@subtitle}}
+ {{/if}}
+
+
+ {{/if}}
+
+ {{#if @description}}
+
{{@description}}
+ {{/if}}
+
+ {{yield (hash
+ link=(component 'rose/message/link')
+ )}}
+
diff --git a/addons/rose/addon/templates/components/rose/message/link.hbs b/addons/rose/addon/templates/components/rose/message/link.hbs
new file mode 100644
index 000000000..95337aaf2
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/message/link.hbs
@@ -0,0 +1,13 @@
+{{#if @leftAlign}}
+
+ {{yield}}
+
+{{else}}
+
+ {{yield}}
+
+{{/if}}
diff --git a/addons/rose/app/components/rose/message.js b/addons/rose/app/components/rose/message.js
new file mode 100644
index 000000000..914d08e41
--- /dev/null
+++ b/addons/rose/app/components/rose/message.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/message';
diff --git a/addons/rose/app/components/rose/message/link.js b/addons/rose/app/components/rose/message/link.js
new file mode 100644
index 000000000..fe210dd3a
--- /dev/null
+++ b/addons/rose/app/components/rose/message/link.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/message/link';
diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs
index bb8ae24db..bbcb98065 100644
--- a/addons/rose/tests/dummy/app/templates/index.hbs
+++ b/addons/rose/tests/dummy/app/templates/index.hbs
@@ -1,7 +1,22 @@
-
- Level 1
- Level 2
- Level 3
- Level 4
- Level 5
-
+
+ {{inline-svg "chevron-left"}} Go back
+ Need help?
+
+
+
+ {{inline-svg "chevron-left"}} Go back
+ Need help?
+
+
+
+ {{inline-svg "refresh-default"}} Try again
+ Need help?
+
+
+
+
+
+
+ Add a namespace
+
+
diff --git a/addons/rose/tests/dummy/app/templates/playground.hbs b/addons/rose/tests/dummy/app/templates/playground.hbs
index 634ea800c..00b1c94cf 100644
--- a/addons/rose/tests/dummy/app/templates/playground.hbs
+++ b/addons/rose/tests/dummy/app/templates/playground.hbs
@@ -229,5 +229,13 @@
+
+
+ Level 1
+ Level 2
+ Level 3
+ Level 4
+ Level 5
+
diff --git a/addons/rose/tests/integration/components/rose/message-test.js b/addons/rose/tests/integration/components/rose/message-test.js
new file mode 100644
index 000000000..497974080
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/message-test.js
@@ -0,0 +1,44 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, findAll } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | rose/message', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-message'));
+ });
+
+ test('it renders with attributes', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('#message'));
+ });
+
+ test('it renders with title', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('.rose-message-title').textContent.trim(), 'Title');
+ });
+
+ test('it renders with subtitle', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('.rose-message-subtitle').textContent.trim(), 'Subtitle');
+ });
+
+ test('it renders with description', async function (assert) {
+ await render(hbs``);
+ assert.equal(
+ find('.rose-message-description').textContent.trim(),
+ 'Description'
+ );
+ });
+
+ test('it renders with link', async function (assert) {
+ await render(hbs`
+
+
+ `);
+ assert.equal(findAll('.rose-message-link').length, 2);
+ });
+});
diff --git a/addons/rose/tests/integration/components/rose/message/link-test.js b/addons/rose/tests/integration/components/rose/message/link-test.js
new file mode 100644
index 000000000..b92f3004a
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/message/link-test.js
@@ -0,0 +1,28 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | rose/message/link', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-message-link'));
+ assert.ok(find('.rose-message-link-right-align'));
+ });
+
+ test('it renders with content', async function (assert) {
+ await render(hbs`Link content`);
+ assert.equal(find('.rose-message-link').textContent.trim(), 'Link content');
+ });
+
+ test('it renders left aligned', async function (assert) {
+ this.set('leftAlign', true);
+ await render(hbs``);
+ assert.ok(find('.rose-message-link-left-align'));
+
+ this.set('leftAlign', false);
+ assert.notOk(find('.rose-message-link-left-align'));
+ });
+});