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-21530] Custom Messages List View w/ Pagination and LazyPaginatedQuery #24133

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 22 additions & 1 deletion ui/app/adapters/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,26 @@
*/

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

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

if (id) {
url = url + '/' + encodePath(id);
}
return url;
}

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

Choose a reason for hiding this comment

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

What if query is false? Does the API error? You might want to consider a catch statement here and doing something with the error if it's a 404 (no response, e.g. no items in the list) vs anything else like a 403. There is a good example of this on the ldap library adapter.

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 originally had a try/catch here but the 404/403 error didn't bubble to the query. Since I'm using lazyPaginatedQuery, I did the try/catch in the route instead and the errors get surfaced in there. https://github.com/hashicorp/vault/pull/24133/files#diff-5a46be6bfd29de6b487be8b6242b62bdd861524c47cc1fb10eb4a876e00c129f


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

deleteRecord(store, type, snapshot) {
const { id } = snapshot;
return this.ajax(this.getCustomMessagesUrl(id), 'DELETE');
}
}
1 change: 1 addition & 0 deletions ui/app/components/sidebar/nav/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
<Nav.Title data-test-sidebar-nav-heading="Settings">Settings</Nav.Title>
<Nav.Link
@route="vault.cluster.config-ui.messages"
@query={{hash authenticated=true}}
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
@text="Custom Messages"
data-test-sidebar-nav-link="Custom Messages"
/>
Expand Down
8 changes: 8 additions & 0 deletions ui/app/models/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import Model, { attr } from '@ember-data/model';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { isAfter } from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';

