diff --git a/ui/app/adapters/config-ui/message.js b/ui/app/adapters/config-ui/message.js index eb5fbd3b96c2..6aa3afbed2a5 100644 --- a/ui/app/adapters/config-ui/message.js +++ b/ui/app/adapters/config-ui/message.js @@ -14,4 +14,14 @@ export default class MessageAdapter extends ApplicationAdapter { const { authenticated } = query; return super.query(store, type, { authenticated, list: true }); } + + queryRecord(store, type, id) { + return this.ajax(`${this.buildURL(type)}/${id}`, 'GET'); + } + + updateRecord(store, type, snapshot) { + return this.ajax(`${this.buildURL(type)}/${snapshot.record.id}`, 'POST', { + data: this.serialize(snapshot.record), + }); + } } diff --git a/ui/app/models/config-ui/message.js b/ui/app/models/config-ui/message.js index 6bff9d42752d..5f01062b357f 100644 --- a/ui/app/models/config-ui/message.js +++ b/ui/app/models/config-ui/message.js @@ -4,8 +4,7 @@ */ import Model, { attr } from '@ember-data/model'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -import { isAfter, format, addDays, startOfDay } from 'date-fns'; -import { datetimeLocalStringFormat, parseAPITimestamp } from 'core/utils/date-formatters'; +import { isAfter, addDays, startOfDay } from 'date-fns'; import { withModelValidations } from 'vault/decorators/model-validations'; import { withFormFields } from 'vault/decorators/model-form-fields'; @@ -77,7 +76,7 @@ export default class MessageModel extends Model { editType: 'dateTimeLocal', label: 'Message starts', subText: 'Defaults to 12:00 a.m. the following day (local timezone).', - defaultValue: format(addDays(startOfDay(new Date() || this.startTime), 1), datetimeLocalStringFormat), + defaultValue: addDays(startOfDay(new Date() || this.startTime), 1).toISOString(), }) startTime; @attr('date', { editType: 'yield', label: 'Message expires' }) endTime; @@ -90,7 +89,7 @@ export default class MessageModel extends Model { // date helpers get isStartTimeAfterToday() { - return isAfter(parseAPITimestamp(this.startTime), new Date()); + return isAfter(this.startTime, new Date()); } // capabilities diff --git a/ui/app/serializers/config-ui/message.js b/ui/app/serializers/config-ui/message.js index 9bf65ff062da..2635175b4e80 100644 --- a/ui/app/serializers/config-ui/message.js +++ b/ui/app/serializers/config-ui/message.js @@ -3,23 +3,42 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { encodeString } from 'core/utils/b64'; +import { decodeString, encodeString } from 'core/utils/b64'; import ApplicationSerializer from '../application'; export default class MessageSerializer extends ApplicationSerializer { - primaryKey = 'id'; + attrs = { + link: { serialize: false }, + active: { serialize: false }, + }; - serialize() { + normalizeResponse(store, primaryModelClass, payload, id, requestType) { + if (requestType === 'queryRecord') { + const transformed = { + ...payload.data, + message: decodeString(payload.data.message), + link_title: payload.data.link.title, + link_href: payload.data.link.href, + }; + delete transformed.link; + return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType); + } + return super.normalizeResponse(store, primaryModelClass, payload, id, requestType); + } + + serialize(snapshot) { const json = super.serialize(...arguments); json.message = encodeString(json.message); json.link = { - title: json.link_title, - href: json.link_href, + title: json?.link_title || '', + href: json?.link_href || '', }; - - delete json.link_title; - delete json.link_href; - + // using the snapshot startTime and endTime since the json start and end times are null when + // it gets to the serialize function. + json.start_time = snapshot.record.startTime; + json.end_time = snapshot.record.endTime; + delete json?.link_title; + delete json?.link_href; return json; } diff --git a/ui/app/styles/helper-classes/layout.scss b/ui/app/styles/helper-classes/layout.scss index 83f778ef3ccf..60c85718f600 100644 --- a/ui/app/styles/helper-classes/layout.scss +++ b/ui/app/styles/helper-classes/layout.scss @@ -50,6 +50,11 @@ visibility: hidden; } +// overflow +.is-overflow-hidden { + overflow: hidden; +} + // width and height .is-fullwidth { width: 100%; @@ -59,6 +64,10 @@ width: 75%; } +.is-two-thirds-width { + width: 66%; +} + .is-auto-width { width: auto; } @@ -75,6 +84,10 @@ height: 125px; } +.is-calc-large-height { + height: calc($desktop * 0.66); +} + // float .is-pulled-left { float: left !important; diff --git a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs index 39e6f0f61f58..1a734e5af5aa 100644 --- a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs +++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs @@ -5,7 +5,7 @@
@@ -58,8 +58,14 @@ {{/each}} - {{! TODO: VAULT-21533 preview modal }} - + -
\ No newline at end of file + + +{{#if this.showMessagePreviewModal}} + {{#if (eq @message.type "modal")}} + + + {{@message.title}} + + + {{@message.message}} + {{#if @message.linkHref}} + + {{@message.linkTitle}} + + {{/if}} + + + + + + {{else}} + + {{/if}} +{{/if}} \ No newline at end of file diff --git a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js index 49cf1089c476..2bfa880594a9 100644 --- a/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js +++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js @@ -21,11 +21,13 @@ import { inject as service } from '@ember/service'; export default class MessagesList extends Component { @service router; + @service store; @service flashMessages; @tracked errorBanner = ''; @tracked modelValidations; @tracked invalidFormMessage; + @tracked showMessagePreviewModal = false; willDestroy() { super.willDestroy(); @@ -36,15 +38,6 @@ export default class MessagesList extends Component { } } - get breadcrumbs() { - const authenticated = - this.args.message.authenticated === undefined ? true : this.args.message.authenticated; - return [ - { label: 'Messages', route: 'messages.index', query: { authenticated } }, - { label: 'Create Message' }, - ]; - } - @task *save(event) { event.preventDefault(); @@ -55,17 +48,9 @@ export default class MessagesList extends Component { if (isValid) { const { isNew } = this.args.message; - - // We do these checks here since there could be a scenario where startTime and endTime are strings. - // The model expects these attrs to be a date object, so we will need to update these attrs to be in - // date object format. - if (typeof this.args.message.startTime === 'string') - this.args.message.startTime = new Date(this.args.message.startTime); - if (typeof this.args.message.endTime === 'string') - this.args.message.endTime = new Date(this.args.message.endTime); - - const { id } = yield this.args.message.save(); - this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the message.`); + const { id, title } = yield this.args.message.save(); + this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} ${title} message.`); + this.store.clearDataset('config-ui/message'); this.router.transitionTo('vault.cluster.config-ui.messages.message.details', id); } } catch (error) { diff --git a/ui/lib/config-ui/addon/components/messages/page/list.hbs b/ui/lib/config-ui/addon/components/messages/page/list.hbs index 8ec009d2084c..2cf43921ac09 100644 --- a/ui/lib/config-ui/addon/components/messages/page/list.hbs +++ b/ui/lib/config-ui/addon/components/messages/page/list.hbs @@ -22,7 +22,7 @@
{{#if @messages.length}} - {{#each this.getMessages as |message|}} + {{#each this.formattedMessages as |message|}} { let badgeDisplayText = ''; - if (message.active) { if (message.endTime) { badgeDisplayText = `Active until ${dateFormat([message.endTime, 'MMM d, yyyy hh:mm aaa'], { @@ -68,5 +69,7 @@ export default class MessagesList extends Component { *deleteMessage(message) { this.store.clearDataset('config-ui/message'); yield message.destroyRecord(message.id); + this.router.transitionTo('vault.cluster.config-ui.messages'); + this.flashMessages.success(`Successfully deleted ${message.title}.`); } } diff --git a/ui/lib/config-ui/addon/components/messages/preview-image.hbs b/ui/lib/config-ui/addon/components/messages/preview-image.hbs new file mode 100644 index 000000000000..c30c8deaba11 --- /dev/null +++ b/ui/lib/config-ui/addon/components/messages/preview-image.hbs @@ -0,0 +1,40 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + + + + {{@message.title}} + + {{@message.message}} + {{#if @message.linkHref}} + + {{@message.linkTitle}} + + {{/if}} + + + {{if + + + + + \ No newline at end of file diff --git a/ui/lib/config-ui/addon/routes/messages/create.js b/ui/lib/config-ui/addon/routes/messages/create.js index f6361394810d..40102e0b7c7b 100644 --- a/ui/lib/config-ui/addon/routes/messages/create.js +++ b/ui/lib/config-ui/addon/routes/messages/create.js @@ -22,4 +22,13 @@ export default class MessagesCreateRoute extends Route { authenticated, }); } + + setupController(controller, resolvedModel) { + super.setupController(controller, resolvedModel); + + controller.breadcrumbs = [ + { label: 'Messages', route: 'messages', query: { authenticated: !!resolvedModel.authenticated } }, + { label: 'Create Message' }, + ]; + } } diff --git a/ui/lib/config-ui/addon/routes/messages/message/edit.js b/ui/lib/config-ui/addon/routes/messages/message/edit.js index 44146b493524..6d56a14e027f 100644 --- a/ui/lib/config-ui/addon/routes/messages/message/edit.js +++ b/ui/lib/config-ui/addon/routes/messages/message/edit.js @@ -4,5 +4,23 @@ */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class MessagesMessageEditRoute extends Route {} +export default class MessagesMessageEditRoute extends Route { + @service store; + + model() { + const { id } = this.paramsFor('messages.message'); + + return this.store.queryRecord('config-ui/message', id); + } + + setupController(controller, resolvedModel) { + super.setupController(controller, resolvedModel); + + controller.breadcrumbs = [ + { label: 'Messages', route: 'messages', query: { authenticated: resolvedModel.authenticated } }, + { label: 'Edit Message' }, + ]; + } +} diff --git a/ui/lib/config-ui/addon/templates/messages/create.hbs b/ui/lib/config-ui/addon/templates/messages/create.hbs index 6b7f2b648b33..e78c5a7bfe5c 100644 --- a/ui/lib/config-ui/addon/templates/messages/create.hbs +++ b/ui/lib/config-ui/addon/templates/messages/create.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/config-ui/addon/templates/messages/message/edit.hbs b/ui/lib/config-ui/addon/templates/messages/message/edit.hbs index de0a22cde6fe..e78c5a7bfe5c 100644 --- a/ui/lib/config-ui/addon/templates/messages/message/edit.hbs +++ b/ui/lib/config-ui/addon/templates/messages/message/edit.hbs @@ -3,5 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -Message Edit -{{outlet}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/public/images/custom-messages-dashboard.png b/ui/public/images/custom-messages-dashboard.png new file mode 100644 index 000000000000..767c3c5d4b0c Binary files /dev/null and b/ui/public/images/custom-messages-dashboard.png differ diff --git a/ui/public/images/custom-messages-login.png b/ui/public/images/custom-messages-login.png new file mode 100644 index 000000000000..ad29bdf37f23 Binary files /dev/null and b/ui/public/images/custom-messages-login.png differ diff --git a/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js b/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js index d375357d4cf4..e45173bab06d 100644 --- a/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js +++ b/ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js @@ -19,6 +19,12 @@ const PAGE = { button: (buttonName) => `[data-test-button="${buttonName}"]`, inlineErrorMessage: `[data-test-inline-error-message]`, fieldVaildation: (fieldName) => `[data-test-field-validation="${fieldName}"]`, + modal: (name) => `[data-test-modal="${name}"]`, + modalTitle: (title) => `[data-test-modal-title="${title}"]`, + modalBody: '[data-test-modal-body]', + modalButton: (name) => `[data-test-modal-button="${name}"]`, + alertTitle: (name) => `[data-test-alert-title="${name}"]`, + alertDescription: (name) => `[data-test-alert-description="${name}"]`, }; module('Integration | Component | messages/page/create-and-edit-message', function (hooks) { @@ -112,6 +118,8 @@ module('Integration | Component | messages/page/create-and-edit-message', functi message: 'Blah blah blah. Some super long message.', start_time: '2023-12-12T08:00:00.000Z', end_time: '2023-12-21T08:00:00.000Z', + link_title: 'Learn more', + link_href: 'www.learnmore.com', }); this.message = this.store.peekRecord('config-ui/message', 'hhhhh-iiii-lllll-dddd'); await render(hbs``, { @@ -129,7 +137,9 @@ module('Integration | Component | messages/page/create-and-edit-message', functi assert.dom(PAGE.input('title')).hasValue('Hello world'); assert.dom(PAGE.input('message')).hasValue('Blah blah blah. Some super long message.'); assert.dom(PAGE.input('linkTitle')).exists(); + assert.dom(PAGE.input('linkTitle')).hasValue('Learn more'); assert.dom(PAGE.input('linkHref')).exists(); + assert.dom(PAGE.input('linkHref')).hasValue('www.learnmore.com'); await click('#specificDate'); assert .dom(PAGE.input('startTime')) @@ -138,4 +148,43 @@ module('Integration | Component | messages/page/create-and-edit-message', functi .dom(PAGE.input('endTime')) .hasValue(format(new Date(this.message.endTime), datetimeLocalStringFormat)); }); + + test('it should show a preview image modal when preview is clicked', async function (assert) { + await render(hbs``, { + owner: this.engine, + }); + await fillIn(PAGE.input('title'), 'Awesome custom message title'); + await fillIn( + PAGE.input('message'), + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar mattis nunc sed blandit libero volutpat sed cras ornare.' + ); + await click(PAGE.button('preview')); + assert.dom(PAGE.modal('preview modal')).doesNotExist(); + assert.dom(PAGE.modal('preview image')).exists(); + assert.dom(PAGE.alertTitle('Awesome custom message title')).hasText('Awesome custom message title'); + assert + .dom(PAGE.alertDescription('Awesome custom message title')) + .hasText( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar mattis nunc sed blandit libero volutpat sed cras ornare.' + ); + assert.dom('img').hasAttribute('src', '/ui/images/custom-messages-dashboard.png'); + await click(PAGE.modalButton('Close')); + await click('#unauthenticated'); + await click(PAGE.button('preview')); + assert.dom('img').hasAttribute('src', '/ui/images/custom-messages-login.png'); + }); + + test('it should show a preview modal when preview is clicked', async function (assert) { + await render(hbs``, { + owner: this.engine, + }); + await click(PAGE.radio('modal')); + await fillIn(PAGE.input('title'), 'Preview modal title'); + await fillIn(PAGE.input('message'), 'Some preview modal message thats super long.'); + await click(PAGE.button('preview')); + assert.dom(PAGE.modal('preview modal')).exists(); + assert.dom(PAGE.modal('preview image')).doesNotExist(); + assert.dom(PAGE.modalTitle('Preview modal title')).hasText('Preview modal title'); + assert.dom(PAGE.modalBody).hasText('Some preview modal message thats super long.'); + }); });