From b85365e98078bb8a5fc91dd66abf8a1190ec80ea Mon Sep 17 00:00:00 2001
From: Kianna <30884335+kiannaquach@users.noreply.github.com>
Date: Tue, 23 Jan 2024 15:04:17 -0800
Subject: [PATCH] UI: [VAULT-19096] Customizable banners (#23945)
* UI: [VAULT-21521] Initial config-ui engine and routes set up (#23922)
* UI: [VAULT-21526] Create adapter, serializer, and model files (#23947)
* UI: [VAULT-21588] Add Custom Messages to the sidebar (#23946)
* UI: [VAULT-21527] Mirage setup (#24000)
* UI: [VAULT-21530] Custom Messages List View w/ Pagination and LazyPaginatedQuery (#24133)
* UI: Add list to adapter query param (#24187)
* UI: [VAULT-21532] Create message (#24407)
* WIP create message
* Add breadcrumns
* Create and edit form
* Add save to create/edit form
* Add cancel and todo
* Fix cancel route
* Fix breadcrumb label to be title case
* add start time logic
* Update breadcrumb
* Fix breadcrumbs and merge conflict test
* Update create form description
* Fix sidenav so it always highlights
* Fix up forms
* Mostly working create form
* Form cleanup
* Fix link title and href form fields
* Default startTime
* Fix messages
* Update dropdown to use the updated ConfirmAction component
* Update create and edit form
* Add wip tests
* Fix breadcrumb formatter
* Comment out test
* Update create message test
* Update more tests
* Add comment for fixing date on edit
* Update Message form
* Code cleanup!
* Add validation tests
* Remove authenticated from route model
* SOme more code cleanup
* Add controller so authenticated is parsed
* Working radio buttons
* Use an object instead of arrays
* Wip date form
* Fix license headers
* Fix license headers addition of files
* Fix copyright format issues and clean up code
* Fix tests
* Rename FormField radio getter and ay11 improvements
* Address feedback
* Fix specific date so it remembers the values
* Address feedback!
* Update more form fields
* Use formfield action instead
* Update to every
* Update syntax of onchange
* Fix tests
* Update willDestroy so it doesnt break tests
* Remove set and brodcast datetimelocal
* Put FormField back the way it was in favor of putting FormField to a seperate PR
* Remove getter in formfield component file
* Address more feedback
* Put back test
* Update datetime string format var name and location
* UI: [VAULT-21534 VAULT-21533 VAULT-21536] edit, preview, and delete custom message (#24603)
* Working edit
* VAULT-21536 update delete message and create/update flash message
* VAULT-21533 add preview modal
* Update serializer
* Preview refinements
* Move preview to its own component
* Move breadcrumbs to setupController
* Add more tests
* Address some feedback
* Address more feedback!
* Update serailizer
* Remove stylesheet
* Add comment
* UI: [VAULT-21435] Message details (#24645)
* WIP
* Fix timezone bug
* Fix date issues on create/edit form
* Add details screen
* Use allFields instead of formFields
* Fix tests
* Address comments!
* UI: VAULT-21538 unauth endpoint message display (#24665)
* WIP unauth display
* Add modal custom message
* Close multiple modals
* Update todo with ticket number
* On init make custom message request
* Use serializer
* Update fetchMessages
* Add copyright headers
* Add services and serializers
* Send null instead of empty strings
* Fix tests!
* Add copywrite headers
* Add some acceptance tests
* Test cleanup
* Put tests back
* pass hooks to module
* Move module out
* Seperate tests
* Copywrite
* Add aria-prohibited-attr runList options
* Code cleanup
* Add date-time-local transform
* Add copyright headers
* Remove comments
* Remove date transform stuff for now!
* Put getISODateFormat back into the serailize function
* UI: Date time local transform (#24694)
* Date time local
* Add deserialize
* Add copyright header
* check if date exists
* Use parseISO for date strings since datefns requires this in new update
* Update tests
* Ensure we cehck for an ISOString
* Add checks so tests wont fail
* Update parseISO
* Address feedback
* UI: multiple banner message on create and edit form (#24742)
* WIP multiple banner message on create and edit form
* Fix tests
* Put checks back
* Add try/catch to query
* Fix breadcrumbs
* Add page size to pagination
* Add multiple modal message tests
* Address feedback
* Check for valid form first
* Add extra checks
* Address feedback
* Move getter to the route
* Fix tests!
* Address more feedback
* Use still when cancelling
* Update multiple banner modal
* Fix tests
* Set user confirmation to empty string
* UI: VAULT-21539 auth messages display (#24842)
* WIP auth message display
* Move block to show only when authenticated
* VAULT-22046 working search by name
* Some code clean up
* Fix merge conflict
* Add tests
* Fetch messages again after creation
* UI: [VAULT-22908] Update kv object editor, add max number of messages reached modal, small improvements (#24918)
* Update kv object editor to only use a single row
* continute using kv editype
* Fix failing dashboard tests!
* Fix failing test on sidebranch
* Fix tests and update validations
* Add optional tag
* Address feedback
* Add documentation
* Clear messages when logging out
* Fix tests!
* Add 100 message limit modal
* Add max message modal test
* Do more checks!
* Pair with Claire on the refactor of validator!
* Only show validationerror for multiple rows
* Update pageSize to 100 since when paginations are active it causes accessbility errors
* Fix tests!
* Add links to test
* Make banners dismissable
* Add cancel button
* Address feedback!
* Update test selectors
* Update validator
* Remove validations check in kvobjecteditor
* Revert validationError in kvobjecteditor template
* Put back if/else statements for link
* Add changelog
* UI: fix link bug and add colors (#24977)
* Fix edit bug and put transform back
* Edit badgeColor
* Add tests
* Revert changes to transform
* Edit badge colors
* remove universal object transform
* Update changelog filename
* UI: Add form inline warning (#24986)
* Add form inline warning
* Remove title
* Only show form warning for unauth
* Address feedback!
---
changelog/23945.txt | 3 +
ui/app/adapters/config-ui/message.js | 27 +++
ui/app/app.js | 5 +
ui/app/components/sidebar/nav/cluster.hbs | 9 +
ui/app/controllers/vault/cluster.js | 1 +
ui/app/controllers/vault/cluster/auth.js | 4 +
ui/app/models/config-ui/message.js | 125 ++++++++++
ui/app/router.js | 1 +
ui/app/routes/vault/cluster.js | 1 +
ui/app/routes/vault/cluster/logout.js | 2 +
ui/app/serializers/config-ui/message.js | 58 +++++
ui/app/services/custom-messages.js | 63 +++++
ui/app/services/permissions.js | 3 +
ui/app/styles/helper-classes/colors.scss | 5 +
ui/app/styles/helper-classes/layout.scss | 13 +
ui/app/styles/helper-classes/spacing.scss | 4 +
ui/app/templates/components/auth-form.hbs | 1 -
ui/app/templates/vault/cluster.hbs | 39 +++
ui/app/transforms/date-time-local.js | 34 +++
.../messages/message-expiration-date-form.hbs | 54 +++++
.../messages/message-expiration-date-form.js | 33 +++
.../page/create-and-edit-message-form.hbs | 117 +++++++++
.../page/create-and-edit-message-form.js | 95 ++++++++
.../components/messages/page/details.hbs | 60 +++++
.../addon/components/messages/page/details.js | 35 +++
.../addon/components/messages/page/list.hbs | 134 +++++++++++
.../addon/components/messages/page/list.js | 107 +++++++++
.../components/messages/preview-image.hbs | 40 +++
.../components/messages/tab-page-header.hbs | 58 +++++
.../addon/controllers/messages/create.js | 11 +
.../addon/controllers/messages/index.js | 13 +
ui/lib/config-ui/addon/engine.js | 23 ++
ui/lib/config-ui/addon/routes.js | 16 ++
.../config-ui/addon/routes/messages/create.js | 52 ++++
.../config-ui/addon/routes/messages/index.js | 55 +++++
.../addon/routes/messages/message/details.js | 26 ++
.../addon/routes/messages/message/edit.js | 37 +++
.../addon/templates/messages/create.hbs | 11 +
.../addon/templates/messages/index.hbs | 10 +
.../templates/messages/message/details.hbs | 6 +
.../addon/templates/messages/message/edit.hbs | 11 +
ui/lib/config-ui/config/environment.js | 15 ++
ui/lib/config-ui/index.js | 17 ++
ui/lib/config-ui/package.json | 16 ++
ui/lib/core/addon/components/form-field.hbs | 7 +-
.../addon/components/kv-object-editor.hbs | 32 +--
.../core/addon/components/kv-object-editor.js | 9 +-
ui/lib/core/addon/utils/date-formatters.js | 2 +
ui/mirage/handlers/custom-messages.js | 164 +++++++++++++
ui/mirage/handlers/index.js | 2 +
ui/package.json | 3 +-
.../images/custom-messages-dashboard.png | Bin 0 -> 13073 bytes
ui/public/images/custom-messages-login.png | Bin 0 -> 30879 bytes
.../acceptance/custom-messages-auth-test.js | 112 +++++++++
ui/tests/acceptance/dashboard-test.js | 100 ++++++++
ui/tests/acceptance/mfa-login-test.js | 2 +-
.../helpers/config-ui/message-selectors.js | 22 ++
.../page/create-and-edit-message-test.js | 227 ++++++++++++++++++
.../config-ui/messages/page/details-test.js | 92 +++++++
.../config-ui/messages/page/list-test.js | 144 +++++++++++
.../mfa-login-enforcement-form-test.js | 1 +
.../components/sidebar/nav/cluster-test.js | 3 +-
.../serializers/config-ui/message-test.js | 72 ++++++
.../unit/transforms/date-time-local-test.js | 30 +++
64 files changed, 2452 insertions(+), 22 deletions(-)
create mode 100644 changelog/23945.txt
create mode 100644 ui/app/adapters/config-ui/message.js
create mode 100644 ui/app/models/config-ui/message.js
create mode 100644 ui/app/serializers/config-ui/message.js
create mode 100644 ui/app/services/custom-messages.js
create mode 100644 ui/app/transforms/date-time-local.js
create mode 100644 ui/lib/config-ui/addon/components/messages/message-expiration-date-form.hbs
create mode 100644 ui/lib/config-ui/addon/components/messages/message-expiration-date-form.js
create mode 100644 ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs
create mode 100644 ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js
create mode 100644 ui/lib/config-ui/addon/components/messages/page/details.hbs
create mode 100644 ui/lib/config-ui/addon/components/messages/page/details.js
create mode 100644 ui/lib/config-ui/addon/components/messages/page/list.hbs
create mode 100644 ui/lib/config-ui/addon/components/messages/page/list.js
create mode 100644 ui/lib/config-ui/addon/components/messages/preview-image.hbs
create mode 100644 ui/lib/config-ui/addon/components/messages/tab-page-header.hbs
create mode 100644 ui/lib/config-ui/addon/controllers/messages/create.js
create mode 100644 ui/lib/config-ui/addon/controllers/messages/index.js
create mode 100644 ui/lib/config-ui/addon/engine.js
create mode 100644 ui/lib/config-ui/addon/routes.js
create mode 100644 ui/lib/config-ui/addon/routes/messages/create.js
create mode 100644 ui/lib/config-ui/addon/routes/messages/index.js
create mode 100644 ui/lib/config-ui/addon/routes/messages/message/details.js
create mode 100644 ui/lib/config-ui/addon/routes/messages/message/edit.js
create mode 100644 ui/lib/config-ui/addon/templates/messages/create.hbs
create mode 100644 ui/lib/config-ui/addon/templates/messages/index.hbs
create mode 100644 ui/lib/config-ui/addon/templates/messages/message/details.hbs
create mode 100644 ui/lib/config-ui/addon/templates/messages/message/edit.hbs
create mode 100644 ui/lib/config-ui/config/environment.js
create mode 100644 ui/lib/config-ui/index.js
create mode 100644 ui/lib/config-ui/package.json
create mode 100644 ui/mirage/handlers/custom-messages.js
create mode 100644 ui/public/images/custom-messages-dashboard.png
create mode 100644 ui/public/images/custom-messages-login.png
create mode 100644 ui/tests/acceptance/custom-messages-auth-test.js
create mode 100644 ui/tests/helpers/config-ui/message-selectors.js
create mode 100644 ui/tests/integration/components/config-ui/messages/page/create-and-edit-message-test.js
create mode 100644 ui/tests/integration/components/config-ui/messages/page/details-test.js
create mode 100644 ui/tests/integration/components/config-ui/messages/page/list-test.js
create mode 100644 ui/tests/unit/serializers/config-ui/message-test.js
create mode 100644 ui/tests/unit/transforms/date-time-local-test.js
diff --git a/changelog/23945.txt b/changelog/23945.txt
new file mode 100644
index 0000000000..6ebb782d66
--- /dev/null
+++ b/changelog/23945.txt
@@ -0,0 +1,3 @@
+```release-note:feature
+**Custom messages**: Introduces custom messages settings, allowing users to view, and operators to configure system-wide messages.
+```
diff --git a/ui/app/adapters/config-ui/message.js b/ui/app/adapters/config-ui/message.js
new file mode 100644
index 0000000000..6aa3afbed2
--- /dev/null
+++ b/ui/app/adapters/config-ui/message.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import ApplicationAdapter from '../application';
+
+export default class MessageAdapter extends ApplicationAdapter {
+ pathForType() {
+ return 'config/ui/custom-messages';
+ }
+
+ query(store, type, query) {
+ 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/app.js b/ui/app/app.js
index ca5ec0a052..5564cb86e9 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -13,6 +13,11 @@ export default class App extends Application {
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
engines = {
+ configUi: {
+ dependencies: {
+ services: ['auth', 'flash-messages', 'namespace', 'router', 'store', 'version', 'customMessages'],
+ },
+ },
openApiExplorer: {
dependencies: {
services: ['auth', 'flash-messages', 'namespace', 'router', 'version'],
diff --git a/ui/app/components/sidebar/nav/cluster.hbs b/ui/app/components/sidebar/nav/cluster.hbs
index 33dd4f2e58..9566215d52 100644
--- a/ui/app/components/sidebar/nav/cluster.hbs
+++ b/ui/app/components/sidebar/nav/cluster.hbs
@@ -105,4 +105,13 @@
data-test-sidebar-nav-link="Seal Vault"
/>
{{/if}}
+
+ {{#if (has-permission "settings")}}
+ Settings
+
+ {{/if}}
\ No newline at end of file
diff --git a/ui/app/controllers/vault/cluster.js b/ui/app/controllers/vault/cluster.js
index 5392131345..99df90ec4b 100644
--- a/ui/app/controllers/vault/cluster.js
+++ b/ui/app/controllers/vault/cluster.js
@@ -16,6 +16,7 @@ export default Controller.extend({
permissions: service(),
namespaceService: service('namespace'),
flashMessages: service(),
+ customMessages: service(),
vaultVersion: service('version'),
console: service(),
diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js
index dd35d2b63e..1c270fc316 100644
--- a/ui/app/controllers/vault/cluster/auth.js
+++ b/ui/app/controllers/vault/cluster/auth.js
@@ -17,6 +17,7 @@ export default Controller.extend({
version: service(),
auth: service(),
router: service(),
+ customMessages: service(),
queryParams: [{ authMethod: 'with', oidcProvider: 'o' }],
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
wrappedToken: alias('vaultController.wrappedToken'),
@@ -52,6 +53,7 @@ export default Controller.extend({
yield timeout(500);
const ns = this.fullNamespaceFromInput(value);
this.namespaceService.setNamespace(ns, true);
+ this.customMessages.fetchMessages(ns);
this.set('namespaceQueryParam', ns);
}).restartable(),
@@ -67,6 +69,8 @@ export default Controller.extend({
transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
}
transition.followRedirects().then(() => {
+ this.customMessages.fetchMessages(namespace);
+
if (isRoot) {
this.auth.set('isRootToken', true);
this.flashMessages.warning(
diff --git a/ui/app/models/config-ui/message.js b/ui/app/models/config-ui/message.js
new file mode 100644
index 0000000000..56a95cf40e
--- /dev/null
+++ b/ui/app/models/config-ui/message.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+import Model, { attr } from '@ember-data/model';
+import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
+import { isAfter, addDays, startOfDay, parseISO } from 'date-fns';
+import { withModelValidations } from 'vault/decorators/model-validations';
+import { withFormFields } from 'vault/decorators/model-form-fields';
+
+const validations = {
+ title: [{ type: 'presence', message: 'Title is required.' }],
+ message: [{ type: 'presence', message: 'Message is required.' }],
+ link: [
+ {
+ validator(model) {
+ if (!model?.link) return true;
+ const [title] = Object.keys(model.link);
+ const [href] = Object.values(model.link);
+ return title || href ? !!(title && href) : true;
+ },
+ message: 'Link title and url are required.',
+ },
+ ],
+};
+
+@withModelValidations(validations)
+@withFormFields(['authenticated', 'type', 'title', 'message', 'link', 'startTime', 'endTime'])
+export default class MessageModel extends Model {
+ @attr('boolean') active;
+ @attr('string', {
+ label: 'Type',
+ editType: 'radio',
+ possibleValues: [
+ {
+ label: 'Alert message',
+ subText:
+ 'A banner that appears on the top of every page to display brief but high-signal messages like an update or system alert.',
+ value: 'banner',
+ },
+ {
+ label: 'Modal',
+ subText: 'A pop-up window used to bring immediate attention for important notifications or actions.',
+ value: 'modal',
+ },
+ ],
+ defaultValue: 'banner',
+ })
+ type;
+ // The authenticated attr is a boolean. The authenticatedString getter and setter is used only in forms to get and set the boolean via
+ // strings values. The server and query params expects the attr to be boolean values.
+ @attr({
+ label: 'Where should we display this message?',
+ editType: 'radio',
+ fieldValue: 'authenticatedString',
+ possibleValues: [
+ {
+ label: 'After the user logs in',
+ subText: 'Display to users after they have successfully logged in to Vault.',
+ value: 'authenticated',
+ },
+ {
+ label: 'On the login page',
+ subText: 'Display to users on the login page before they have authenticated.',
+ value: 'unauthenticated',
+ },
+ ],
+ defaultValue: true,
+ })
+ authenticated;
+
+ get authenticatedString() {
+ return this.authenticated ? 'authenticated' : 'unauthenticated';
+ }
+
+ set authenticatedString(value) {
+ this.authenticated = value === 'authenticated' ? true : false;
+ }
+
+ @attr('string')
+ title;
+ @attr('string', {
+ editType: 'textarea',
+ })
+ message;
+ @attr('dateTimeLocal', {
+ editType: 'dateTimeLocal',
+ label: 'Message starts',
+ subText: 'Defaults to 12:00 a.m. the following day (local timezone).',
+ defaultValue: addDays(startOfDay(new Date()), 1).toISOString(),
+ })
+ startTime;
+ @attr('dateTimeLocal', { editType: 'yield', label: 'Message expires' }) endTime;
+
+ @attr('object', {
+ editType: 'kv',
+ keyPlaceholder: 'Display text (e.g. Learn more)',
+ valuePlaceholder: 'Link URL (e.g. https://www.learnmore.com)',
+ label: 'Link (optional)',
+ isSingleRow: true,
+ allowWhiteSpace: true,
+ })
+ link;
+
+ // date helpers
+ get isStartTimeAfterToday() {
+ return isAfter(parseISO(this.startTime), new Date());
+ }
+
+ // capabilities
+ @lazyCapabilities(apiPath`sys/config/ui/custom-messages`) customMessagesPath;
+
+ get canCreateCustomMessages() {
+ return this.customMessagesPath.get('canCreate') !== false;
+ }
+ get canReadCustomMessages() {
+ return this.customMessagesPath.get('canRead') !== false;
+ }
+ get canEditCustomMessages() {
+ return this.customMessagesPath.get('canUpdate') !== false;
+ }
+ get canDeleteCustomMessages() {
+ return this.customMessagesPath.get('canDelete') !== false;
+ }
+}
diff --git a/ui/app/router.js b/ui/app/router.js
index 51396f4870..4bfdf02cb9 100644
--- a/ui/app/router.js
+++ b/ui/app/router.js
@@ -15,6 +15,7 @@ Router.map(function () {
this.route('vault', { path: '/' }, function () {
this.route('cluster', { path: '/:cluster_name' }, function () {
this.route('dashboard');
+ this.mount('config-ui');
this.mount('sync');
this.route('oidc-provider-ns', { path: '/*namespace/identity/oidc/provider/:provider_name/authorize' });
this.route('oidc-provider', { path: '/identity/oidc/provider/:provider_name/authorize' });
diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js
index 7d55d33b90..999281c901 100644
--- a/ui/app/routes/vault/cluster.js
+++ b/ui/app/routes/vault/cluster.js
@@ -33,6 +33,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
permissions: service(),
store: service(),
auth: service(),
+ customMessages: service(),
featureFlagService: service('featureFlag'),
currentCluster: service(),
modelTypes: computed(function () {
diff --git a/ui/app/routes/vault/cluster/logout.js b/ui/app/routes/vault/cluster/logout.js
index 8fa567ada4..3a7a4b29ab 100644
--- a/ui/app/routes/vault/cluster/logout.js
+++ b/ui/app/routes/vault/cluster/logout.js
@@ -18,6 +18,7 @@ export default Route.extend(ModelBoundaryRoute, {
namespaceService: service('namespace'),
router: service(),
version: service(),
+ customMessages: service(),
modelTypes: computed(function () {
return ['secret', 'secret-engine'];
@@ -34,6 +35,7 @@ export default Route.extend(ModelBoundaryRoute, {
this.flashMessages.clearMessages();
this.permissions.reset();
this.version.version = null;
+ this.customMessages.clearCustomMessages();
queryParams.with = authType;
if (ns) {
diff --git a/ui/app/serializers/config-ui/message.js b/ui/app/serializers/config-ui/message.js
new file mode 100644
index 0000000000..2333eb2475
--- /dev/null
+++ b/ui/app/serializers/config-ui/message.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { decodeString, encodeString } from 'core/utils/b64';
+import ApplicationSerializer from '../application';
+
+export default class MessageSerializer extends ApplicationSerializer {
+ attrs = {
+ active: { serialize: false },
+ start_time: { serialize: false },
+ end_time: { serialize: false },
+ };
+
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ if (requestType === 'query' && !payload.meta) {
+ const transformed = this.mapPayload(payload);
+ return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType);
+ }
+ if (requestType === 'queryRecord') {
+ const transformed = {
+ ...payload.data,
+ message: decodeString(payload.data.message),
+ };
+ return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType);
+ }
+ return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
+ }
+
+ serialize() {
+ const json = super.serialize(...arguments);
+ json.message = encodeString(json.message);
+ return json;
+ }
+
+ mapPayload(payload) {
+ if (payload.data) {
+ if (payload.data?.keys && Array.isArray(payload.data.keys)) {
+ return payload.data.keys.map((key) => {
+ const data = {
+ id: key,
+ ...payload.data.key_info[key],
+ };
+ if (data.message) data.message = decodeString(data.message);
+ return data;
+ });
+ }
+ Object.assign(payload, payload.data);
+ delete payload.data;
+ }
+ return payload;
+ }
+
+ extractLazyPaginatedData(payload) {
+ return this.mapPayload(payload);
+ }
+}
diff --git a/ui/app/services/custom-messages.js b/ui/app/services/custom-messages.js
new file mode 100644
index 0000000000..ab7ea565ab
--- /dev/null
+++ b/ui/app/services/custom-messages.js
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { action } from '@ember/object';
+import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { TrackedObject } from 'tracked-built-ins';
+export default class CustomMessagesService extends Service {
+ @service store;
+ @service namespace;
+ @service auth;
+ @tracked messages = [];
+ @tracked showMessageModal = true;
+ bannerState = new TrackedObject();
+
+ constructor() {
+ super(...arguments);
+ this.fetchMessages(this.namespace.path);
+ }
+
+ get bannerMessages() {
+ if (!this.messages || !this.messages.length) return [];
+ return this.messages?.filter((message) => message?.type === 'banner');
+ }
+
+ get modalMessages() {
+ if (!this.messages || !this.messages.length) return [];
+ return this.messages?.filter((message) => message?.type === 'modal');
+ }
+
+ async fetchMessages(ns) {
+ try {
+ const url = this.auth.currentToken
+ ? '/v1/sys/internal/ui/authenticated-messages'
+ : '/v1/sys/internal/ui/unauthenticated-messages';
+ const opts = {
+ method: 'GET',
+ headers: {},
+ };
+ if (this.auth.currentToken) opts.headers['X-Vault-Token'] = this.auth.currentToken;
+ if (ns) opts.headers['X-Vault-Namespace'] = ns;
+ const result = await fetch(url, opts);
+ const body = await result.json();
+ if (body.errors) return (this.messages = []);
+ const serializer = this.store.serializerFor('config-ui/message');
+ this.messages = serializer.mapPayload(body);
+ this.bannerMessages?.forEach((bm) => (this.bannerState[bm.id] = true));
+ } catch (e) {
+ return e;
+ }
+ }
+
+ clearCustomMessages() {
+ this.messages = [];
+ }
+
+ @action
+ onBannerDismiss(id) {
+ this.bannerState[id] = false;
+ }
+}
diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js
index 72240f5238..feab78caaf 100644
--- a/ui/app/services/permissions.js
+++ b/ui/app/services/permissions.js
@@ -41,6 +41,9 @@ const API_PATHS = {
activity: 'sys/internal/counters/activity',
config: 'sys/internal/counters/config',
},
+ settings: {
+ customMessages: 'sys/config/ui/custom-messages',
+ },
};
const API_PATHS_TO_ROUTE_PARAMS = {
diff --git a/ui/app/styles/helper-classes/colors.scss b/ui/app/styles/helper-classes/colors.scss
index d505ef8b56..a3a6b7c390 100644
--- a/ui/app/styles/helper-classes/colors.scss
+++ b/ui/app/styles/helper-classes/colors.scss
@@ -49,6 +49,11 @@ select.has-error-border,
border: 1px solid $red-500;
}
+.error-border-child-inputs input,
+.error-border-child-inputs textarea {
+ border: 1px solid $red-500;
+}
+
// specifically for the SearchSelect dropdown.
.dropdown-has-error-border > div.ember-basic-dropdown-trigger {
border: 1px solid $red-500;
diff --git a/ui/app/styles/helper-classes/layout.scss b/ui/app/styles/helper-classes/layout.scss
index 83f778ef3c..60c85718f6 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/app/styles/helper-classes/spacing.scss b/ui/app/styles/helper-classes/spacing.scss
index a208705622..8d95939e53 100644
--- a/ui/app/styles/helper-classes/spacing.scss
+++ b/ui/app/styles/helper-classes/spacing.scss
@@ -98,6 +98,10 @@
margin: $spacing-4 0;
}
+.has-top-margin-negative-m {
+ margin-top: -$spacing-16;
+}
+
.has-top-bottom-margin-negative-m {
margin-top: -$spacing-16;
margin-bottom: -$spacing-16;
diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs
index 01ea4f760f..d7c1c35e7c 100644
--- a/ui/app/templates/components/auth-form.hbs
+++ b/ui/app/templates/components/auth-form.hbs
@@ -2,7 +2,6 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
-
{{#if (and this.waitingForOktaNumberChallenge (not this.cancelAuthForOktaNumberChallenge))}}
+{{#each this.customMessages.bannerMessages as |bannerMessage|}}
+ {{#if (get this.customMessages.bannerState bannerMessage.id)}}
+
+ {{bannerMessage.title}}
+
+ {{bannerMessage.message}}
+ {{#unless (is-empty-value bannerMessage.link)}}
+ {{#each-in bannerMessage.link as |title href|}}
+ {{title}}
+ {{/each-in}}
+ {{/unless}}
+
+
+ {{/if}}
+{{/each}}
+{{#each this.customMessages.modalMessages as |modalMessage|}}
+
+
+ {{modalMessage.title}}
+
+
+ {{modalMessage.message}}
+ {{#unless (is-empty-value modalMessage.link)}}
+ {{#each-in modalMessage.link as |title href|}}
+ {{title}}
+ {{/each-in}}
+ {{/unless}}
+
+
+
+
+
+{{/each}}
{{#if this.activeCluster.version.isEnterprise}}
+
+
+
+ Never
+
+
+ This message will never expire unless manually deleted by an operator.
+
+
+
+
+
+
+
+
+ Specific date
+
+
+ This message will expire at midnight (local timezone) at the specific date.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/components/messages/message-expiration-date-form.js b/ui/lib/config-ui/addon/components/messages/message-expiration-date-form.js
new file mode 100644
index 0000000000..daaff91305
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/message-expiration-date-form.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { datetimeLocalStringFormat } from 'core/utils/date-formatters';
+
+/**
+ * @module Messages::MessageExpirationDateForm
+ * Messages::MessageExpirationDateForm components are used to display list of messages.
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {array} messages - array message objects
+ */
+
+export default class MessageExpirationDateForm extends Component {
+ datetimeLocalStringFormat = datetimeLocalStringFormat;
+ @tracked groupValue = 'never';
+ @tracked formDateTime = '';
+
+ constructor() {
+ super(...arguments);
+
+ if (this.args.message.endTime) {
+ this.groupValue = 'specificDate';
+ this.formDateTime = this.args.message.endTime;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..c687f1822f
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.hbs
@@ -0,0 +1,117 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+
+
+{{#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
new file mode 100644
index 0000000000..d6fbb4df99
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/create-and-edit-message-form.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { task, timeout } from 'ember-concurrency';
+import errorMessage from 'vault/utils/error-message';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import Ember from 'ember';
+
+/**
+ * @module Page::CreateAndEditMessageForm
+ * Page::CreateAndEditMessageForm components are used to display create and edit message form fields.
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {model} message - message model to pass to form components
+ */
+
+export default class MessagesList extends Component {
+ @service router;
+ @service store;
+ @service flashMessages;
+ @service customMessages;
+ @service namespace;
+
+ @tracked errorBanner = '';
+ @tracked modelValidations;
+ @tracked invalidFormMessage;
+ @tracked showMessagePreviewModal = false;
+ @tracked showMultipleModalsMessage = false;
+ @tracked userConfirmation = '';
+
+ willDestroy() {
+ super.willDestroy();
+ const noTeardown = this.store && !this.store.isDestroying;
+ const { model } = this;
+ if (noTeardown && model && model.get('isDirty') && !model.isDestroyed && !model.isDestroying) {
+ model.rollbackAttributes();
+ }
+ }
+
+ @task
+ *save(event) {
+ event.preventDefault();
+ try {
+ this.userConfirmation = '';
+
+ const { isValid, state, invalidFormMessage } = this.args.message.validate();
+ this.modelValidations = isValid ? null : state;
+ this.invalidFormAlert = invalidFormMessage;
+
+ if (this.args.hasSomeActiveModals && this.args.message.type === 'modal') {
+ this.showMultipleModalsMessage = true;
+ const isConfirmed = yield this.getUserConfirmation.perform();
+ if (!isConfirmed) return;
+ }
+
+ if (isValid) {
+ const { isNew } = this.args.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.customMessages.fetchMessages(this.namespace.path);
+ this.router.transitionTo('vault.cluster.config-ui.messages.message.details', id);
+ }
+ } catch (error) {
+ this.errorBanner = errorMessage(error);
+ this.invalidFormAlert = 'There was an error submitting this form.';
+ }
+ }
+
+ @task
+ *getUserConfirmation() {
+ while (true) {
+ if (Ember.testing) {
+ return;
+ }
+ if (this.userConfirmation) {
+ return this.userConfirmation === 'confirmed';
+ }
+ yield timeout(500);
+ }
+ }
+
+ @action
+ updateUserConfirmation(userConfirmation) {
+ this.userConfirmation = userConfirmation;
+ this.showMultipleModalsMessage = false;
+ }
+}
diff --git a/ui/lib/config-ui/addon/components/messages/page/details.hbs b/ui/lib/config-ui/addon/components/messages/page/details.hbs
new file mode 100644
index 0000000000..4c1e675cca
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/details.hbs
@@ -0,0 +1,60 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+
+
+
+ {{#if @message.canDeleteCustomMessages}}
+
+
+ {{/if}}
+ {{#if @message.canEditCustomMessages}}
+
+ Edit message
+
+
+ {{/if}}
+
+
+
+{{#each @message.allFields as |attr|}}
+ {{#if (or (eq attr.name "endTime") (eq attr.name "startTime"))}}
+ {{! if the attr is an endTime and is falsy, we want to show a 'Never' text value }}
+
+ {{else if (eq attr.name "link")}}
+ {{#if (is-empty-value @message.link)}}
+
+ {{else}}
+ {{#each-in @message.link as |title href|}}
+
+ {{title}}
+
+ {{/each-in}}
+ {{/if}}
+ {{else}}
+
+ {{/if}}
+
+{{/each}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/components/messages/page/details.js b/ui/lib/config-ui/addon/components/messages/page/details.js
new file mode 100644
index 0000000000..56e5ad73cc
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/details.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+/**
+ * @module Page::MessageDetails
+ * Page::MessageDetails components are used to display a message
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {model} message - message model
+ */
+
+export default class MessageDetails extends Component {
+ @service store;
+ @service router;
+ @service flashMessages;
+ @service customMessages;
+ @service namespace;
+
+ @action
+ async deleteMessage() {
+ this.store.clearDataset('config-ui/message');
+ await this.args.message.destroyRecord(this.args.message.id);
+ this.router.transitionTo('vault.cluster.config-ui.messages');
+ this.customMessages.fetchMessages(this.namespace.path);
+ this.flashMessages.success(`Successfully deleted ${this.args.message.title}.`);
+ }
+}
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 0000000000..c1819373bb
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/list.hbs
@@ -0,0 +1,134 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+ <:toolbarFilters>
+ {{#if @messages.meta.total}}
+
+ {{/if}}
+
+ <:toolbarActions>
+
+
+
+
+{{#if @messages.length}}
+ {{#each this.formattedMessages as |message|}}
+
+
+
+
+
+
+ {{message.title}}
+
+
+
+
+
+
+
+
+
+
+ {{/each}}
+
+{{else}}
+
+
+
+{{/if}}
+
+{{#if this.showMaxMessageModal}}
+
+
+ Maximum number of messages reached
+
+
+ Vault can only store up to 100 messages. To create a message, delete one of your messages to clear up space.
+
+
+
+
+
+{{/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 0000000000..7325277f37
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/page/list.js
@@ -0,0 +1,107 @@
+/**
+ * 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';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+
+/**
+ * @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;
+ @service router;
+ @service flashMessages;
+ @service namespace;
+ @service customMessages;
+
+ @tracked showMaxMessageModal = false;
+
+ get formattedMessages() {
+ return this.args.messages.map((message) => {
+ let badgeDisplayText = '';
+ let badgeColor = 'neutral';
+
+ if (message.active) {
+ if (message.endTime) {
+ badgeDisplayText = `Active until ${dateFormat([message.endTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ } else {
+ badgeDisplayText = 'Active';
+ }
+ badgeColor = 'success';
+ } else {
+ if (message.isStartTimeAfterToday) {
+ badgeDisplayText = `Scheduled: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ badgeColor = 'highlight';
+ } else {
+ badgeDisplayText = `Inactive: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
+ withTimeZone: true,
+ })}`;
+ badgeColor = 'neutral';
+ }
+ }
+
+ message.badgeDisplayText = badgeDisplayText;
+ message.badgeColor = badgeColor;
+ return message;
+ });
+ }
+
+ get breadcrumbs() {
+ const label = this.args.authenticated ? 'After User Logs In' : 'On Login Page';
+ return [{ label: 'Messages' }, { label }];
+ }
+
+ // 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);
+ this.router.transitionTo('vault.cluster.config-ui.messages');
+ this.customMessages.fetchMessages(this.namespace.path);
+ this.flashMessages.success(`Successfully deleted ${message.title}.`);
+ }
+
+ @action
+ onFilterChange(pageFilter) {
+ this.router.transitionTo('vault.cluster.config-ui.messages', {
+ queryParams: { pageFilter },
+ });
+ }
+
+ @action
+ createMessage() {
+ if (this.args.messages?.meta.total >= 100) {
+ this.showMaxMessageModal = true;
+ return;
+ }
+
+ this.router.transitionTo('vault.cluster.config-ui.messages.create', {
+ queryParams: { authenticated: this.args.authenticated },
+ });
+ }
+}
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 0000000000..76963b413d
--- /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}}
+
+
+
+
+
+
+
+
\ No newline at end of file
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 0000000000..256ecb7299
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/messages/tab-page-header.hbs
@@ -0,0 +1,58 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
+ {{#if @breadcrumbs}}
+
+
+
+ {{/if}}
+
+
+ {{@pageTitle}}
+
+
+
+
+{{#if @showTabs}}
+
+
+
+
+ {{! Explicitly setting page to 1 here since we want to reset the page param on transition}}
+
+ After user logs in
+
+
+
+
+ On login page
+
+
+
+
+
+
+ {{#if (or (has-block "toolbarFilters") (has-block "toolbarActions"))}}
+
+
+ {{yield to="toolbarFilters"}}
+
+
+ {{yield to="toolbarActions"}}
+
+
+ {{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/controllers/messages/create.js b/ui/lib/config-ui/addon/controllers/messages/create.js
new file mode 100644
index 0000000000..9c75000ab6
--- /dev/null
+++ b/ui/lib/config-ui/addon/controllers/messages/create.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Controller from '@ember/controller';
+export default class MessagesController extends Controller {
+ queryParams = ['authenticated'];
+
+ authenticated = true;
+}
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 0000000000..06bb5661ed
--- /dev/null
+++ b/ui/lib/config-ui/addon/controllers/messages/index.js
@@ -0,0 +1,13 @@
+/**
+ * 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;
+ pageFilter = '';
+}
diff --git a/ui/lib/config-ui/addon/engine.js b/ui/lib/config-ui/addon/engine.js
new file mode 100644
index 0000000000..2eedbf9a9b
--- /dev/null
+++ b/ui/lib/config-ui/addon/engine.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Engine from '@ember/engine';
+
+import loadInitializers from 'ember-load-initializers';
+import Resolver from 'ember-resolver';
+
+import config from './config/environment';
+
+const { modulePrefix } = config;
+
+export default class ConfigUiEngine extends Engine {
+ modulePrefix = modulePrefix;
+ Resolver = Resolver;
+ dependencies = {
+ services: ['auth', 'store', 'flash-messages', 'namespace', 'router', 'version', 'customMessages'],
+ };
+}
+
+loadInitializers(ConfigUiEngine, modulePrefix);
diff --git a/ui/lib/config-ui/addon/routes.js b/ui/lib/config-ui/addon/routes.js
new file mode 100644
index 0000000000..14b397a940
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes.js
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import buildRoutes from 'ember-engines/routes';
+
+export default buildRoutes(function () {
+ this.route('messages', function () {
+ this.route('create');
+ this.route('message', { path: '/:id' }, function () {
+ this.route('details');
+ this.route('edit');
+ });
+ });
+});
diff --git a/ui/lib/config-ui/addon/routes/messages/create.js b/ui/lib/config-ui/addon/routes/messages/create.js
new file mode 100644
index 0000000000..608aa2938b
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/messages/create.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+export default class MessagesCreateRoute extends Route {
+ @service store;
+
+ queryParams = {
+ authenticated: {
+ refreshModel: true,
+ },
+ };
+
+ async getMessages(authenticated) {
+ try {
+ return await this.store.query('config-ui/message', {
+ authenticated,
+ });
+ } catch {
+ return [];
+ }
+ }
+
+ async model(params) {
+ const { authenticated } = params;
+ const message = this.store.createRecord('config-ui/message', {
+ authenticated,
+ });
+ const messages = await this.getMessages(authenticated);
+
+ return {
+ message,
+ messages,
+ authenticated,
+ hasSomeActiveModals:
+ messages.length && messages?.some((message) => message.type === 'modal' && message.active),
+ };
+ }
+
+ 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/index.js b/ui/lib/config-ui/addon/routes/messages/index.js
new file mode 100644
index 0000000000..3d468818e8
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/messages/index.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { hash } from 'rsvp';
+
+export default class MessagesRoute extends Route {
+ @service store;
+
+ queryParams = {
+ page: {
+ refreshModel: true,
+ },
+ authenticated: {
+ refreshModel: true,
+ },
+ pageFilter: {
+ refreshModel: true,
+ },
+ };
+
+ model(params) {
+ const { authenticated, page, pageFilter } = params;
+ const filter = pageFilter
+ ? (dataset) => dataset.filter((item) => item?.title.toLowerCase().includes(pageFilter.toLowerCase()))
+ : null;
+ const messages = this.store
+ .lazyPaginatedQuery('config-ui/message', {
+ authenticated,
+ pageFilter: filter,
+ responsePath: 'data.keys',
+ page: page || 1,
+ size: 10,
+ })
+ .catch((e) => {
+ if (e.httpStatus === 404) {
+ return [];
+ }
+ throw e;
+ });
+ return hash({
+ pageFilter,
+ messages,
+ });
+ }
+
+ setupController(controller, resolvedModel) {
+ super.setupController(controller, resolvedModel);
+ const label = controller.authenticated ? 'After User Logs In' : 'On Login Page';
+ controller.breadcrumbs = [{ label: 'Messages' }, { label }];
+ }
+}
diff --git a/ui/lib/config-ui/addon/routes/messages/message/details.js b/ui/lib/config-ui/addon/routes/messages/message/details.js
new file mode 100644
index 0000000000..1889e5e2a6
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/messages/message/details.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class MessagesMessageDetailsRoute 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: resolvedModel.title },
+ ];
+ }
+}
diff --git a/ui/lib/config-ui/addon/routes/messages/message/edit.js b/ui/lib/config-ui/addon/routes/messages/message/edit.js
new file mode 100644
index 0000000000..0666c187fa
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/messages/message/edit.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { hash } from 'rsvp';
+
+export default class MessagesMessageEditRoute extends Route {
+ @service store;
+
+ getMessages(authenticated = true) {
+ return this.store.query('config-ui/message', { authenticated }).catch(() => []);
+ }
+
+ async model() {
+ const { id } = this.paramsFor('messages.message');
+ const message = await this.store.queryRecord('config-ui/message', id);
+ const messages = await this.getMessages(message.authenticated);
+ return hash({
+ message,
+ messages,
+ hasSomeActiveModals:
+ messages.length && messages?.some((message) => message.type === 'modal' && message.active),
+ });
+ }
+
+ setupController(controller, resolvedModel) {
+ super.setupController(controller, resolvedModel);
+
+ controller.breadcrumbs = [
+ { label: 'Messages', route: 'messages', query: { authenticated: resolvedModel.message.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
new file mode 100644
index 0000000000..3008f04505
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/messages/create.hbs
@@ -0,0 +1,11 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/templates/messages/index.hbs b/ui/lib/config-ui/addon/templates/messages/index.hbs
new file mode 100644
index 0000000000..d28fe8c862
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/messages/index.hbs
@@ -0,0 +1,10 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/templates/messages/message/details.hbs b/ui/lib/config-ui/addon/templates/messages/message/details.hbs
new file mode 100644
index 0000000000..ec68bb6440
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/messages/message/details.hbs
@@ -0,0 +1,6 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
\ 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
new file mode 100644
index 0000000000..3008f04505
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/messages/message/edit.hbs
@@ -0,0 +1,11 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+~}}
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/config/environment.js b/ui/lib/config-ui/config/environment.js
new file mode 100644
index 0000000000..c91dc65d0e
--- /dev/null
+++ b/ui/lib/config-ui/config/environment.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+'use strict';
+
+module.exports = function (environment) {
+ const ENV = {
+ modulePrefix: 'config-ui',
+ environment: environment,
+ };
+
+ return ENV;
+};
diff --git a/ui/lib/config-ui/index.js b/ui/lib/config-ui/index.js
new file mode 100644
index 0000000000..3b5d11283e
--- /dev/null
+++ b/ui/lib/config-ui/index.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+/* eslint-disable n/no-extraneous-require */
+const { buildEngine } = require('ember-engines/lib/engine-addon');
+
+module.exports = buildEngine({
+ name: 'config-ui',
+ lazyLoading: {
+ enabled: false,
+ },
+ isDevelopingAddon() {
+ return true;
+ },
+});
diff --git a/ui/lib/config-ui/package.json b/ui/lib/config-ui/package.json
new file mode 100644
index 0000000000..90ffd202b8
--- /dev/null
+++ b/ui/lib/config-ui/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "config-ui",
+ "keywords": [
+ "ember-addon",
+ "ember-engine"
+ ],
+ "dependencies": {
+ "ember-cli-htmlbars": "*",
+ "ember-cli-babel": "*"
+ },
+ "ember-addon": {
+ "paths": [
+ "../core"
+ ]
+ }
+}
diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs
index 4f0fa8dbbf..6ee6f15efc 100644
--- a/ui/lib/core/addon/components/form-field.hbs
+++ b/ui/lib/core/addon/components/form-field.hbs
@@ -137,8 +137,11 @@
@helpText={{@attr.options.helpText}}
@subText={{@attr.options.subText}}
@onKeyUp={{this.handleKeyUp}}
- @validationError={{this.validationError}}
- class={{if @attr.options.isSectionHeader "form-section"}}
+ @keyPlaceholder={{@attr.options.keyPlaceholder}}
+ @valuePlaceholder={{@attr.options.valuePlaceholder}}
+ @isSingleRow={{@attr.options.isSingleRow}}
+ @allowWhiteSpace={{@attr.options.allowWhiteSpace}}
+ class="{{if this.validationError 'error-border-child-inputs'}} {{if @attr.options.isSectionHeader 'form-section'}}"
/>
{{else if (eq @attr.options.editType "file")}}
{{! File Input }}
diff --git a/ui/lib/core/addon/components/kv-object-editor.hbs b/ui/lib/core/addon/components/kv-object-editor.hbs
index eac26ede34..5951c5b704 100644
--- a/ui/lib/core/addon/components/kv-object-editor.hbs
+++ b/ui/lib/core/addon/components/kv-object-editor.hbs
@@ -53,21 +53,23 @@
/>
{{/if}}
-