Skip to content

Commit

Permalink
UI: VAULT-21538 unauth endpoint message display (#24665)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kiannaquach authored Jan 5, 2024
1 parent 833c527 commit 1622f09
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 39 deletions.
2 changes: 2 additions & 0 deletions ui/app/controllers/vault/cluster/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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(),

Expand Down
15 changes: 11 additions & 4 deletions ui/app/serializers/config-ui/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export default class MessageSerializer extends ApplicationSerializer {

return snapshotDateTime;
}

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'queryRecord') {
const transformed = {
Expand Down Expand Up @@ -55,27 +54,35 @@ export default class MessageSerializer extends ApplicationSerializer {
// if this date is not an object and isn’t a local date string, then return the snapshot date, which is set by default
// values defined on the model.
json.start_time = this.getISODateFormat(snapshot.record.startTime, json.start_time);
json.end_time = this.getISODateFormat(snapshot.record.endTime, json.end_time);
json.end_time = snapshot.record.endTime
? this.getISODateFormat(snapshot.record.endTime, json.end_time)
: null;
delete json?.link_title;
delete json?.link_href;
return json;
}

extractLazyPaginatedData(payload) {
mapPayload(payload) {
if (payload.data) {
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
return payload.data.keys.map((key) => {
return {
const data = {
id: key,
linkTitle: payload.data.key_info.link?.title,
linkHref: payload.data.key_info.link?.href,
...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);
}
}
47 changes: 47 additions & 0 deletions ui/app/services/custom-messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

export default class CustomMessageService extends Service {
@service store;
@service namespace;
@tracked messages = [];
@tracked showMessageModal = true;

constructor() {
super(...arguments);
this.fetchMessages(this.namespace.path);
}

get bannerMessages() {
return this.messages?.filter((message) => message?.type === 'banner');
}

get modalMessages() {
return this.messages?.filter((message) => message?.type === 'modal');
}

async fetchMessages(ns) {
try {
const url = '/v1/sys/internal/ui/unauthenticated-messages';
const opts = {
method: 'GET',
headers: {},
};
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);
} catch (e) {
return e;
}
}
}
1 change: 0 additions & 1 deletion ui/app/templates/components/auth-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<div class="auth-form" data-test-auth-form>
{{#if (and this.waitingForOktaNumberChallenge (not this.cancelAuthForOktaNumberChallenge))}}
<OktaNumberChallenge
Expand Down
30 changes: 30 additions & 0 deletions ui/app/templates/vault/cluster/auth.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@
</EmptyState>
</div>
{{else}}
{{#if this.customMessages.bannerMessages.length}}
{{#each this.customMessages.bannerMessages as |bannerMessage|}}
<Hds::Alert @type="inline" @color="warning" data-test-alert={{bannerMessage.id}} as |A|>
<A.Title data-test-alert-title={{bannerMessage.id}}>{{bannerMessage.title}}</A.Title>
<A.Description data-test-alert-description={{bannerMessage.id}}>
{{bannerMessage.message}}
</A.Description>
<A.Description class="has-top-margin-xs">
{{! TODO: VAULT-22908 display links when api is updated to { link: { 'learn': 'www.learn.com'} } }}
</A.Description>
</Hds::Alert>
{{/each}}
{{/if}}
{{#if this.customMessages.modalMessages.length}}
{{#each this.customMessages.modalMessages as |modalMessage|}}
<Hds::Modal id={{modalMessage.id}} @size="large" @color="warning" data-test-modal={{modalMessage.id}} as |M|>
<M.Header data-test-modal-title={{modalMessage.id}}>
{{modalMessage.title}}
</M.Header>
<M.Body data-test-modal-body={{modalMessage.id}}>
{{modalMessage.message}}
{{! TODO: VAULT-22908 display links when api is updated to { link: { 'learn': 'www.learn.com'} } }}
</M.Body>
<M.Footer as |F|>
<Hds::Button @text="Confirm" {{on "click" F.close}} data-test-modal-button={{modalMessage.id}} />
</M.Footer>
</Hds::Modal>
{{/each}}
{{/if}}

<SplashPage>
<:header>
{{#if this.oidcProvider}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
<M.Header data-test-modal-title={{@message.title}}>
{{@message.title}}
</M.Header>
<M.Body data-test-modal-body>
<M.Body data-test-modal-body={{@message.title}}>
{{@message.message}}
{{#if @message.linkHref}}
<Hds::Link::Inline @icon="external-link" @href={{@message.linkHref}}>
Expand Down
46 changes: 29 additions & 17 deletions ui/mirage/handlers/custom-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,31 +92,43 @@ export default function (server) {

server.get('/sys/internal/ui/unauthenticated-messages', () => {
return {
request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
key_info: {
'01234567-89ab-cdef-0123-456789abcdef': {
title: 'Unauthenticated Title One',
message:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla augue, placerat quis risus blandit, molestie imperdiet massa. Sed blandit rutrum odio quis varius. Fusce purus orci, maximus ac libero.',
type: 'modal',
'02180e3f-bd5b-a851-bcc9-6f7983806df0': {
authenticated: false,
start_time: '2023-10-15T02:36:43.986212308Z',
end_time: '2024-10-15T02:36:43.986212308Z',
options: {},
},
'76543210-89ab-cdef-0123-456789abcdef': {
title: 'Unauthenticated Title Two',
message:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla augue, placerat quis risus blandit, molestie imperdiet massa. Sed blandit rutrum odio quis varius. Fusce purus orci, maximus ac libero.',
end_time: null,
link: {
title: '',
},
message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
options: null,
start_time: '2024-01-04T08:00:00Z',
title: 'Banner title',
type: 'banner',
},
'a7d7d9b1-a1ca-800c-17c5-0783be88e29c': {
authenticated: false,
start_time: '2021-10-15T02:36:43.986212308Z',
end_time: '2031-10-15T02:36:43.986212308Z',
options: {},
end_time: null,
link: {
title: '',
},
message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
options: null,
start_time: '2024-01-01T08:00:00Z',
title: 'Modal title',
type: 'modal',
},
},
keys: ['01234567-89ab-cdef-0123-456789abcdef', '76543210-89ab-cdef-0123-456789abcdef'],
keys: ['02180e3f-bd5b-a851-bcc9-6f7983806df0', 'a7d7d9b1-a1ca-800c-17c5-0783be88e29c'],
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: '',
};
});

Expand Down
115 changes: 115 additions & 0 deletions ui/tests/acceptance/custom-messages-auth-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, visit } from '@ember/test-helpers';
import { PAGE } from 'vault/tests/helpers/config-ui/message-selectors';
import { setupMirage } from 'ember-cli-mirage/test-support';

const unauthenticatedMessageResponse = {
request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
key_info: {
'some-awesome-id-2': {
authenticated: false,
end_time: null,
link: {
title: '',
},
message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
options: null,
start_time: '2024-01-04T08:00:00Z',
title: 'Banner title',
type: 'banner',
},
'some-awesome-id-1': {
authenticated: false,
end_time: null,
link: {
title: '',
},
message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
options: null,
start_time: '2024-01-01T08:00:00Z',
title: 'Modal title',
type: 'modal',
},
},
keys: ['some-awesome-id-2', 'some-awesome-id-1'],
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: '',
};

module('Acceptance | auth custom messages auth tests', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);

hooks.beforeEach(function () {
return this.server.get('/sys/internal/ui/mounts', () => ({}));
});

test('it shows the alert banner and modal message', async function (assert) {
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
return unauthenticatedMessageResponse;
});
await visit('/vault/auth');
const modalId = 'some-awesome-id-1';
const alertId = 'some-awesome-id-2';
assert.dom(PAGE.modal(modalId)).exists();
assert.dom(PAGE.modalTitle(modalId)).hasText('Modal title');
assert.dom(PAGE.modalBody(modalId)).exists();
assert.dom(PAGE.modalBody(modalId)).hasText('here is a cool message');
await click(PAGE.modalButton(modalId));
assert.dom(PAGE.alertTitle(alertId)).hasText('Banner title');
assert.dom(PAGE.alertDescription(alertId)).hasText('hello world hello wolrd');
});
test('it shows the multiple modal messages', async function (assert) {
const modalIdOne = 'some-awesome-id-2';
const modalIdTwo = 'some-awesome-id-1';

this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
unauthenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal';
unauthenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1';
unauthenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal';
unauthenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2';
return unauthenticatedMessageResponse;
});
await visit('/vault/auth');
assert.dom(PAGE.modal(modalIdOne)).exists();
assert.dom(PAGE.modalTitle(modalIdOne)).hasText('Modal title 1');
assert.dom(PAGE.modalBody(modalIdOne)).exists();
assert.dom(PAGE.modalBody(modalIdOne)).hasText('hello world hello wolrd');
await click(PAGE.modalButton(modalIdOne));
assert.dom(PAGE.modal(modalIdTwo)).exists();
assert.dom(PAGE.modalTitle(modalIdTwo)).hasText('Modal title 2');
assert.dom(PAGE.modalBody(modalIdTwo)).exists();
assert.dom(PAGE.modalBody(modalIdTwo)).hasText('here is a cool message');
await click(PAGE.modalButton(modalIdTwo));
});
test('it shows the multiple banner messages', async function (assert) {
const bannerIdOne = 'some-awesome-id-2';
const bannerIdTwo = 'some-awesome-id-1';

this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
unauthenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner';
unauthenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1';
unauthenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner';
unauthenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2';
return unauthenticatedMessageResponse;
});
await visit('/vault/auth');
assert.dom(PAGE.alertTitle(bannerIdOne)).hasText('Banner title 1');
assert.dom(PAGE.alertDescription(bannerIdOne)).hasText('hello world hello wolrd');
assert.dom(PAGE.alertTitle(bannerIdTwo)).hasText('Banner title 2');
assert.dom(PAGE.alertDescription(bannerIdTwo)).hasText('here is a cool message');
});
});
21 changes: 21 additions & 0 deletions ui/tests/helpers/config-ui/message-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

export const PAGE = {
// General selectors that are common between pages
radio: (radioName) => `[data-test-radio="${radioName}"]`,
field: (fieldName) => `[data-test-field="${fieldName}"]`,
input: (input) => `[data-test-input="${input}"]`,
button: (buttonName) => `[data-test-button="${buttonName}"]`,
inlineErrorMessage: `[data-test-inline-error-message]`,
fieldVaildation: (fieldName) => `[data-test-field-validation="${fieldName}"]`,
modal: (name) => `[data-test-modal="${name}"]`,
modalTitle: (title) => `[data-test-modal-title="${title}"]`,
modalBody: (name) => `[data-test-modal-body="${name}"]`,
modalButton: (name) => `[data-test-modal-button="${name}"]`,
alert: (name) => `data-test-alert=${name}`,
alertTitle: (name) => `[data-test-alert-title="${name}"]`,
alertDescription: (name) => `[data-test-alert-description="${name}"]`,
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,7 @@ import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { datetimeLocalStringFormat } from 'core/utils/date-formatters';
import { format, addDays, startOfDay } from 'date-fns';

const PAGE = {
radio: (radioName) => `[data-test-radio="${radioName}"]`,
field: (fieldName) => `[data-test-field="${fieldName}"]`,
input: (input) => `[data-test-input="${input}"]`,
button: (buttonName) => `[data-test-button="${buttonName}"]`,
inlineErrorMessage: `[data-test-inline-error-message]`,
fieldVaildation: (fieldName) => `[data-test-field-validation="${fieldName}"]`,
modal: (name) => `[data-test-modal="${name}"]`,
modalTitle: (title) => `[data-test-modal-title="${title}"]`,
modalBody: '[data-test-modal-body]',
modalButton: (name) => `[data-test-modal-button="${name}"]`,
alertTitle: (name) => `[data-test-alert-title="${name}"]`,
alertDescription: (name) => `[data-test-alert-description="${name}"]`,
};
import { PAGE } from 'vault/tests/helpers/config-ui/message-selectors';

module('Integration | Component | messages/page/create-and-edit-message', function (hooks) {
setupRenderingTest(hooks);
Expand Down Expand Up @@ -185,6 +171,6 @@ module('Integration | Component | messages/page/create-and-edit-message', functi
assert.dom(PAGE.modal('preview modal')).exists();
assert.dom(PAGE.modal('preview image')).doesNotExist();
assert.dom(PAGE.modalTitle('Preview modal title')).hasText('Preview modal title');
assert.dom(PAGE.modalBody).hasText('Some preview modal message thats super long.');
assert.dom(PAGE.modalBody('Preview modal title')).hasText('Some preview modal message thats super long.');
});
});
Loading

0 comments on commit 1622f09

Please sign in to comment.