mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-29 17:59:24 +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(),
|
||||
|
||||
/**
|
||||
* Adds the service for banners in order to trigger the application to render the banners when
|
||||
* they are triggered
|
||||
*/
|
||||
banners: service(),
|
||||
|
||||
init() {
|
||||
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
|
||||
*/
|
||||
body {
|
||||
padding-top: $nav-min-height;
|
||||
padding-bottom: $nav-min-height;
|
||||
background-color: set-color(white, base);
|
||||
overflow-y: scroll;
|
||||
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
|
||||
* Since `*` has a specificity of 0, it does not override the `html` value
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
@import 'navbar';
|
||||
@import 'hero';
|
||||
@import 'application/all';
|
||||
@import 'avatar/all';
|
||||
@import 'dataset-author/all';
|
||||
@import 'dataset-compliance/all';
|
||||
|
||||
@ -11,6 +11,7 @@ $item-spacing: 10px;
|
||||
* Explicitly sets the .navbar min-height to a shared value
|
||||
*/
|
||||
min-height: $nav-min-height;
|
||||
transition: top 0.7s ease;
|
||||
|
||||
&-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,18 +1,22 @@
|
||||
{{#if session.isAuthenticated}}
|
||||
{{application/banner-alerts}}
|
||||
|
||||
{{partial "navbar"}}
|
||||
|
||||
{{#hero-container}}
|
||||
<header class="nacho-hero__header">
|
||||
Search for datasets, metrics and flows
|
||||
</header>
|
||||
<section class="nacho-hero__content">
|
||||
{{search-bar-form didSearch=(action "didSearch")}}
|
||||
</section>
|
||||
{{/hero-container}}
|
||||
<div class="app-container {{if banners.isShowingBanners "banner-alert-offset"}}">
|
||||
{{#hero-container}}
|
||||
<header class="nacho-hero__header">
|
||||
Search for datasets, metrics and flows
|
||||
</header>
|
||||
<section class="nacho-hero__content">
|
||||
{{search-bar-form didSearch=(action "didSearch")}}
|
||||
</section>
|
||||
{{/hero-container}}
|
||||
|
||||
<section class="container-fluid">
|
||||
{{partial "main"}}
|
||||
</section>
|
||||
<section class="container-fluid">
|
||||
{{partial "main"}}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{{notifications-service service=notifications}}
|
||||
{{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="navbar-header">
|
||||
<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