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.