Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: [VAULT-21532] Create message #24407

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
85c6097
WIP create message
kiannaquach Dec 5, 2023
56c6846
Add breadcrumns
kiannaquach Dec 5, 2023
f358607
Create and edit form
kiannaquach Dec 6, 2023
d01b98a
Add save to create/edit form
kiannaquach Dec 6, 2023
d520b65
Add cancel and todo
kiannaquach Dec 6, 2023
bd225e2
Fix cancel route
kiannaquach Dec 6, 2023
4512457
Fix breadcrumb label to be title case
kiannaquach Dec 6, 2023
27ea7de
Merge branch 'ui/VAULT-19096/customizable-banners' into ui/VAULT-2153…
kiannaquach Dec 6, 2023
6817a4e
add start time logic
kiannaquach Dec 6, 2023
b449605
Update breadcrumb
kiannaquach Dec 6, 2023
f9fd9d3
Fix breadcrumbs and merge conflict test
kiannaquach Dec 6, 2023
bb18706
Update create form description
kiannaquach Dec 6, 2023
88ce18b
Fix sidenav so it always highlights
kiannaquach Dec 6, 2023
7c0521d
Fix up forms
kiannaquach Dec 7, 2023
8030a92
Mostly working create form
kiannaquach Dec 7, 2023
e4aa33d
Form cleanup
kiannaquach Dec 7, 2023
4798cac
Fix link title and href form fields
kiannaquach Dec 8, 2023
ef59ae6
Default startTime
kiannaquach Dec 8, 2023
4214bb2
Fix messages
kiannaquach Dec 8, 2023
7b63062
Update dropdown to use the updated ConfirmAction component
kiannaquach Dec 8, 2023
0660e86
Update create and edit form
kiannaquach Dec 11, 2023
5e6ac6e
Add wip tests
kiannaquach Dec 11, 2023
8bf7609
Fix breadcrumb formatter
kiannaquach Dec 11, 2023
c1935bf
Comment out test
kiannaquach Dec 11, 2023
b6edf77
Update create message test
kiannaquach Dec 11, 2023
63475dc
Update more tests
kiannaquach Dec 11, 2023
3433239
Add comment for fixing date on edit
kiannaquach Dec 11, 2023
24c03c8
Update Message form
kiannaquach Dec 11, 2023
58d7c38
Code cleanup!
kiannaquach Dec 11, 2023
cb743f7
Add validation tests
kiannaquach Dec 12, 2023
3938ca1
Remove authenticated from route model
kiannaquach Dec 12, 2023
fc73f5f
SOme more code cleanup
kiannaquach Dec 12, 2023
1ceb1f2
Add controller so authenticated is parsed
kiannaquach Dec 12, 2023
e3a82d5
Merge branch 'ui/VAULT-19096/customizable-banners' into ui/VAULT-2153…
kiannaquach Dec 12, 2023
2beeefc
Working radio buttons
kiannaquach Dec 12, 2023
62d4f40
Use an object instead of arrays
kiannaquach Dec 13, 2023
49d8caa
Wip date form
kiannaquach Dec 13, 2023
24b5aa1
Fix license headers
kiannaquach Dec 13, 2023
6240747
Fix license headers addition of files
kiannaquach Dec 13, 2023
7e110b3
Fix copyright format issues and clean up code
kiannaquach Dec 13, 2023
87d47f8
Fix tests
kiannaquach Dec 13, 2023
3c6eddc
Rename FormField radio getter and ay11 improvements
kiannaquach Dec 13, 2023
6303dbe
Address feedback
kiannaquach Dec 13, 2023
29c0193
Fix specific date so it remembers the values
kiannaquach Dec 13, 2023
e56a137
Address feedback!
kiannaquach Dec 14, 2023
b5d28e6
Update more form fields
kiannaquach Dec 14, 2023
cdf8f74
Use formfield action instead
kiannaquach Dec 14, 2023
0856e8e
Update to every
kiannaquach Dec 14, 2023
9116b97
Update syntax of onchange
kiannaquach Dec 14, 2023
7d50f5b
Fix tests
kiannaquach Dec 14, 2023
2faf60e
Update willDestroy so it doesnt break tests
kiannaquach Dec 14, 2023
cba3c26
Remove set and brodcast datetimelocal
kiannaquach Dec 14, 2023
ea3919f
Put FormField back the way it was in favor of putting FormField to a …
kiannaquach Dec 14, 2023
286cbd9
Remove getter in formfield component file
kiannaquach Dec 14, 2023
6070022
Merge branch 'ui/VAULT-19096/customizable-banners' into ui/VAULT-2153…
kiannaquach Dec 14, 2023
30424e1
Merge branch 'ui/VAULT-19096/customizable-banners' into ui/VAULT-2153…
kiannaquach Dec 14, 2023
9611ec2
Address more feedback
kiannaquach Dec 14, 2023
3786144
Put back test
kiannaquach Dec 14, 2023
f1279a4
Update datetime string format var name and location
kiannaquach Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions ui/app/adapters/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,14 @@
*/

