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: Update custom message list filters #26653

Merged
merged 5 commits into from
Apr 26, 2024
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
3 changes: 3 additions & 0 deletions changelog/26653.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui (enterprise): Update filters on the custom messages list view.
```
4 changes: 4 additions & 0 deletions ui/app/styles/components/toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,7 @@ a.disabled.toolbar-link {
margin: 0 $spacing-8;
width: 0;
}

.segment-filter {
width: min-content;
}
92 changes: 58 additions & 34 deletions ui/lib/config-ui/addon/components/messages/page/list.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,64 @@
@breadcrumbs={{this.breadcrumbs}}
>
<:toolbarFilters>
<FilterInput
aria-label="Search by message title"
placeholder="Search by message title"
id="message-filter"
value={{@params.pageFilter}}
@autofocus={{true}}
@onInput={{this.onFilterInputChange}}
/>
<div>
<SearchSelect
@id="filter-by-message-status"
class="has-left-margin-s"
@options={{this.statusFilterOptions}}
@selectLimit="1"
@searchEnabled={{false}}
@fallbackComponent="select"
@onChange={{fn this.onFilterChange "status"}}
@placeholder="Filter by message status"
@inputValue={{if @params.status (array @params.status)}}
data-test-filter-by-message-status
/>
</div>
<SearchSelect
@id="filter-by-message-type"
class="has-left-margin-s"
@options={{this.typeFilterOptions}}
@selectLimit="1"
@searchEnabled={{false}}
@fallbackComponent="select"
@onChange={{fn this.onFilterChange "type"}}
@placeholder="Filter by message type"
@inputValue={{if @params.type (array @params.type)}}
data-test-filter-by-message-type
/>
<form {{on "submit" (perform this.handleSearch)}} aria-label="Filter custom message list">
<Hds::SegmentedGroup as |S|>
Copy link
Contributor

Choose a reason for hiding this comment

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

👏

<S.Button
@color="secondary"
@text="Clear filters"
@icon="trash"
@isIconOnly={{true}}
type="reset"
{{on "click" this.resetFilters}}
data-test-filter-reset
/>
<S.TextInput
@value={{@params.pageFilter}}
@type="search"
name="pageFilter"
class="segment-filter"
placeholder="Search by message title"
aria-label="Search by message title"
size="32"
data-test-filter-by="pageFilter"
/>
<S.Select
aria-label="Filter by message status"
name="status"
class="segment-filter {{unless @params.status 'has-text-grey'}}"
data-test-filter-by="status"
as |S|
>
<S.Options>
<option value="" class="default-option">Message status</option>
{{#each (array "active" "inactive") as |status|}}
<option value={{status}} selected={{eq @params.status status}}>{{status}}</option>
{{/each}}
</S.Options>
</S.Select>
<S.Select
aria-label="Filter by type"
name="type"
class="segment-filter {{unless @params.status 'has-text-grey'}}"
data-test-filter-by="type"
as |S|
>
<S.Options>
<option value="" class="has-text-grey">Message type</option>
{{#each (array "modal" "banner") as |type|}}
<option value={{type}} selected={{eq @params.type type}}>{{type}}</option>
{{/each}}
</S.Options>
</S.Select>
<S.Button
@color="secondary"
@text="Apply filters"
@icon={{if this.handleSearch.isRunning "loading" "filter"}}
type="submit"
data-test-filter-submit
/>
</Hds::SegmentedGroup>
</form>
</:toolbarFilters>
<:toolbarActions>
<Hds::Button
Expand Down
64 changes: 19 additions & 45 deletions ui/lib/config-ui/addon/components/messages/page/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import Ember from 'ember';
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { task, timeout } from 'ember-concurrency';
import { dateFormat } from 'core/helpers/date-format';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import errorMessage from 'vault/utils/error-message';
import { next } from '@ember/runloop';

/**
* @module Page::MessagesList
Expand All @@ -32,26 +32,6 @@ export default class MessagesList extends Component {
@tracked showMaxMessageModal = false;
@tracked messageToDelete = null;

// This follows the pattern in sync/addon/components/secrets/page/destinations for FilterInput.
// Currently, FilterInput doesn't do a full page refresh causing it to lose focus.
// The work around is to verify that a transition from this route was completed and then focus the input.
constructor(owner, args) {
super(owner, args);
this.router.on('routeDidChange', this.focusNameFilter);
}

willDestroy() {
super.willDestroy();
this.router.off('routeDidChange', this.focusNameFilter);
}

focusNameFilter(transition) {
const route = 'vault.cluster.config-ui.messages.index';
if (transition?.from?.name === route && transition?.to?.name === route) {
next(() => document.getElementById('message-filter')?.focus());
}
}

get formattedMessages() {
return this.args.messages.map((message) => {
let badgeDisplayText = '';
Expand Down Expand Up @@ -91,20 +71,6 @@ export default class MessagesList extends Component {
return [{ label: 'Messages' }, { label }];
}

get statusFilterOptions() {
return [
{ id: 'active', name: 'active' },
{ id: 'inactive', name: 'inactive' },
];
}

get typeFilterOptions() {
return [
{ id: 'modal', name: 'modal' },
{ id: 'banner', name: 'banner' },
];
}

// callback from HDS pagination to set the queryParams page
get paginationQueryParams() {
return (page) => {
Expand All @@ -116,7 +82,8 @@ export default class MessagesList extends Component {

transitionToMessagesWithParams(queryParams) {
this.router.transitionTo('vault.cluster.config-ui.messages', {
queryParams,
// always reset back to page 1 when changing filters
queryParams: { ...queryParams, page: 1 },
});
}

Expand All @@ -136,17 +103,24 @@ export default class MessagesList extends Component {
}
}

@action
onFilterInputChange(pageFilter) {
this.transitionToMessagesWithParams({ pageFilter });
@task
Copy link
Contributor

Choose a reason for hiding this comment

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

nicely handled!

*handleSearch(evt) {
evt.preventDefault();
const formData = new FormData(evt.target);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 today I learned about the FormData class. Is this why you don't have to manage and track the value on the form inputs in the hbs file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right! As long as we pass a name to the input, we can access the values by name from the form submit event. When we don't need to show other things based on form values or do validation on keyup, this is a simple way to handle form submits

// shows loader to indicate that the search was executed
yield timeout(Ember.testing ? 0 : 250);
const params = {};
for (const key of formData.keys()) {
const valDefault = key === 'pageFilter' ? '' : null;
const val = formData.get(key) || valDefault;
params[key] = val;
}
this.transitionToMessagesWithParams(params);
}

@action
onFilterChange(filterType, [filterOption]) {
const param = {};
param[filterType] = filterOption;
param.page = 1;
this.transitionToMessagesWithParams(param);
resetFilters() {
this.transitionToMessagesWithParams({ pageFilter: '', status: null, type: null });
}

@action
Expand Down
102 changes: 38 additions & 64 deletions ui/tests/acceptance/config-ui/messages/messages-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,22 @@ import logout from 'vault/tests/pages/logout';
import { format, addDays, startOfDay } from 'date-fns';
import { datetimeLocalStringFormat } from 'core/utils/date-formatters';
import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';

const MESSAGES_LIST = {
listItem: '.linked-block',
filterBy: (name) => `[data-test-filter-by="${name}"]`,
filterSubmit: '[data-test-filter-submit]',
filterReset: '[data-test-filter-reset]',
};

module('Acceptance | Community | config-ui/messages', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);

hooks.beforeEach(async function () {
this.server.get('/sys/health', function () {
return {
enterprise: false,
initialized: true,
sealed: false,
standby: false,
license: {
expiry: '2024-01-12T23:20:50.52Z',
state: 'stored',
},
performance_standby: false,
replication_performance_mode: 'disabled',
replication_dr_mode: 'disabled',
server_time_utc: 1622562585,
version: '1.16.0',
cluster_name: 'vault-cluster-e779cd7c',
cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b',
last_wal: 121,
};
});
const version = this.owner.lookup('service:version');
version.type = 'community';
await authPage.login();
});

Expand All @@ -58,6 +46,8 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
setupMirage(hooks);

hooks.beforeEach(async function () {
const version = this.owner.lookup('service:version');
version.type = 'enterprise';
this.messageDetailId = () => {
return currentURL().match(/messages\/(.*)\/details/)[1];
};
Expand Down Expand Up @@ -97,26 +87,6 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
await click(CUSTOM_MESSAGES.confirmActionButton('Delete message'));
await click(GENERAL.confirmButton);
};
this.server.get('/sys/health', function () {
return {
enterprise: true,
initialized: true,
sealed: false,
standby: false,
license: {
expiry: '2024-01-12T23:20:50.52Z',
state: 'stored',
},
performance_standby: false,
replication_performance_mode: 'disabled',
replication_dr_mode: 'disabled',
server_time_utc: 1622562585,
version: '1.16.0+ent',
cluster_name: 'vault-cluster-e779cd7c',
cluster_id: '5f20f5ab-acea-0481-787e-71ec2ff5a60b',
last_wal: 121,
};
});
await authPage.login();
});

Expand All @@ -126,10 +96,10 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
test('it should show an empty state when no messages are created', async function (assert) {
assert.expect(4);
await click(CUSTOM_MESSAGES.navLink);
Copy link
Contributor

Choose a reason for hiding this comment

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

nice clean up work

assert.dom('[data-test-component="empty-state"]').exists();
assert.dom(GENERAL.emptyStateTitle).exists();
assert.dom(GENERAL.emptyStateTitle).hasText('No messages yet');
await click(CUSTOM_MESSAGES.tab('On login page'));
assert.dom('[data-test-component="empty-state"]').exists();
assert.dom(GENERAL.emptyStateTitle).exists();
assert.dom(GENERAL.emptyStateTitle).hasText('No messages yet');
});

Expand Down Expand Up @@ -165,37 +135,41 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
await click(CUSTOM_MESSAGES.listItem('Awesome custom message title'));
await click(CUSTOM_MESSAGES.confirmActionButton('Delete message'));
await click(GENERAL.confirmButton);
assert.dom('[data-test-component="empty-state"]').exists('Message was deleted');
assert.dom(GENERAL.emptyStateTitle).exists('Message was deleted');
});
test('it should filter by type and status', async function (assert) {
await this.createMessage('banner', null);
const msg1 = this.messageDetailId();
await this.createMessage('banner');
const msg2 = this.messageDetailId();
await visit('vault/config-ui/messages');
await visit('vault/config-ui/messages?pageFilter=foobar&status=inactive&type=banner');
// check that filters inherit param values
assert.dom(MESSAGES_LIST.filterBy('pageFilter')).hasValue('foobar');
assert.dom(MESSAGES_LIST.filterBy('status')).hasValue('inactive');
assert.dom(MESSAGES_LIST.filterBy('type')).hasValue('banner');
assert.dom(GENERAL.emptyStateTitle).exists();

// clear filters works
await click(MESSAGES_LIST.filterReset);
assert.dom(MESSAGES_LIST.listItem).exists({ count: 2 });

// check number of messages with status filters
await clickTrigger('#filter-by-message-status');
await click('.ember-power-select-options [data-option-index="0"]');
assert.dom('.linked-block').exists({ count: 1 }, 'filtered by active');
await click('[data-test-selected-list-button="delete"]');
await clickTrigger('#filter-by-message-status');
await click('.ember-power-select-options [data-option-index="1"]');
assert.dom('.linked-block').exists({ count: 1 }, 'filtered by inactive');
await click('[data-test-selected-list-button="delete"]');
await fillIn(MESSAGES_LIST.filterBy('status'), 'active');
assert.dom(MESSAGES_LIST.listItem).exists({ count: 2 }, 'list does not filter before clicking submit');
await click(MESSAGES_LIST.filterSubmit);
assert.dom(MESSAGES_LIST.listItem).exists({ count: 1 });

// check number of messages with type filters
await clickTrigger('#filter-by-message-type');
await click('.ember-power-select-options [data-option-index="0"]');
assert.dom('.linked-block').exists({ count: 0 }, 'filtered by modal');
await click('[data-test-selected-list-button="delete"]');
await clickTrigger('#filter-by-message-type');
await click('.ember-power-select-options [data-option-index="1"]');
assert.dom('.linked-block').exists({ count: 2 }, 'filtered by banner');
await click('[data-test-selected-list-button="delete"]');
await click(MESSAGES_LIST.filterReset);
await fillIn(MESSAGES_LIST.filterBy('type'), 'modal');
await click(MESSAGES_LIST.filterSubmit);
assert.dom(GENERAL.emptyStateTitle).exists();

// check number of messages with no filters
assert.dom('.linked-block').exists({ count: 2 }, 'no filters selected');
// unsetting a filter will reset that item in the query
await fillIn(MESSAGES_LIST.filterBy('type'), '');
await fillIn(MESSAGES_LIST.filterBy('status'), 'inactive');
await click(MESSAGES_LIST.filterSubmit);
assert.dom(MESSAGES_LIST.listItem).exists({ count: 1 });

// clean up custom messages
await this.deleteMessage(msg1);
Expand Down Expand Up @@ -270,7 +244,7 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
await click(CUSTOM_MESSAGES.listItem('Awesome custom message title'));
await click(CUSTOM_MESSAGES.confirmActionButton('Delete message'));
await click(GENERAL.confirmButton);
assert.dom('[data-test-component="empty-state"]').exists('Message was deleted');
assert.dom(GENERAL.emptyStateTitle).exists('Message was deleted');
});
test('it should show info message on create and edit form', async function (assert) {
assert.expect(1);
Expand Down
Loading