From 3b4df51848f1ff3b4f22771eacdd49e77077125d Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Fri, 10 Jul 2020 14:58:59 -0700 Subject: [PATCH] style: new toast design closer to SIP-34 (#10178) --- superset-frontend/images/icons/check.svg | 22 ++++ superset-frontend/images/icons/error.svg | 22 ++++ .../components/DashboardBuilder_spec.jsx | 5 + .../components/ToastPresenter_spec.jsx | 4 +- .../messageToasts/components/Toast_spec.jsx | 26 +++-- .../sqllab/TabbedSqlEditors_spec.jsx | 7 ++ superset-frontend/src/components/Icon.tsx | 6 + .../src/messageToasts/components/Toast.jsx | 109 ------------------ .../src/messageToasts/components/Toast.tsx | 93 +++++++++++++++ .../components/ToastPresenter.jsx | 56 --------- .../components/ToastPresenter.tsx | 91 +++++++++++++++ .../src/messageToasts/stylesheets/toast.less | 78 ------------- superset-frontend/src/messageToasts/types.ts | 24 ++++ 13 files changed, 286 insertions(+), 257 deletions(-) create mode 100644 superset-frontend/images/icons/check.svg create mode 100644 superset-frontend/images/icons/error.svg delete mode 100644 superset-frontend/src/messageToasts/components/Toast.jsx create mode 100644 superset-frontend/src/messageToasts/components/Toast.tsx delete mode 100644 superset-frontend/src/messageToasts/components/ToastPresenter.jsx create mode 100644 superset-frontend/src/messageToasts/components/ToastPresenter.tsx delete mode 100644 superset-frontend/src/messageToasts/stylesheets/toast.less create mode 100644 superset-frontend/src/messageToasts/types.ts diff --git a/superset-frontend/images/icons/check.svg b/superset-frontend/images/icons/check.svg new file mode 100644 index 0000000000..7f6fd6eb00 --- /dev/null +++ b/superset-frontend/images/icons/check.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/images/icons/error.svg b/superset-frontend/images/icons/error.svg new file mode 100644 index 0000000000..03c19629e8 --- /dev/null +++ b/superset-frontend/images/icons/error.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index dbdee6a33b..04d885e1d6 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -22,6 +22,7 @@ import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; import { ParentSize } from '@vx/responsive'; +import { supersetTheme, ThemeProvider } from '@superset-ui/style'; import { Sticky, StickyContainer } from 'react-sticky'; import { TabContainer, TabContent, TabPane } from 'react-bootstrap'; @@ -77,6 +78,10 @@ describe('DashboardBuilder', () => { {builder} , + { + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, + }, ) : shallow(builder); } diff --git a/superset-frontend/spec/javascripts/messageToasts/components/ToastPresenter_spec.jsx b/superset-frontend/spec/javascripts/messageToasts/components/ToastPresenter_spec.jsx index d888dad606..9b3e4a0b41 100644 --- a/superset-frontend/spec/javascripts/messageToasts/components/ToastPresenter_spec.jsx +++ b/superset-frontend/spec/javascripts/messageToasts/components/ToastPresenter_spec.jsx @@ -34,9 +34,9 @@ describe('ToastPresenter', () => { return wrapper; } - it('should render a div with class toast-presenter', () => { + it('should render a div with id toast-presenter', () => { const wrapper = setup(); - expect(wrapper.find('.toast-presenter')).toHaveLength(1); + expect(wrapper.find('#toast-presenter')).toHaveLength(1); }); it('should render a Toast for each toast object', () => { diff --git a/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx b/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx index a37b834c98..d4110933bc 100644 --- a/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx +++ b/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx @@ -19,21 +19,19 @@ import { Alert } from 'react-bootstrap'; import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import Toast from 'src/messageToasts/components/Toast'; import mockMessageToasts from '../mockMessageToasts'; +const props = { + toast: mockMessageToasts[0], + onCloseToast() {}, +}; + +const setup = overrideProps => mount(); + describe('Toast', () => { - const props = { - toast: mockMessageToasts[0], - onCloseToast() {}, - }; - - function setup(overrideProps) { - const wrapper = mount(); - return wrapper; - } - it('should render an Alert', () => { const wrapper = setup(); expect(wrapper.find(Alert)).toHaveLength(1); @@ -52,9 +50,13 @@ describe('Toast', () => { expect(id).toBe(props.toast.id); done(); }; + const wrapper = setup({ onCloseToast }); - const handleClosePress = wrapper.instance().handleClosePress; - expect(wrapper.find(Alert).prop('onDismiss')).toBe(handleClosePress); + const handleClosePress = wrapper.find('[label="Close alert"]').props() + .onClick; + + const alertProps = wrapper.find(Alert).props(); + expect(alertProps.onDismiss).toBe(handleClosePress); handleClosePress(); // there is a timeout for onCloseToast to be called }); }); diff --git a/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx b/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx index 2d09f3650d..f7db50836f 100644 --- a/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx @@ -23,6 +23,7 @@ import URI from 'urijs'; import { Tab } from 'react-bootstrap'; import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; +import { supersetTheme, ThemeProvider } from '@superset-ui/style'; import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors'; import SqlEditor from 'src/SqlLab/components/SqlEditor'; @@ -98,6 +99,8 @@ describe('TabbedSqlEditors', () => { uriStub.returns({ id: 1 }); wrapper = mount(, { context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, }); expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe( true, @@ -110,6 +113,8 @@ describe('TabbedSqlEditors', () => { uriStub.returns({ savedQueryId: 1 }); wrapper = mount(, { context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, }); expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe( true, @@ -122,6 +127,8 @@ describe('TabbedSqlEditors', () => { uriStub.returns({ sql: 1, dbid: 1 }); wrapper = mount(, { context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, }); expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe( true, diff --git a/superset-frontend/src/components/Icon.tsx b/superset-frontend/src/components/Icon.tsx index 9040ca767f..e7dfe8da8b 100644 --- a/superset-frontend/src/components/Icon.tsx +++ b/superset-frontend/src/components/Icon.tsx @@ -19,12 +19,14 @@ import React, { SVGProps } from 'react'; import styled from '@superset-ui/style'; import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg'; +import { ReactComponent as CheckIcon } from 'images/icons/check.svg'; import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg'; import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg'; import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg'; import { ReactComponent as CompassIcon } from 'images/icons/compass.svg'; import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg'; import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg'; +import { ReactComponent as ErrorIcon } from 'images/icons/error.svg'; import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg'; import { ReactComponent as SearchIcon } from 'images/icons/search.svg'; import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg'; @@ -35,12 +37,14 @@ import { ReactComponent as WarningIcon } from 'images/icons/warning.svg'; type Icon = | 'cancel-x' + | 'check' | 'checkbox-half' | 'checkbox-off' | 'checkbox-on' | 'compass' | 'dataset-physical' | 'dataset-virtual' + | 'error' | 'pencil' | 'search' | 'sort' @@ -58,7 +62,9 @@ const iconsRegistry: { [key in Icon]: React.ComponentType } = { 'dataset-virtual': DatasetVirtualIcon, 'sort-asc': SortAscIcon, 'sort-desc': SortDescIcon, + check: CheckIcon, compass: CompassIcon, + error: ErrorIcon, pencil: PencilIcon, search: SearchIcon, sort: SortIcon, diff --git a/superset-frontend/src/messageToasts/components/Toast.jsx b/superset-frontend/src/messageToasts/components/Toast.jsx deleted file mode 100644 index bbce2e97be..0000000000 --- a/superset-frontend/src/messageToasts/components/Toast.jsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Alert } from 'react-bootstrap'; -import cx from 'classnames'; -import Interweave from 'interweave'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import { toastShape } from '../propShapes'; - -import { - INFO_TOAST, - SUCCESS_TOAST, - WARNING_TOAST, - DANGER_TOAST, -} from '../constants'; - -const propTypes = { - toast: toastShape.isRequired, - onCloseToast: PropTypes.func.isRequired, -}; - -const defaultProps = {}; - -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 { toast } = this.props; - - setTimeout(this.showToast); - - if (toast.duration > 0) { - this.hideTimer = setTimeout(this.handleClosePress, toast.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 ( - - - - ); - } -} - -Toast.propTypes = propTypes; -Toast.defaultProps = defaultProps; - -export default Toast; diff --git a/superset-frontend/src/messageToasts/components/Toast.tsx b/superset-frontend/src/messageToasts/components/Toast.tsx new file mode 100644 index 0000000000..87fba1388a --- /dev/null +++ b/superset-frontend/src/messageToasts/components/Toast.tsx @@ -0,0 +1,93 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Alert } from 'react-bootstrap'; +import styled from '@superset-ui/style'; +import cx from 'classnames'; +import Interweave from 'interweave'; +import React, { useEffect, useState } from 'react'; +import Icon from 'src/components/Icon'; +import { ToastType } from 'src/messageToasts/types'; + +import { SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../constants'; + +const ToastContianer = styled.div` + display: flex; + justify-content: center; + align-items: center; + + span { + padding: 0 11px; + } +`; + +interface ToastPresenterProps { + toast: { id: string; toastType: ToastType; text: string; duration: number }; + onCloseToast: (id: string) => void; +} + +export default function Toast({ toast, onCloseToast }: ToastPresenterProps) { + let hideTimer: ReturnType; + const [visible, setVisible] = useState(false); + const showToast = () => { + setVisible(true); + }; + + const handleClosePress = () => { + clearTimeout(hideTimer); + // Wait for the transition + setVisible(() => { + setTimeout(() => { + onCloseToast(toast.id); + }, 150); + return false; + }); + }; + + useEffect(() => { + setTimeout(showToast); + + if (toast.duration > 0) { + hideTimer = setTimeout(handleClosePress, toast.duration); + } + return () => { + clearTimeout(hideTimer); + }; + }, []); + + return ( + + + {toast.toastType === SUCCESS_TOAST && } + {toast.toastType === WARNING_TOAST || + (toast.toastType === DANGER_TOAST && )} + + + + ); +} diff --git a/superset-frontend/src/messageToasts/components/ToastPresenter.jsx b/superset-frontend/src/messageToasts/components/ToastPresenter.jsx deleted file mode 100644 index 69fa1449fa..0000000000 --- a/superset-frontend/src/messageToasts/components/ToastPresenter.jsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Toast from './Toast'; -import { toastShape } from '../propShapes'; - -import '../stylesheets/toast.less'; - -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-frontend/src/messageToasts/components/ToastPresenter.tsx b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx new file mode 100644 index 0000000000..545573501a --- /dev/null +++ b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import styled from '@superset-ui/style'; +import { ToastType } from 'src/messageToasts/types'; +import Toast from './Toast'; + +const StyledToastPresenter = styled.div` + max-width: 600px; + position: fixed; + bottom: 0px; + right: -110px; + transform: translate(-50%, 0); + z-index: ${({ theme }) => theme.zIndex.max}; + + .toast { + background: ${({ theme }) => theme.colors.grayscale.dark1}; + border-radius: ${({ theme }) => theme.borderRadius}; + box-shadow: 0 2px 4px 0 + fade( + ${({ theme }) => theme.colors.grayscale.dark2}, + ${({ theme }) => theme.opacity.mediumLight} + ); + color: ${({ theme }) => theme.colors.grayscale.light5}; + opacity: 0; + position: relative; + transform: translateY(-100%); + white-space: pre-line; + will-change: transform, opacity; + transition: transform ${({ theme }) => theme.transitionTiming}s, + opacity ${({ theme }) => theme.transitionTiming}s; + + &:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 6px; + height: 100%; + } + } + + .toast > button { + color: ${({ theme }) => theme.colors.grayscale.light5}; + opacity: 1; + } + + .toast--visible { + opacity: 1; + transform: translateY(0); + } +`; + +type ToastShape = { + id: string; + toastType: ToastType; + text: string; + duration: number; +}; + +interface ToastPresenterProps { + toasts: Array; + removeToast: () => void; +} + +const ToastPresenter = ({ toasts, removeToast }: ToastPresenterProps) => + toasts.length > 0 && ( + + {toasts.map(toast => ( + + ))} + + ); + +export default ToastPresenter; diff --git a/superset-frontend/src/messageToasts/stylesheets/toast.less b/superset-frontend/src/messageToasts/stylesheets/toast.less deleted file mode 100644 index 7221e9fc20..0000000000 --- a/superset-frontend/src/messageToasts/stylesheets/toast.less +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -@import '../../../stylesheets/less/variables.less'; - -.toast-presenter { - position: fixed; - bottom: 16px; - left: 50%; - transform: translate(-50%, 0); - width: 600px; - z-index: @z-index-max; -} - -.toast { - background: @lightest; - color: @almost-black; - opacity: 0; - position: relative; - white-space: pre-line; - box-shadow: 0 2px 4px 0 fade(@darkest, @opacity-medium-light); - will-change: transform, opacity; - transform: translateY(-100%); - transition: transform @timing-normal, opacity @timing-normal; - - &:after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 6px; - height: 100%; - } -} - -.toast > button { - color: @almost-black; - - &:hover { - color: @gray-dark; - } -} - -.toast--visible { - transform: translateY(0); - opacity: 1; -} - -.toast--info:after { - background: @info; -} - -.toast--success:after { - background: @success; -} - -.toast--warning:after { - background: @warning; -} - -.toast--danger:after { - background: @danger; -} diff --git a/superset-frontend/src/messageToasts/types.ts b/superset-frontend/src/messageToasts/types.ts new file mode 100644 index 0000000000..471a3a0ac6 --- /dev/null +++ b/superset-frontend/src/messageToasts/types.ts @@ -0,0 +1,24 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type ToastType = + | 'INFO_TOAST' + | 'SUCCESS_TOAST' + | 'WARNING_TOAST' + | 'DANGER_TOAST';