diff --git a/ui/app/adapters/config-ui/message.js b/ui/app/adapters/config-ui/message.js
index c5ba6f711dbb..cdf7d5c94b9c 100644
--- a/ui/app/adapters/config-ui/message.js
+++ b/ui/app/adapters/config-ui/message.js
@@ -4,5 +4,26 @@
*/
import ApplicationAdapter from '../application';
+import { encodePath } from 'vault/utils/path-encoding-helpers';
-export default class MessageAdapter extends ApplicationAdapter {}
+export default class MessageAdapter extends ApplicationAdapter {
+ getCustomMessagesUrl(id) {
+ let url = `${this.buildURL()}/config/ui/custom-messages`;
+
+ if (id) {
+ url = url + '/' + encodePath(id);
+ }
+ return url;
+ }
+
+ query(store, type, query) {
+ const { authenticated } = query;
+
+ return this.ajax(this.getCustomMessagesUrl(), 'GET', { data: { authenticated } });
+ }
+
+ deleteRecord(store, type, snapshot) {
+ const { id } = snapshot;
+ return this.ajax(this.getCustomMessagesUrl(id), 'DELETE');
+ }
+}
diff --git a/ui/app/components/sidebar/nav/cluster.hbs b/ui/app/components/sidebar/nav/cluster.hbs
index edd7cb0d2e1f..cec9e585d83e 100644
--- a/ui/app/components/sidebar/nav/cluster.hbs
+++ b/ui/app/components/sidebar/nav/cluster.hbs
@@ -120,6 +120,7 @@
Settings
diff --git a/ui/app/models/config-ui/message.js b/ui/app/models/config-ui/message.js
index ba57a402f832..e2f89bba74ce 100644
--- a/ui/app/models/config-ui/message.js
+++ b/ui/app/models/config-ui/message.js
@@ -5,6 +5,8 @@
import Model, { attr } from '@ember-data/model';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
+import { isAfter } from 'date-fns';
+import { parseAPITimestamp } from 'core/utils/date-formatters';
export default class MessageModel extends Model {
@attr('boolean') active;
@@ -16,6 +18,12 @@ export default class MessageModel extends Model {
@attr('string') startTime;
@attr('string') endTime;
+ // date helpers
+ get isStartTimeAfterToday() {
+ return isAfter(parseAPITimestamp(this.startTime), new Date());
+ }
+
+ // capabilities
@lazyCapabilities(apiPath`sys/config/ui/custom-messages`) customMessagesPath;
get canCreateCustomMessages() {
diff --git a/ui/app/serializers/config-ui/message.js b/ui/app/serializers/config-ui/message.js
index b711d10c01d5..77b60cc1c5c9 100644
--- a/ui/app/serializers/config-ui/message.js
+++ b/ui/app/serializers/config-ui/message.js
@@ -5,4 +5,17 @@
import ApplicationSerializer from '../application';
-export default class MessageSerializer extends ApplicationSerializer {}
+export default class MessageSerializer extends ApplicationSerializer {
+ primaryKey = 'id';
+
+ extractLazyPaginatedData(payload) {
+ if (payload.data) {
+ if (payload.data?.keys && Array.isArray(payload.data.keys)) {
+ return payload.data.keys.map((key) => ({ id: key, ...payload.data.key_info[key] }));
+ }
+ Object.assign(payload, payload.data);
+ delete payload.data;
+ }
+ return payload;
+ }
+}
diff --git a/ui/lib/config-ui/addon/components/messages/page/list.hbs b/ui/lib/config-ui/addon/components/messages/page/list.hbs
new file mode 100644
index 000000000000..d84c39ff63f5
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/list.hbs
@@ -0,0 +1,101 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ <:toolbarActions>
+
+ Create message
+
+
+
+
+{{#if @messages.length}}
+ {{#each this.getMessages as |message|}}
+
+
+
+
+
+
+ {{message.title}}
+
+
+
+
+
+
+
+
+
+
+ {{/each}}
+
+{{else}}
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/components/messages/page/list.js b/ui/lib/config-ui/addon/components/messages/page/list.js
new file mode 100644
index 000000000000..10427746e5ff
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/list.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { dateFormat } from 'core/helpers/date-format';
+
+/**
+ * @module Page::MessagesList
+ * Page::MessagesList components are used to display list of messages.
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {array} messages - array message objects
+ */
+
+export default class MessagesList extends Component {
+ @service store;
+
+ get getMessages() {
+ return this.args.messages.map((message) => {
+ let badgeDisplayText = '';
+
+ if (message.active) {
+ if (message.endTime) {
+ badgeDisplayText = `Active until ${dateFormat([message.endTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ } else {
+ badgeDisplayText = 'Active';
+ }
+ } else {
+ if (message.isStartTimeAfterToday) {
+ badgeDisplayText = `Scheduled: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ } else {
+ badgeDisplayText = `Inactive: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ }
+ }
+
+ message.badgeDisplayText = badgeDisplayText;
+ return message;
+ });
+ }
+
+ // callback from HDS pagination to set the queryParams page
+ get paginationQueryParams() {
+ return (page) => {
+ return {
+ page,
+ };
+ };
+ }
+
+ @task
+ *deleteMessage(message) {
+ this.store.clearDataset('config-ui/message');
+ yield message.destroyRecord(message.id);
+ }
+}
diff --git a/ui/lib/config-ui/addon/components/messages/tab-page-header.hbs b/ui/lib/config-ui/addon/components/messages/tab-page-header.hbs
new file mode 100644
index 000000000000..10d2d528e77f
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/tab-page-header.hbs
@@ -0,0 +1,52 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ {{#if @breadcrumbs}}
+
+
+
+ {{/if}}
+
+
+ Custom messages
+
+
+
+
+
+
+
+
+{{#if (or (has-block "toolbarFilters") (has-block "toolbarActions"))}}
+
+
+ {{yield to="toolbarFilters"}}
+
+
+ {{yield to="toolbarActions"}}
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/controllers/messages/index.js b/ui/lib/config-ui/addon/controllers/messages/index.js
new file mode 100644
index 000000000000..a3247161d99a
--- /dev/null
+++ b/ui/lib/config-ui/addon/controllers/messages/index.js
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Controller from '@ember/controller';
+export default class MessagesController extends Controller {
+ queryParams = ['authenticated', 'page'];
+
+ authenticated = true;
+ page = 1;
+}
diff --git a/ui/lib/config-ui/addon/routes/messages/index.js b/ui/lib/config-ui/addon/routes/messages/index.js
index e0cd5f4bbf9b..ca8cc6d1d970 100644
--- a/ui/lib/config-ui/addon/routes/messages/index.js
+++ b/ui/lib/config-ui/addon/routes/messages/index.js
@@ -4,5 +4,34 @@
*/
import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
-export default class MessagesRoute extends Route {}
+export default class MessagesRoute extends Route {
+ @service store;
+
+ queryParams = {
+ page: {
+ refreshModel: true,
+ },
+ authenticated: {
+ refreshModel: true,
+ },
+ };
+
+ async model(params) {
+ try {
+ const { authenticated, page } = params;
+ return await this.store.lazyPaginatedQuery('config-ui/message', {
+ authenticated,
+ responsePath: 'data.keys',
+ page: page || 1,
+ });
+ } catch (e) {
+ if (e.httpStatus === 404) {
+ return [];
+ }
+
+ throw e;
+ }
+ }
+}
diff --git a/ui/lib/config-ui/addon/templates/messages/index.hbs b/ui/lib/config-ui/addon/templates/messages/index.hbs
index d17f40698bbe..d2929e103697 100644
--- a/ui/lib/config-ui/addon/templates/messages/index.hbs
+++ b/ui/lib/config-ui/addon/templates/messages/index.hbs
@@ -3,4 +3,4 @@
SPDX-License-Identifier: BUSL-1.1
~}}
-Messages List View
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui/mirage/handlers/custom-messages.js b/ui/mirage/handlers/custom-messages.js
index 39f64d59f06f..2591cb6f1896 100644
--- a/ui/mirage/handlers/custom-messages.js
+++ b/ui/mirage/handlers/custom-messages.js
@@ -5,35 +5,56 @@
export default function (server) {
server.get('/sys/config/ui/custom-messages', (schema, request) => {
- if (request.queryParams.authenticated === 'true') {
+ if (request.queryParams.authenticated && JSON.parse(request.queryParams.authenticated)) {
return {
data: {
- 'key-info': {
+ key_info: {
'01234567-89ab-cdef-0123-456789abcdef': {
- title: 'Authenticated custom message title',
+ title: 'Has expiration date',
type: 'modal',
authenticated: true,
start_time: '2023-10-15T02:36:43.986212308Z',
- end_time: '2024-10-15T02:36:43.986212308Z',
+ end_time: '2023-12-17T02:36:43.986212308Z',
+ active: true,
+ },
+ '22234567-89ab-cdef-0123-456789abcdef': {
+ title: 'No expiration date',
+ type: 'modal',
+ authenticated: true,
+ start_time: '2023-10-15T02:36:43.986212308Z',
+ end_time: '',
active: true,
},
'76543210-89ab-cdef-0123-456789abcdef': {
- title: 'Authenticated custom message title two',
+ title: 'Inactive message',
+ type: 'banner',
+ authenticated: true,
+ start_time: '2023-10-15T02:36:43.986212308Z',
+ end_time: '2023-11-15T02:36:43.986212308Z',
+ active: false,
+ },
+ '11543210-89ab-cdef-0123-456789abcdef': {
+ title: 'Inactive, but start time is past current date',
type: 'banner',
authenticated: true,
- start_time: '2021-10-15T02:36:43.986212308Z',
- end_time: '2021-11-15T02:36:43.986212308Z',
+ start_time: '2024-10-15T02:36:43.986212308Z',
+ end_time: '2024-11-15T02:36:43.986212308Z',
active: false,
},
},
- keys: ['01234567-89ab-cdef-0123-456789abcdef', '76543210-89ab-cdef-0123-456789abcdef'],
+ keys: [
+ '01234567-89ab-cdef-0123-456789abcdef',
+ '22234567-89ab-cdef-0123-456789abcdef',
+ '76543210-89ab-cdef-0123-456789abcdef',
+ '11543210-89ab-cdef-0123-456789abcdef',
+ ],
},
};
}
return {
data: {
- 'key-info': {
+ key_info: {
'8d6ba39-5c23-50af-3d79-76c26a2845f49': {
title: 'Unauthenticated custom message title',
type: 'modal',
@@ -72,7 +93,7 @@ export default function (server) {
server.get('/sys/internal/ui/unauthenticated-messages', () => {
return {
data: {
- 'key-info': {
+ key_info: {
'01234567-89ab-cdef-0123-456789abcdef': {
title: 'Unauthenticated Title One',
message:
@@ -102,7 +123,7 @@ export default function (server) {
server.get('/sys/internal/ui/authenticated-messages', () => {
return {
data: {
- 'key-info': {
+ key_info: {
'6543210-89ab-cdef-0123-456780abcieh': {
title: 'Authenticated Title One',
message:
diff --git a/ui/tests/integration/components/config-ui/messages/page/list-test.js b/ui/tests/integration/components/config-ui/messages/page/list-test.js
new file mode 100644
index 000000000000..8323edae8718
--- /dev/null
+++ b/ui/tests/integration/components/config-ui/messages/page/list-test.js
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setupEngine } from 'ember-engines/test-support';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+// TODO: test file needs to be updated to use mirage handler and to have the correct META pagination numbers
+
+const META = {
+ currentPage: 1,
+ lastPage: 2,
+ nextPage: 2,
+ prevPage: 1,
+ total: 16,
+ filteredTotal: 16,
+ pageSize: 15,
+};
+
+module('Integration | Component | messages/page/list', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'config-ui');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.context = { owner: this.engine };
+ this.store = this.owner.lookup('service:store');
+
+ this.store.pushPayload('config-ui/message', {
+ modelName: 'config-ui/message',
+ id: '01234567-89ab-cdef-0123-456789abcdef',
+ active: true,
+ type: 'banner',
+ authenticated: true,
+ title: 'Message title 1',
+ message: 'Some long long long message',
+ link: { title: 'here', href: 'www.example.com' },
+ startTime: '2021-08-01T00:00:00Z',
+ endTime: '',
+ });
+ this.store.pushPayload('config-ui/message', {
+ modelName: 'config-ui/message',
+ id: '01234567-89ab-dddd-0123-456789abcdef',
+ active: false,
+ type: 'modal',
+ authenticated: true,
+ title: 'Message title 2',
+ message: 'Some long long long message blah blah blah',
+ link: { title: 'here', href: 'www.example2.com' },
+ startTime: '2023-08-01T00:00:00Z',
+ endTime: '2023-08-01T00:00:00Z',
+ });
+ this.store.pushPayload('config-ui/message', {
+ modelName: 'config-ui/message',
+ id: '01234567-89ab-vvvv-0123-456789abcdef',
+ active: true,
+ type: 'banner',
+ authenticated: false,
+ title: 'Message title 3',
+ message: 'Some long long long message',
+ link: { title: 'here', href: 'www.example.com' },
+ startTime: '2021-08-01T00:00:00Z',
+ endTime: '2090-08-01T00:00:00Z',
+ });
+ });
+
+ test('it should show the messages empty state', async function (assert) {
+ this.messages = [];
+
+ await render(hbs``, {
+ owner: this.engine,
+ });
+
+ assert.dom('[data-test-empty-state-title]').hasText('No messages yet');
+ assert
+ .dom('[data-test-empty-state-message]')
+ .hasText(
+ 'Add a custom message for all users after they log into Vault. Create message to get started.'
+ );
+ assert.dom('[data-test-empty-state-actions] a').hasText('Create message');
+ });
+
+ test('it should show the list of custom messages', async function (assert) {
+ this.messages = this.store.peekAll('config-ui/message', {});
+ this.messages.meta = META;
+
+ await render(hbs``, {
+ owner: this.engine,
+ });
+ assert.dom('[data-test-icon="message-circle"]').exists();
+ for (const message of this.messages) {
+ assert.dom(`[data-test-list-item="${message.id}"]`).exists();
+ assert.dom(`[data-linked-block-title="${message.id}"]`).hasText(message.title);
+ // TODO: add tests for active/inactive badges
+ }
+ });
+});