import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class MessageAdapter extends ApplicationAdapter {
getCustomMessagesUrl(id) {
let url = `${this.buildURL()}/config/ui/custom-messages`;

if (id) {
url = url + '/' + encodePath(id);
}
return url;
pathForType() {
return 'config/ui/custom-messages';
}

query(store, type, query) {
const { authenticated } = query;

return this.ajax(this.getCustomMessagesUrl(), 'GET', { data: { authenticated, list: true } });
}

deleteRecord(store, type, snapshot) {
const { id } = snapshot;
return this.ajax(this.getCustomMessagesUrl(id), 'DELETE');
return super.query(store, type, { authenticated, list: true });
}
}
1 change: 0 additions & 1 deletion ui/app/components/sidebar/nav/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@
<Nav.Title data-test-sidebar-nav-heading="Settings">Settings</Nav.Title>
<Nav.Link
@route="vault.cluster.config-ui.messages"
@query={{hash authenticated=true}}
@text="Custom Messages"
data-test-sidebar-nav-link="Custom Messages"
/>
Expand Down
90 changes: 80 additions & 10 deletions ui/app/models/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,91 @@
* 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 } from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { isAfter, format, addDays, startOfDay } from 'date-fns';
import { datetimeLocalStringFormat, parseAPITimestamp } from 'core/utils/date-formatters';
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.' }],
};

