Skip to content

Commit

Permalink
Add basic mechanism for surfacing toasts.
Browse files Browse the repository at this point in the history
  • Loading branch information
cjcenizal committed Dec 22, 2017
1 parent 85dcb8f commit 16439a7
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/ui/public/chrome/directives/kbn_chrome.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@

<div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
<div class="app-wrapper-panel">
<kbn-notifications list="notifList"></kbn-notifications>
<kbn-notifications
list="notifList"
></kbn-notifications>

<global-toast-list
toasts="toastNotifications"
dismiss-toast="dismissToast"
></global-toast-list>

<kbn-loading-indicator></kbn-loading-indicator>

<div
class="application"
ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
Expand Down
6 changes: 5 additions & 1 deletion src/ui/public/chrome/directives/kbn_chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getUnhashableStatesProvider,
unhashUrl,
} from 'ui/state_management/state_hashing';
import { notify } from 'ui/notify';
import { notify, toastNotifications } from 'ui/notify';
import { SubUrlRouteFilterProvider } from './sub_url_route_filter';

export function kbnChromeProvider(chrome, internals) {
Expand Down Expand Up @@ -66,7 +66,11 @@ export function kbnChromeProvider(chrome, internals) {

// and some local values
chrome.httpActive = $http.pendingRequests;

// Notifications
$scope.notifList = notify._notifs;
$scope.toastNotifications = toastNotifications.list;
$scope.dismissToast = toastNotifications.remove;

return chrome;
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/public/notify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { notify } from './notify';
export { Notifier } from './notifier';
export { fatalError } from './fatal_error';
export { addFatalErrorCallback } from './fatal_error';
export { toastNotifications } from './toasts';
121 changes: 121 additions & 0 deletions src/ui/public/notify/toasts/global_toast_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, {
// cloneElement,
Component,
} from 'react';
import PropTypes from 'prop-types';
import 'ngreact';
import { uiModules } from 'ui/modules';

import {
EuiGlobalToastList,
EuiGlobalToastListItem,
EuiLink,
EuiToast,
} from '@elastic/eui';

const TOAST_LIFE_TIME_MS = 4000;
const TOAST_FADE_OUT_MS = 250;

class GlobalToastList extends Component {
constructor(props) {
super(props);

this.state = {
toastIdToDismissedMap: {}
};

this.timeoutIds = [];
this.toastsScheduledForDismissal = {};
}

static propTypes = {
toasts: PropTypes.array,
dismissToast: PropTypes.func.isRequired,
};

static defaultProps = {
toasts: [],
};

scheduleToastForDismissal = (toast, isImmediate = false) => {
const lifeTime = isImmediate ? TOAST_FADE_OUT_MS : TOAST_LIFE_TIME_MS;

this.timeoutIds.push(setTimeout(() => {
this.props.dismissToast(toast);
this.setState(prevState => {
const toastIdToDismissedMap = { ...this.state.toastIdToDismissedMap };
delete toastIdToDismissedMap[toast.id];

return {
toastIdToDismissedMap,
};
});
}, lifeTime));

this.timeoutIds.push(setTimeout(() => {
this.startDismissingToast(toast);
}, lifeTime - TOAST_FADE_OUT_MS));
};

startDismissingToast(toast) {
this.setState(prevState => {
const toastIdToDismissedMap = { ...this.state.toastIdToDismissedMap };
toastIdToDismissedMap[toast.id] = true;

return {
toastIdToDismissedMap,
};
});
}

componentWillUnmount() {
this.timeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
}

componentDidUpdate(prevProps) {
prevProps.toasts.forEach(toast => {
if (!this.toastsScheduledForDismissal[toast.id]) {
this.scheduleToastForDismissal(toast);
}
});
}

render() {
const {
toasts,
dismissToast,
} = this.props;

const renderedToasts = toasts.map(toast => {
const { text, ...rest } = toast;
return (
<EuiGlobalToastListItem
key={toast.id}
isDismissed={this.state.toastIdToDismissedMap[toast.id]}
>
<EuiToast
onClose={this.scheduleToastForDismissal.bind(toast, true)}
{...rest}
>
{text}
</EuiToast>
</EuiGlobalToastListItem>
);
});

return (
<EuiGlobalToastList>
{renderedToasts}
</EuiGlobalToastList>
);
}
}

const app = uiModules.get('app/kibana', ['react']);

app.directive('globalToastList', function (reactDirective) {
return reactDirective(GlobalToastList, [
'toasts',
['dismissToast', { watchDepth: 'reference' }],
]);
});
2 changes: 2 additions & 0 deletions src/ui/public/notify/toasts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './global_toast_list';
export { toastNotifications } from './toast_notifications';
21 changes: 21 additions & 0 deletions src/ui/public/notify/toasts/toast_notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
let toastCounter = 0;

class ToastNotifications {
constructor() {
this.list = [];
}

add = toast => {
this.list.push({
id: toastCounter++,
...toast
});
};

remove = toast => {
const index = this.list.indexOf(toast);
this.list.splice(index, 1);
};
}

export const toastNotifications = new ToastNotifications();
2 changes: 2 additions & 0 deletions src/ui/public/react_components.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
import { uiModules } from 'ui/modules';

const app = uiModules.get('app/kibana', ['react']);

app.directive('toolBarSearchBox', function (reactDirective) {
return reactDirective(KuiToolBarSearchBox);
});

app.directive('confirmModal', function (reactDirective) {
return reactDirective(KuiConfirmModal);
});

0 comments on commit 16439a7

Please sign in to comment.