mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-30 10:14:53 +00:00
Creates a banner alerts component to handle banner notifications
This commit is contained in:
parent
e23f2744cf
commit
9ca78d9570
49
wherehows-web/app/components/application/banner-alerts.ts
Normal file
49
wherehows-web/app/components/application/banner-alerts.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import Component from '@ember/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { computed, get, set } from '@ember/object';
|
||||||
|
import ComputedProperty from '@ember/object/computed';
|
||||||
|
import BannerService, { IBanner } from 'wherehows-web/services/banners';
|
||||||
|
|
||||||
|
export default class BannerAlerts extends Component {
|
||||||
|
/**
|
||||||
|
* Imports the service used to handle actual activation and dismissal of banners. The service also
|
||||||
|
* maintains the banners list
|
||||||
|
* @type {Serivce}
|
||||||
|
*/
|
||||||
|
banners: ComputedProperty<BannerService> = service();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tagname for the html element rendered by this component
|
||||||
|
*/
|
||||||
|
tagName = 'section';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the classnames to attach to the html element rendered by this component
|
||||||
|
*/
|
||||||
|
classNames = ['banner-alerts'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds classnames to specific truthy values of properties on this component
|
||||||
|
*/
|
||||||
|
classNameBindings = ['isShowingBanners:banner-alerts--show:banner-alerts--hide'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* References the banners service computation on whether or not we should be showing banners
|
||||||
|
*/
|
||||||
|
isShowingBanners: ComputedProperty<boolean> = computed.alias('banners.isShowingBanners');
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
/**
|
||||||
|
* Triggered by the user by clicking the dismiss icon on the banner, triggers the exiting state on the
|
||||||
|
* topmost (first in queue) banner and starts the timer/css animation for the dismissal action
|
||||||
|
* @param this - explicit this declaration for typescript
|
||||||
|
* @param {IBanner} banner - the banner as a subject for the dismissal action
|
||||||
|
*/
|
||||||
|
onDismissBanner(this: BannerAlerts, banner: IBanner) {
|
||||||
|
const banners = get(this, 'banners');
|
||||||
|
|
||||||
|
set(banner, 'isExiting', true);
|
||||||
|
banners.dequeue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -22,6 +22,12 @@ export default Controller.extend({
|
|||||||
|
|
||||||
notifications: service(),
|
notifications: service(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the service for banners in order to trigger the application to render the banners when
|
||||||
|
* they are triggered
|
||||||
|
*/
|
||||||
|
banners: service(),
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
|
|||||||
87
wherehows-web/app/services/banners.ts
Normal file
87
wherehows-web/app/services/banners.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { computed, get } from '@ember/object';
|
||||||
|
import { NotificationEvent } from 'wherehows-web/services/notifications';
|
||||||
|
import { delay } from 'wherehows-web/utils/promise-delay';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected properties to be found on a basic banner object added to our list
|
||||||
|
*/
|
||||||
|
export interface IBanner {
|
||||||
|
content: string;
|
||||||
|
type: NotificationEvent;
|
||||||
|
isExiting: boolean;
|
||||||
|
isDismissable: boolean;
|
||||||
|
iconName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When creating a new banner, helps to determine whether that banner isDismissable
|
||||||
|
*/
|
||||||
|
const isDismissableMap: { [key: string]: boolean } = {
|
||||||
|
[NotificationEvent['info']]: true,
|
||||||
|
[NotificationEvent['confirm']]: true,
|
||||||
|
[NotificationEvent['error']]: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When creating a new banner, helps to determine the kind of font awesome icon indicator will be on
|
||||||
|
* the left side of the banner
|
||||||
|
*/
|
||||||
|
const iconNameMap: { [key: string]: string } = {
|
||||||
|
[NotificationEvent['info']]: 'info-circle',
|
||||||
|
[NotificationEvent['confirm']]: 'exclamation-circle',
|
||||||
|
[NotificationEvent['error']]: 'times-circle'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class BannerService extends Service {
|
||||||
|
/**
|
||||||
|
* Our cached list of banner objects to be rendered
|
||||||
|
* @type {Array<IBanner>}
|
||||||
|
*/
|
||||||
|
banners: Array<IBanner> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the banners list elements to toggle the isShowingBanners flag, which not only activates the
|
||||||
|
* banner component but also triggers the navbar and app-container body to shift up/down to accommodate
|
||||||
|
* for the banner component space
|
||||||
|
* @type {ComputedProperty<boolean>}
|
||||||
|
*/
|
||||||
|
isShowingBanners = computed('banners.@each.isExiting', function() {
|
||||||
|
const banners = get(this, 'banners');
|
||||||
|
// Note: If we have no banners, flag should always be false. If we have more than one banner, flag
|
||||||
|
// should always be true, BUT if we only have one banner and it is in an exiting state we can go ahead
|
||||||
|
// and trigger this to be false to line up with the animation
|
||||||
|
return banners.length > 0 && (banners.length > 1 || !banners[0].isExiting);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to actually take care of removing the first banner from our queue.
|
||||||
|
*/
|
||||||
|
async dequeue(): Promise<void> {
|
||||||
|
// Note: Since dequeuing the banner will remove it from the DOM, we don't want to actually dequeue
|
||||||
|
// until the removal animation, which takes 0.7 seconds, is completed.
|
||||||
|
const animationSpeed = 0.8;
|
||||||
|
const dismissDelay = delay(animationSpeed);
|
||||||
|
const banners = get(this, 'banners');
|
||||||
|
|
||||||
|
await dismissDelay;
|
||||||
|
banners.removeAt(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to add the banner to our queue. Takes the content and type of banner and creates a
|
||||||
|
* standardized interface that our servie can understand
|
||||||
|
* @param message - the message to put in the banner's content box
|
||||||
|
* @param {NotificationEvent} type - what type of banner notification we are going for (which will
|
||||||
|
* determine the appearance and interaction properties)
|
||||||
|
*/
|
||||||
|
addBanner(message: string, type: NotificationEvent = NotificationEvent['info']): void {
|
||||||
|
get(this, 'banners').addObject({
|
||||||
|
type,
|
||||||
|
content: message,
|
||||||
|
isExiting: false,
|
||||||
|
isDismissable: isDismissableMap[type],
|
||||||
|
iconName: iconNameMap[type]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,13 +11,21 @@ html {
|
|||||||
* Apply padding to top and bottom to account for navigation and footer
|
* Apply padding to top and bottom to account for navigation and footer
|
||||||
*/
|
*/
|
||||||
body {
|
body {
|
||||||
padding-top: $nav-min-height;
|
|
||||||
padding-bottom: $nav-min-height;
|
|
||||||
background-color: set-color(white, base);
|
background-color: set-color(white, base);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
margin-top: $nav-min-height;
|
||||||
|
margin-bottom: $nav-min-height;
|
||||||
|
transition: margin 0.7s ease;
|
||||||
|
|
||||||
|
&.banner-alert-offset {
|
||||||
|
margin-top: $nav-min-height + 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make all elements from the DOM inherit from the parent box-sizing
|
* Make all elements from the DOM inherit from the parent box-sizing
|
||||||
* Since `*` has a specificity of 0, it does not override the `html` value
|
* Since `*` has a specificity of 0, it does not override the `html` value
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
@import 'navbar';
|
@import 'navbar';
|
||||||
@import 'hero';
|
@import 'hero';
|
||||||
|
@import 'application/all';
|
||||||
@import 'avatar/all';
|
@import 'avatar/all';
|
||||||
@import 'dataset-author/all';
|
@import 'dataset-author/all';
|
||||||
@import 'dataset-compliance/all';
|
@import 'dataset-compliance/all';
|
||||||
|
|||||||
@ -11,6 +11,7 @@ $item-spacing: 10px;
|
|||||||
* Explicitly sets the .navbar min-height to a shared value
|
* Explicitly sets the .navbar min-height to a shared value
|
||||||
*/
|
*/
|
||||||
min-height: $nav-min-height;
|
min-height: $nav-min-height;
|
||||||
|
transition: top 0.7s ease;
|
||||||
|
|
||||||
&-inverse {
|
&-inverse {
|
||||||
/**
|
/**
|
||||||
@ -32,6 +33,10 @@ $item-spacing: 10px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.navbar-top-offset {
|
||||||
|
top: 52px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
@import 'banner-alerts';
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
.banner-alerts {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
transition: height 0.6s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--show {
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hide {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-alert {
|
||||||
|
transition: height 0.6s ease;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
min-width: 1128px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
color: get-color(white);
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--exiting {
|
||||||
|
height: 0;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
background-color: get-color(slate6);
|
||||||
|
border-bottom: 1px solid get-color(slate3, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirm {
|
||||||
|
background-color: get-color(orange5);
|
||||||
|
border-bottom: 1px solid get-color(orange3, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background-color: get-color(red5);
|
||||||
|
border-bottom: 1px solid get-color(red3, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(n + 3) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 90vw;
|
||||||
|
min-width: 1056px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dismiss {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
{{#if session.isAuthenticated}}
|
{{#if session.isAuthenticated}}
|
||||||
|
{{application/banner-alerts}}
|
||||||
|
|
||||||
{{partial "navbar"}}
|
{{partial "navbar"}}
|
||||||
|
|
||||||
|
<div class="app-container {{if banners.isShowingBanners "banner-alert-offset"}}">
|
||||||
{{#hero-container}}
|
{{#hero-container}}
|
||||||
<header class="nacho-hero__header">
|
<header class="nacho-hero__header">
|
||||||
Search for datasets, metrics and flows
|
Search for datasets, metrics and flows
|
||||||
@ -13,6 +16,7 @@
|
|||||||
<section class="container-fluid">
|
<section class="container-fluid">
|
||||||
{{partial "main"}}
|
{{partial "main"}}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{notifications-service service=notifications}}
|
{{notifications-service service=notifications}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
{{#each banners.banners as |banner|}}
|
||||||
|
<div class="banner-alert {{if banner.isExiting "banner-alert--exiting" "banner-alert--active"}} banner-alert--{{banner.type}}">
|
||||||
|
{{fa-icon banner.iconName class="banner-alert__icon"}}
|
||||||
|
<div class="banner-alert__content">
|
||||||
|
{{banner.content}}
|
||||||
|
</div>
|
||||||
|
{{#if banner.isDismissable}}
|
||||||
|
<button {{action "onDismissBanner" banner}} class="banner-alert__dismiss">
|
||||||
|
{{fa-icon "close"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
<nav class="navbar navbar-inverse navbar-fixed-top {{if banners.isShowingBanners "navbar-top-offset"}}" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button class="navbar-toggle collapsed" aria-expanded="false" aria-controls="navbar">
|
<button class="navbar-toggle collapsed" aria-expanded="false" aria-controls="navbar">
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { moduleForComponent, test } from 'ember-qunit';
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
|
const banners = [
|
||||||
|
{
|
||||||
|
content: 'He told ne enough! He told me you killed him!',
|
||||||
|
type: 'info',
|
||||||
|
isExiting: false,
|
||||||
|
isDimissable: true,
|
||||||
|
iconName: 'info-circle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'No, Luke, I am your father',
|
||||||
|
type: 'confirm',
|
||||||
|
isExiting: false,
|
||||||
|
isDimissable: true,
|
||||||
|
iconName: 'exclamation-circle'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Stubbing the banner service to use in the integration test
|
||||||
|
const bannersStub = Service.extend({
|
||||||
|
banners,
|
||||||
|
dequeue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
moduleForComponent('application/banner-alerts', 'Integration | Component | application/banner alerts', {
|
||||||
|
integration: true,
|
||||||
|
beforeEach() {
|
||||||
|
this.register('service:banners', bannersStub);
|
||||||
|
this.inject.service('banners', { as: 'banners' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bannerAlertClass = '.banner-alert';
|
||||||
|
|
||||||
|
test('it renders', function(assert) {
|
||||||
|
this.render(hbs`{{application/banner-alerts}}`);
|
||||||
|
assert.ok(this.$(), 'Renders without errors');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders the correct information', function(assert) {
|
||||||
|
this.render(hbs`{{application/banner-alerts}}`);
|
||||||
|
assert.equal(this.$(bannerAlertClass).length, 2, 'Renders the correct amount of banners');
|
||||||
|
assert.equal(
|
||||||
|
this.$(`${bannerAlertClass}__content:eq(0)`)
|
||||||
|
.text()
|
||||||
|
.trim(),
|
||||||
|
banners[0].content,
|
||||||
|
'Renders the correct text'
|
||||||
|
);
|
||||||
|
assert.equal(this.$(`.fa-${banners[0].iconName}`).length, 1, 'Renders the correct types of banners');
|
||||||
|
});
|
||||||
23
wherehows-web/tests/unit/services/banners-test.js
Normal file
23
wherehows-web/tests/unit/services/banners-test.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { moduleFor, test } from 'ember-qunit';
|
||||||
|
|
||||||
|
moduleFor('service:banners', 'Unit | Service | banners', {});
|
||||||
|
|
||||||
|
test('it exists', function(assert) {
|
||||||
|
const service = this.subject();
|
||||||
|
assert.ok(service, 'Existence is a good start');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it operates correctly', function(assert) {
|
||||||
|
const service = this.subject();
|
||||||
|
const message = 'Ash Ketchum from Pallet Town';
|
||||||
|
|
||||||
|
service.addBanner('Ash Ketchum from Pallet Town', 'info');
|
||||||
|
|
||||||
|
assert.equal(service.banners.length, 1, 'Created a banner');
|
||||||
|
assert.equal(service.banners[0].content, message, 'Creates a banner with the right message');
|
||||||
|
assert.equal(service.banners[0].isDismissable, true, 'Creates a banner with the right dismiss');
|
||||||
|
|
||||||
|
service.dequeue().then(() => {
|
||||||
|
assert.equal(service.banners.length, 0, 'Removes a banner correctly');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user