@withFormFields(['authenticated', 'type', 'title', 'message', 'linkTitle', 'startTime', 'endTime'])
@withModelValidations(validations)
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
export default class MessageModel extends Model {
@attr('boolean') active;
@attr('string') type;
@attr('boolean') authenticated;
@attr('string') title;
@attr('string') message;
@attr('object') link;
@attr('string') startTime;
@attr('string') endTime;
@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('date', {
editType: 'dateTimeLocal',
label: 'Message starts',
subText: 'Defaults to 12:00 a.m. the following day (local timezone).',
defaultValue: format(addDays(startOfDay(new Date() || this.startTime), 1), datetimeLocalStringFormat),
})
startTime;
@attr('date', { editType: 'yield', label: 'Message expires' }) endTime;

// the api returns link as an object with title and href as keys, but we separate the link key/values into
// different attributes to easily show link title and href fields on the create form. In our serializer,
// we send the link attribute in to the correct format (as an object) to the server.
@attr('string') linkTitle;
@attr('string') linkHref;

// date helpers
get isStartTimeAfterToday() {
Expand Down
24 changes: 23 additions & 1 deletion ui/app/serializers/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,37 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import { encodeString } from 'core/utils/b64';
import ApplicationSerializer from '../application';

export default class MessageSerializer extends ApplicationSerializer {
primaryKey = 'id';

serialize() {
const json = super.serialize(...arguments);
json.message = encodeString(json.message);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For custom messages, we discussed encoding the string when it's sent to the backend and decoding it when we show the message on the details screen as well as on the authenticated / unauth screens.

json.link = {
title: json.link_title,
href: json.link_href,
};

delete json.link_title;
delete json.link_href;

return json;
}

extractLazyPaginatedData(payload) {
if (payload.data) {
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
return payload.data.keys.map((key) => ({ id: key, ...payload.data.key_info[key] }));
return payload.data.keys.map((key) => {
return {
id: key,
linkTitle: payload.data.key_info.link?.title,
linkHref: payload.data.key_info.link?.href,
...payload.data.key_info[key],
};
});
}
Object.assign(payload, payload.data);
delete payload.data;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<div class="has-top-padding-xs has-bottom-padding-s is-narrow is-flex-align-baseline">
<RadioButton
class="radio"
name={{@attr.name}}
id="never"
value="never"
@value="never"
@onChange={{fn (mut @message.endTime) ""}}
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
@groupValue={{this.groupValue}}
/>
<label for="never" class="has-left-margin-xs has-text-black is-size-7">
<span class="has-left-margin-xs">
Never
</span>
<p class="has-left-margin-xs has-text-grey is-size-8">
This message will never expire unless manually deleted by an operator.
</p>
</label>
</div>

<div class="has-top-padding-xs has-bottom-padding-s is-narrow is-flex-align-baseline">
<RadioButton
class="radio"
name={{@attr.name}}
id="specificDate"
value="specificDate"
@value="specificDate"
@onChange={{fn (mut @message.endTime) this.formDateTime}}
@groupValue={{this.groupValue}}
/>
<label for="specificDate" class="has-left-margin-xs has-text-black is-size-7">
<span class="has-left-margin-xs">
Specific date
</span>
<p class="has-left-margin-xs has-text-grey is-size-8">
This message will expire at midnight (local timezone) at the specific date.
</p>
<div class="has-left-margin-xs control">
<Input
@type="datetime-local"
@value={{if this.formDateTime (date-format this.formDateTime this.datetimeLocalStringFormat) ""}}
class="input has-top-margin-xs is-auto-width"
name="endTime"
data-test-input="endTime"
{{on "change" (pipe (pick "target.value") (fn (mut @message.endTime)))}}
/>
</div>
</label>
</div>
Original file line number Diff line number Diff line change
@@ -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
* <Messages::MessageExpirationDateForm @message={{this.message}} @attr={{attr}} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having this component manage setting the model value, could you just pass the attribute and an "onChange" function that passes back the end time as either an empty string or a timestamp? I don't think you'd need a component file in that case 🤔
That way this component acts more as a fancy input, and all of the model updates happen in the create-and-edit- form

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did something similar at one point in this PR, but removed it since I thought it was easier to understand this way since you're able to see the logic in the template rather than navigating back to the create-and-edit- to see the onChange action. 🤷‍♀️ I think we'll need a component no matter what since the groupValue and formDateTime tracked properties are initially set in the constructor based on whether an endTime exists. An argument can be made that this could be in the create-and-edit- too, but I feel like this should be role of the message-expiration-date-form

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make note of this as a possible improvement, but don't think it should be a blocker for merging this in the sidebranch. The next ticket I'll work on is the edit so I'll try addressing this there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. My suggestion was based on handling the input similar to the pattern of how other form fields work. You definitely have more context here! I find attribute changes are easier to follow if all input components are responsible for is returning the selected/inputed value. In this case, that'd be an ISO timestamp string.

* ```
* @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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<Messages::TabPageHeader
@authenticated={{@message.authenticated}}
@pageTitle="{{if @message.isNew 'Create' 'Edit'}} message"
@breadcrumbs={{this.breadcrumbs}}
/>

<form id="message-create-edit-form" {{on "submit" (perform this.save)}} data-test-form="create-and-edit">
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-s">
<Hds::Text::Body @tag="p" class="has-bottom-margin-l" data-test-form-subtext>
{{if @message.isNew "Create" "Edit"}}
a custom message for all users when they access a Vault system via the UI.
</Hds::Text::Body>

<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />

{{#each @message.formFields as |attr|}}
{{#if (eq attr.name "linkTitle")}}
<div class="field has-bottom-margin-m">
<label for="link" class="is-label">Link
<span class="has-text-grey-400 has-font-weight-normal">(optional)</span></label>
<div class="control is-flex-between has-gap-m">
<Input
@type="text"
@value={{@message.linkTitle}}
placeholder="Display text (e.g. Learn more)"
id="link-title"
class="input"
data-test-input={{attr.name}}
{{on "input" (pipe (pick "target.value") (fn (mut @message.linkTitle)))}}
aria-label="Link title"
/>
<Input
@type="text"
@value={{@message.linkHref}}
placeholder="Paste URL (e.g. www.learnmore.com)"
id="link-href"
class="input"
data-test-input="linkHref"
{{on "input" (pipe (pick "target.value") (fn (mut @message.linkHref)))}}
aria-label="Link href"
/>
</div>
</div>
{{else}}
<FormField
@attr={{attr}}
@model={{@message}}
@modelValidations={{this.modelValidations}}
class="has-bottom-margin-m"
>
<Messages::MessageExpirationDateForm @message={{@message}} @attr={{attr}} />
</FormField>
{{/if}}
{{/each}}

<Hds::ButtonSet class="has-top-margin-s has-bottom-margin-m has-top-margin-xl">
{{! TODO: VAULT-21533 preview modal }}
<Hds::Button @text="Preview" @color="tertiary" @icon="eye" />

<Hds::Button
@text="{{if @message.isNew 'Create' 'Edit'}} message"
disabled={{(or this.invalidFormMessage this.save.isRunning)}}
data-test-button="create-message"
type="submit"
/>

<Hds::Button
@text="Cancel"
@color="secondary"
@route="messages"
@query={{hash authenticated=@message.authenticated}}
/>
</Hds::ButtonSet>
</div>
</form>
Loading
Loading