You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/04/04 18:40:22 UTC
[incubator-superset] 02/02: [toasts] add Toast component,
ToastPresenter container and component, and toast redux actions + reducers
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch chris--grid-root-and-spacer
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 3ffe470206d5bfed26e60c07418d097a31b3e3ef
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Wed Apr 4 11:39:56 2018 -0700
[toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers
---
superset/assets/javascripts/dashboard/index.jsx | 7 ++
.../dashboard/v2/actions/messageToasts.js | 47 ++++++++++++
.../dashboard/v2/components/DashboardBuilder.jsx | 2 +
.../javascripts/dashboard/v2/components/Toast.jsx | 87 ++++++++++++++++++++++
.../dashboard/v2/components/ToastPresenter.jsx | 39 ++++++++++
.../dashboard/v2/containers/ToastPresenter.jsx | 10 +++
.../javascripts/dashboard/v2/reducers/index.js | 2 +
.../dashboard/v2/reducers/messageToasts.js | 18 +++++
.../v2/stylesheets/components/divider.less | 2 +-
.../v2/stylesheets/components/header.less | 4 +-
.../dashboard/v2/stylesheets/index.less | 1 +
.../dashboard/v2/stylesheets/toast.less | 61 +++++++++++++++
.../dashboard/v2/stylesheets/variables.less | 8 ++
.../javascripts/dashboard/v2/util/constants.js | 6 ++
.../javascripts/dashboard/v2/util/propShapes.jsx | 7 ++
15 files changed, 299 insertions(+), 2 deletions(-)
diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx
index f7471f5..6f6d177 100644
--- a/superset/assets/javascripts/dashboard/index.jsx
+++ b/superset/assets/javascripts/dashboard/index.jsx
@@ -19,6 +19,7 @@ initJQueryAjax();
const appContainer = document.getElementById('app');
// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
// const initState = Object.assign({}, getInitialState(bootstrapData));
+
const initState = {
dashboardLayout: {
past: [],
@@ -26,6 +27,12 @@ const initState = {
future: [],
},
editMode: true,
+ messageToasts: [
+ { text: 'Info!', id: '157234', toastType: 'INFO_TOAST' },
+ { text: 'Success!', id: '1237545745', toastType: 'SUCCESS_TOAST' },
+ { text: 'Warning!', id: '154623', toastType: 'WARNING_TOAST' },
+ { text: 'Danger!', id: '9128346', toastType: 'DANGER_TOAST' },
+ ],
};
const store = createStore(
diff --git a/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
new file mode 100644
index 0000000..c49c94a
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/actions/messageToasts.js
@@ -0,0 +1,47 @@
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+function getToastUuid(type) {
+ return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
+}
+
+export const ADD_TOAST = 'ADD_TOAST';
+export function addToast({ toastType, text }) {
+ return {
+ type: ADD_TOAST,
+ payload: {
+ id: getToastUuid(toastType),
+ toastType,
+ text,
+ },
+ };
+}
+
+export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
+export function addInfoToast(text) {
+ return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
+}
+
+export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
+export function addSuccessToast(text) {
+ return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
+}
+
+export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
+export function addWarningToast(text) {
+ return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
+}
+
+export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
+export function addDangerToast(text) {
+ return dispatch => dispatch(addToast({ text, toastType: DANGER_TOAST }));
+}
+
+export const REMOVE_TOAST = 'REMOVE_TOAST';
+export function removeToast(id) {
+ return {
+ type: REMOVE_TOAST,
+ payload: {
+ id,
+ },
+ };
+}
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index fc938b1..8e2d985 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -10,6 +10,7 @@ import DashboardGrid from '../containers/DashboardGrid';
import IconButton from './IconButton';
import DragDroppable from './dnd/DragDroppable';
import DashboardComponent from '../containers/DashboardComponent';
+import ToastPresenter from '../containers/ToastPresenter';
import WithPopoverMenu from './menu/WithPopoverMenu';
import {
@@ -114,6 +115,7 @@ class DashboardBuilder extends React.Component {
/>
{editMode && <BuilderComponentPane />}
</div>
+ <ToastPresenter />
</div>
);
}
diff --git a/superset/assets/javascripts/dashboard/v2/components/Toast.jsx b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
new file mode 100644
index 0000000..537388d
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/Toast.jsx
@@ -0,0 +1,87 @@
+import { Alert } from 'react-bootstrap';
+import cx from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { toastShape } from '../util/propShapes';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+
+const propTypes = {
+ toast: toastShape.isRequired,
+ onCloseToast: PropTypes.func.isRequired,
+ delay: PropTypes.number,
+ duration: PropTypes.number, // if duration is >0, the toast will close on its own
+};
+
+const defaultProps = {
+ delay: 0,
+ duration: 0,
+};
+
+class Toast extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ visible: false,
+ };
+
+ this.showToast = this.showToast.bind(this);
+ this.handleClosePress = this.handleClosePress.bind(this);
+ }
+
+ componentDidMount() {
+ const { delay, duration } = this.props;
+
+ setTimeout(this.showToast, delay);
+
+ if (duration > 0) {
+ this.hideTimer = setTimeout(this.handleClosePress, delay + duration);
+ }
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.hideTimer);
+ }
+
+ showToast() {
+ this.setState({ visible: true });
+ }
+
+ handleClosePress() {
+ clearTimeout(this.hideTimer);
+
+ this.setState({ visible: false }, () => {
+ // Wait for the transition
+ setTimeout(() => {
+ this.props.onCloseToast(this.props.toast.id);
+ }, 150);
+ });
+ }
+
+ render() {
+ const { visible } = this.state;
+ const { toast: { toastType, text } } = this.props;
+
+ return (
+ <Alert
+ onDismiss={this.handleClosePress}
+ bsClass={cx(
+ 'alert',
+ 'toast',
+ visible && 'toast--visible',
+ toastType === INFO_TOAST && 'toast--info',
+ toastType === SUCCESS_TOAST && 'toast--success',
+ toastType === WARNING_TOAST && 'toast--warning',
+ toastType === DANGER_TOAST && 'toast--danger',
+ )}
+ >
+ {text}
+ </Alert>
+ );
+ }
+}
+
+Toast.propTypes = propTypes;
+Toast.defaultProps = defaultProps;
+
+export default Toast;
diff --git a/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
new file mode 100644
index 0000000..95a0251
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/ToastPresenter.jsx
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Toast from './Toast';
+import { toastShape } from '../util/propShapes';
+
+const propTypes = {
+ toasts: PropTypes.arrayOf(toastShape),
+ removeToast: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ toasts: [],
+};
+
+// eslint-disable-next-line react/prefer-stateless-function
+class ToastPresenter extends React.Component {
+ render() {
+ const { toasts, removeToast } = this.props;
+
+ return (
+ toasts.length > 0 &&
+ <div className="toast-presenter">
+ {toasts.map(toast => (
+ <Toast
+ key={toast.id}
+ toast={toast}
+ onCloseToast={removeToast}
+ />
+ ))}
+ </div>
+ );
+ }
+}
+
+ToastPresenter.propTypes = propTypes;
+ToastPresenter.defaultProps = defaultProps;
+
+export default ToastPresenter;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
new file mode 100644
index 0000000..7e70abc
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/ToastPresenter.jsx
@@ -0,0 +1,10 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import ToastPresenter from '../components/ToastPresenter';
+
+import { removeToast } from '../actions/messageToasts';
+
+export default connect(
+ ({ messageToasts: toasts }) => ({ toasts }),
+ dispatch => bindActionCreators({ removeToast }, dispatch),
+)(ToastPresenter);
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index b824e9a..731734d 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -3,6 +3,7 @@ import undoable, { distinctState } from 'redux-undo';
import dashboardLayout from './dashboardLayout';
import editMode from './editMode';
+import messageToasts from './messageToasts';
const undoableLayout = undoable(dashboardLayout, {
limit: 15,
@@ -12,4 +13,5 @@ const undoableLayout = undoable(dashboardLayout, {
export default combineReducers({
dashboardLayout: undoableLayout,
editMode,
+ messageToasts,
});
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
new file mode 100644
index 0000000..3b40da4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/reducers/messageToasts.js
@@ -0,0 +1,18 @@
+import { ADD_TOAST, REMOVE_TOAST } from '../actions/messageToasts';
+
+export default function messageToastsReducer(toasts = [], action) {
+ switch (action.type) {
+ case ADD_TOAST: {
+ const { payload: { id, type, text } } = action;
+ return [...toasts, { id, type, text }];
+ }
+
+ case REMOVE_TOAST: {
+ const { payload: { id } } = action;
+ return [...toasts].filter(toast => toast.id !== id);
+ }
+
+ default:
+ return toasts;
+ }
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
index 9347a4e..e4625d3 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/divider.less
@@ -1,6 +1,6 @@
.dashboard-component-divider {
width: 100%;
- padding: 16px 0; /* this is padding not margin to enable a larger mouse target */
+ padding: 8px 0; /* this is padding not margin to enable a larger mouse target */
background-color: transparent;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
index 670155d..37c7598 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/header.less
@@ -36,9 +36,11 @@
padding-right: 16px;
}
-/* grids add margin between items, so don't double pad within columns
+/*
+ * grids add margin between items, so don't double pad within columns
* we'll not worry about double padding on top as it can serve as a visual separator
*/
+// .grid-content > :not(:only-child):not(:last-child) .dashboard-component-header,
.grid-column > :not(:only-child):not(:last-child) .dashboard-component-header {
margin-bottom: -16px;
}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
index d2a41a8..49ff5da 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/index.less
@@ -8,3 +8,4 @@
@import './popover-menu.less';
@import './resizable.less';
@import './components/index.less';
+@import './toast.less';
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
new file mode 100644
index 0000000..b324137
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/toast.less
@@ -0,0 +1,61 @@
+.toast-presenter {
+ position: fixed;
+ bottom: 16px;
+ left: 50%;
+ transform: translate(-50%, 0);
+ width: 500px;
+ z-index: 3000; // top of the world
+}
+
+.toast {
+ will-change: transform, opacity;
+ transform: translateY(-100%);
+ transition: transform .3s, opacity .3s;
+ opacity: 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
+}
+
+.toast {
+ border-radius: 2px;
+ background: white;
+ color: @almost-black;
+}
+
+.toast > button {
+ color: @almost-black;
+}
+
+.toast > button:hover {
+ color: @gray-dark;
+}
+
+.toast--visible {
+ transform: translateY(0);
+ opacity: 1;
+}
+
+.toast:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 4px;
+ height: 100%;
+}
+
+.toast--info:after {
+ background: linear-gradient(to bottom, @pink, @purple);
+}
+
+.toast--success:after {
+ background: @success;
+}
+
+.toast--warning:after {
+ background: @warning;
+}
+
+.toast--danger:after {
+ background: @danger;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
index f3a61df..254af23 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/variables.less
@@ -5,3 +5,11 @@
@gray: #879399;
@gray-light: #CFD8DC;
@gray-bg: #f5f5f5;
+
+/* toasts */
+@pink: #E32364;
+@purple: #2C2261;
+
+@success: #00BFA5;
+@warning: #FFAB00;
+@danger: @pink;
diff --git a/superset/assets/javascripts/dashboard/v2/util/constants.js b/superset/assets/javascripts/dashboard/v2/util/constants.js
index 5dd6968..36ef71b 100644
--- a/superset/assets/javascripts/dashboard/v2/util/constants.js
+++ b/superset/assets/javascripts/dashboard/v2/util/constants.js
@@ -31,3 +31,9 @@ export const LARGE_HEADER = 'LARGE_HEADER';
// Style types
export const BACKGROUND_WHITE = 'BACKGROUND_WHITE';
export const BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT';
+
+// Toast types
+export const INFO_TOAST = 'INFO_TOAST';
+export const SUCCESS_TOAST = 'SUCCESS_TOAST';
+export const WARNING_TOAST = 'WARNING_TOAST';
+export const DANGER_TOAST = 'DANGER_TOAST';
diff --git a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
index d701cc2..8acc192 100644
--- a/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/javascripts/dashboard/v2/util/propShapes.jsx
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import componentTypes from './componentTypes';
import backgroundStyleOptions from './backgroundStyleOptions';
import headerStyleOptions from './headerStyleOptions';
+import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from './constants';
export const componentShape = PropTypes.shape({ // eslint-disable-line
id: PropTypes.string.isRequired,
@@ -22,3 +23,9 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
background: PropTypes.oneOf(backgroundStyleOptions.map(opt => opt.value)),
}),
});
+
+export const toastShape = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
+ text: PropTypes.string.isRequired,
+});
--
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.