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 + } + }); +});