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}} + + +
+ +
+ + +
\ 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 @message.isNew "Create" "Edit"}} + a custom message for all users when they access a Vault system via the UI. + + + + + {{#each @message.formFields as |attr|}} + + + + {{#if (and (eq attr.name "message") (not @message.authenticated))}} + + Note: Do not include sensitive info in this message since users are unauthenticated at this stage. + + {{/if}} + {{/each}} + + + + + + + + +
+ {{#if this.showMultipleModalsMessage}} + + + Warning: more than one modal + {{if @message.authenticated "after the user logs in" "on the login page"}} + + + You have an active modal configured + {{if @message.authenticated "after the user logs in" "on the login page"}} + and are trying to create another one. It is recommended to + avoid having more than one modal + at once as it can be intrusive for users. Would you like to continue creating your message? Click “Confirm” to + continue. + + + + + + + {{/if}} +
+ +{{#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}} + + + {{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}} +
+ +
+ + {{#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}}
-
- {{#if (eq this.kvData.length (inc index))}} - - {{else}} - - {{/if}} -
+ {{#unless @isSingleRow}} +
+ {{#if (eq this.kvData.length (inc index))}} + + {{else}} + + {{/if}} +
+ {{/unless}} {{#if (includes index this.whitespaceWarningRows)}}
diff --git a/ui/lib/core/addon/components/kv-object-editor.js b/ui/lib/core/addon/components/kv-object-editor.js index 40d0f3974f..98b13599bd 100644 --- a/ui/lib/core/addon/components/kv-object-editor.js +++ b/ui/lib/core/addon/components/kv-object-editor.js @@ -27,6 +27,7 @@ import KVObject from 'vault/lib/kv-object'; * @param {string} value - the value is captured from the model. * @param {function} onChange - function that captures the value on change * @param {boolean} [isMasked = false] - when true the renders instead of the default