export default class MessageModel extends Model {
@attr('boolean') active;
Expand All @@ -16,6 +18,12 @@ export default class MessageModel extends Model {
@attr('string') startTime;
@attr('string') endTime;

// date helpers
get isStartTimeAfterToday() {
return isAfter(parseAPITimestamp(this.startTime), new Date());
}

// capabilities
@lazyCapabilities(apiPath`sys/config/ui/custom-messages`) customMessagesPath;

get canCreateCustomMessages() {
Expand Down
15 changes: 14 additions & 1 deletion ui/app/serializers/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,17 @@

import ApplicationSerializer from '../application';

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

extractLazyPaginatedData(payload) {
if (payload.data) {
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
return payload.data.keys.map((key) => ({ id: key, ...payload.data.key_info[key] }));
}
Object.assign(payload, payload.data);
delete payload.data;
}
return payload;
}
}
101 changes: 101 additions & 0 deletions ui/lib/config-ui/addon/components/messages/page/list.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<Messages::TabPageHeader @authenticated={{@authenticated}}>
<:toolbarActions>
<ToolbarLink data-test-toolbar-create-message @route="messages.create" @type="add">
Create message
</ToolbarLink>
</:toolbarActions>
</Messages::TabPageHeader>

{{#if @messages.length}}
{{#each this.getMessages as |message|}}
<LinkedBlock
data-test-list-item={{message.id}}
class="list-item-row"
@params={{array "messages.message.details" message.id}}
@linkPrefix="vault.cluster.config-ui"
>
<div class="level is-mobile">
<div class="level-left">
<div>
<Hds::Text::Display @tag="h2" data-linked-block-title={{message.id}}>
<Icon @name="message-circle" class="auto-width" aria-label="message" />
{{message.title}}
</Hds::Text::Display>
<div class="has-top-margin-xs">
<Hds::Badge
@text={{message.badgeDisplayText}}
@color={{if message.active "highlight" "neutral"}}
data-test-badge={{concat "active:" message.active}}
/>
<Hds::Badge
@text={{(capitalize message.type)}}
@color={{if message.active "highlight" "neutral"}}
data-test-badge={{message.type}}
/>
</div>
</div>
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu @name="engine-menu">
<Confirm as |c|>
<nav class="menu">
<ul class="menu-list">
{{#if message.canEditCustomMessages}}
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
<li class="action">
<LinkTo @route="messages.message.edit" @model={{message.id}}>
Edit
</LinkTo>
</li>
{{/if}}
{{#if message.canDeleteCustomMessages}}
<li class="action">
<c.Message
@id={{message.id}}
@triggerText="Delete"
@message="This will delete this message permanently. You cannot undo this action."
@title="Are you sure?"
@confirmButtonText="Delete"
@onConfirm={{perform this.deleteMessage message}}
data-test-delete-custom-message
/>
</li>
{{/if}}
</ul>
</nav>
</Confirm>
</PopupMenu>
</div>
</div>
</div>
</LinkedBlock>
{{/each}}
<Hds::Pagination::Numbered
class="has-top-margin-m has-bottom-margin-m"
@currentPage={{@messages.meta.currentPage}}
@currentPageSize={{@messages.meta.pageSize}}
@route="messages.index"
@showSizeSelector={{false}}
@totalItems={{@messages.meta.total}}
@queryFunction={{this.paginationQueryParams}}
/>
{{else}}
<EmptyState
@title="No messages yet"
@message="Add a custom message for all users after they log into Vault. Create message to get started."
>
<Hds::Link::Standalone
@icon="plus"
@iconPosition="leading"
@text="Create message"
@route="messages.create"
class="is-no-underline"
data-test-action-text="Create message"
/>
</EmptyState>
{{/if}}
67 changes: 67 additions & 0 deletions ui/lib/config-ui/addon/components/messages/page/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { dateFormat } from 'core/helpers/date-format';

/**
* @module Page::MessagesList
* Page::MessagesList components are used to display list of messages.
* @example
* ```js
* <Page::MessagesList @messages={{this.messages}} />
* ```
* @param {array} messages - array message objects
*/

export default class MessagesList extends Component {
@service store;

get getMessages() {
return this.args.messages.map((message) => {
let badgeDisplayText = '';

if (message.active) {
if (message.endTime) {
badgeDisplayText = `Active until ${dateFormat([message.endTime, 'MMM d, yyyy hh:mm aaa'], {
withTimeZone: true,
})}`;
} else {
badgeDisplayText = 'Active';
}
} else {
if (message.isStartTimeAfterToday) {
badgeDisplayText = `Scheduled: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
withTimeZone: true,
})}`;
} else {
badgeDisplayText = `Inactive: ${dateFormat([message.startTime, 'MMM d, yyyy hh:mm aaa'], {
withTimeZone: true,
})}`;
}
}

message.badgeDisplayText = badgeDisplayText;
return message;
});
}

// callback from HDS pagination to set the queryParams page
get paginationQueryParams() {
return (page) => {
return {
page,
};
};
}

@task
*deleteMessage(message) {
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
this.store.clearDataset('config-ui/message');
yield message.destroyRecord(message.id);
}
}
52 changes: 52 additions & 0 deletions ui/lib/config-ui/addon/components/messages/tab-page-header.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<PageHeader as |p|>
{{#if @breadcrumbs}}
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
{{/if}}
<p.levelLeft>
<Hds::Text::Display @tag="h2" @size="500" class="has-top-margin-xl">
Custom messages
</Hds::Text::Display>
</p.levelLeft>
</PageHeader>

<div class="tabs-container box is-bottomless is-marginless is-paddingless">
<nav class="tabs" aria-label="custom-messages">
<ul>
{{! Explicitly setting page to 1 here since we want to reset the page param on transition}}
<LinkTo
class={{if @authenticated "active"}}
Copy link
Contributor Author

@kiannaquach kiannaquach Nov 15, 2023

Choose a reason for hiding this comment

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

An issue I ran into: when transitioning to different tabs, the page query params are retained from the previous tab. This is the default behavior since controllers are singletons. I tried adding a resetController to reset the page to 1. I noticed that we have explicitly state what the query params are in order for the tabs to transition with the correct query params. Here's a similar description of what I saw happening: https://guides.emberjs.com/v2.14.0/routing/query-params/

Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to reset to null. Let's zoom through this as it gets a little confusing. Ex:

resetController(controller, isExiting) {
    if (isExiting) {
      controller.set('pageFilter', null);
      controller.set('page', null);
    }
  }

@route="messages"
@query={{hash authenticated=true page=1}}
data-test-custom-messages-tab="After user logs in"
>
After user logs in
</LinkTo>
<LinkTo
class={{unless @authenticated "active"}}
Copy link
Contributor

Choose a reason for hiding this comment

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

you might also try using the ember truth-helper not. https://github.com/jmurphyau/ember-truth-helpers. It reads easier than unless.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The linter recommends unless instead of if not @authenticated🫠

Copy link
Contributor

Choose a reason for hiding this comment

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

TIL.

@route="messages"
@query={{hash authenticated=false page=1}}
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
data-test-custom-messages-tab="On login page"
>
On login page
</LinkTo>
</ul>
</nav>
</div>

{{#if (or (has-block "toolbarFilters") (has-block "toolbarActions"))}}
<Toolbar>
<ToolbarFilters>
{{yield to="toolbarFilters"}}
</ToolbarFilters>
<ToolbarActions>
{{yield to="toolbarActions"}}
</ToolbarActions>
</Toolbar>
{{/if}}
12 changes: 12 additions & 0 deletions ui/lib/config-ui/addon/controllers/messages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Controller from '@ember/controller';
export default class MessagesController extends Controller {
queryParams = ['authenticated', 'page'];

authenticated = true;
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
page = 1;
}
31 changes: 30 additions & 1 deletion ui/lib/config-ui/addon/routes/messages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,34 @@
*/

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class MessagesRoute extends Route {}
export default class MessagesRoute extends Route {
@service store;

queryParams = {
page: {
refreshModel: true,
},
authenticated: {
refreshModel: true,
},
};

async model(params) {
try {
const { authenticated, page } = params;
return await this.store.lazyPaginatedQuery('config-ui/message', {
authenticated,
responsePath: 'data.keys',
page: page || 1,
kiannaquach marked this conversation as resolved.
Show resolved Hide resolved
});
} catch (e) {
if (e.httpStatus === 404) {
return [];
}

throw e;
}
}
}
2 changes: 1 addition & 1 deletion ui/lib/config-ui/addon/templates/messages/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
SPDX-License-Identifier: BUSL-1.1
~}}

Messages List View
<Messages::Page::List @messages={{this.model}} @authenticated={{this.authenticated}} />
Loading
Loading