From commits-return-848-archive-asf-public=cust-asf.ponee.io@superset.incubator.apache.org Wed Apr 4 20:40:23 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id C526118064F for ; Wed, 4 Apr 2018 20:40:22 +0200 (CEST) Received: (qmail 47945 invoked by uid 500); 4 Apr 2018 18:40:21 -0000 Mailing-List: contact commits-help@superset.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@superset.incubator.apache.org Delivered-To: mailing list commits@superset.incubator.apache.org Received: (qmail 47927 invoked by uid 99); 4 Apr 2018 18:40:21 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 04 Apr 2018 18:40:21 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 5165C808EF; Wed, 4 Apr 2018 18:40:21 +0000 (UTC) Date: Wed, 04 Apr 2018 18:40:22 +0000 To: "commits@superset.apache.org" Subject: [incubator-superset] 02/02: [toasts] add Toast component, ToastPresenter container and component, and toast redux actions + reducers MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: ccwilliams@apache.org In-Reply-To: <152286722090.6132.7760727316490539135@gitbox.apache.org> References: <152286722090.6132.7760727316490539135@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: incubator-superset X-Git-Refname: refs/heads/chris--grid-root-and-spacer X-Git-Reftype: branch X-Git-Rev: 3ffe470206d5bfed26e60c07418d097a31b3e3ef X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20180404184021.5165C808EF@gitbox.apache.org> 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 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 && } + ); } 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 ( + + {text} + + ); + } +} + +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 && +
+ {toasts.map(toast => ( + + ))} +
+ ); + } +} + +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.