diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index 0558269f115a4..469709528089a 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -188,6 +188,7 @@ module.exports = { ], 'jest/consistent-test-it': 'error', 'no-only-tests/no-only-tests': 'error', + '@typescript-eslint/no-non-null-assertion': 0, }, }, ], diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts index 64bb8fdb62a68..e8c1542300113 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts @@ -29,7 +29,7 @@ describe('Visualization > Line', () => { it('should show validator error when no metric', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [] }; cy.visitChartByParams(JSON.stringify(formData)); - cy.get('.alert-warning').contains(`"Metrics" cannot be empty`); + cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`); }); it('should preload mathjs', () => { @@ -43,7 +43,7 @@ describe('Visualization > Line', () => { it('should not show validator error when metric added', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [] }; cy.visitChartByParams(JSON.stringify(formData)); - cy.get('.alert-warning').contains(`"Metrics" cannot be empty`); + cy.get('.ant-alert-warning').contains(`"Metrics" cannot be empty`); cy.get('.text-danger').contains('Metrics'); cy.get('[data-test=metrics]') @@ -58,14 +58,14 @@ describe('Visualization > Line', () => { cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click(); cy.get('.text-danger').should('not.exist'); - cy.get('.alert-warning').should('not.exist'); + cy.get('.ant-alert-warning').should('not.exist'); }); it('should allow negative values in Y bounds', () => { cy.get('#controlSections-tab-display').click(); cy.get('span').contains('Y Axis Bounds').scrollIntoView(); cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 }); - cy.get('.alert-warning').should('not.exist'); + cy.get('.ant-alert-warning').should('not.exist'); }); it('should allow type to search color schemes', () => { diff --git a/superset-frontend/spec/helpers/theming.ts b/superset-frontend/spec/helpers/theming.ts index 2ec019b70d46a..9815e34899494 100644 --- a/superset-frontend/spec/helpers/theming.ts +++ b/superset-frontend/spec/helpers/theming.ts @@ -52,5 +52,5 @@ export function styledShallow( theme: supersetTheme, ...options?.wrappingComponentProps, }, - }).dive(); + }); } diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index 866b8a35aca79..5af36f345eb4d 100644 --- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx +++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx @@ -296,11 +296,7 @@ describe('ListView', () => { }); it('allows disabling bulkSelect', () => { - wrapper - .find('[data-test="bulk-select-controls"]') - .at(0) - .props() - .onDismiss(); + wrapper.find('[data-test="bulk-select-controls"]').at(0).props().onClose(); expect(mockedProps.disableBulkSelect).toHaveBeenCalled(); }); diff --git a/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx index 7a2c6c7b76be3..fd6952ba1c74c 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx @@ -21,7 +21,7 @@ import { mount } from 'enzyme'; import ModalTrigger from 'src/components/ModalTrigger'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; const getMountWrapper = props => diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx index 69082c5558c15..c871a6a7be123 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/NativeFiltersModal_spec.tsx @@ -21,8 +21,8 @@ import { styledMount as mount } from 'spec/helpers/theming'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; import { Provider } from 'react-redux'; -import Alert from 'react-bootstrap/lib/Alert'; import { FilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterConfigModal/FilterConfigModal'; +import Alert from 'src/components/Alert'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import { mockStore } from 'spec/fixtures/mockStore'; diff --git a/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx b/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx index fb61656fdea08..fa5cae1f82149 100644 --- a/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx +++ b/superset-frontend/spec/javascripts/messageToasts/components/Toast_spec.jsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { Alert } from 'react-bootstrap'; import React from 'react'; import { mount } from 'enzyme'; import Toast from 'src/messageToasts/components/Toast'; @@ -31,20 +30,19 @@ const props = { const setup = overrideProps => mount(); describe('Toast', () => { - it('should render an Alert', () => { + it('should render', () => { const wrapper = setup(); - expect(wrapper.find(Alert)).toExist(); + expect(wrapper.find('[data-test="toast-container"]')).toExist(); }); - it('should render toastText within the alert', () => { + it('should render toastText within the div', () => { const wrapper = setup(); - const alert = wrapper.find(Alert); - - expect(alert.childAt(0).childAt(1).text()).toBe(props.toast.text); + const container = wrapper.find('[data-test="toast-container"]'); + expect(container.hostNodes().childAt(1).text()).toBe(props.toast.text); }); - it('should call onCloseToast upon alert dismissal', async () => { - await act( + it('should call onCloseToast upon toast dismissal', async () => + act( () => new Promise(done => { const onCloseToast = id => { @@ -53,13 +51,7 @@ describe('Toast', () => { }; const wrapper = setup({ onCloseToast }); - 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 + wrapper.find('[data-test="close-button"]').props().onClick(); }), - ); - }); + )); }); diff --git a/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx b/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx index a55559a291046..52f3566f70c76 100644 --- a/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx @@ -18,10 +18,13 @@ */ import React from 'react'; import { shallow } from 'enzyme'; +import { styledMount } from 'spec/helpers/theming'; +import { Provider } from 'react-redux'; import sinon from 'sinon'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import ProgressBar from 'src/common/components/ProgressBar'; - +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import FilterableTable from 'src/components/FilterableTable/FilterableTable'; import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton'; import ResultSet from 'src/SqlLab/components/ResultSet'; @@ -33,8 +36,12 @@ import { queries, runningQuery, stoppedQuery, + initialState, } from './fixtures'; +const mockStore = configureStore([thunk]); +const store = mockStore(initialState); + describe('ResultSet', () => { const clearQuerySpy = sinon.spy(); const fetchQuerySpy = sinon.spy(); @@ -105,17 +112,18 @@ describe('ResultSet', () => { expect(wrapper.find(ExploreResultsButton)).toExist(); }); it('should render empty results', () => { - const wrapper = shallow(); - const emptyResults = { - ...queries[0], - results: { - data: [], - }, + const props = { + ...mockedProps, + query: { ...mockedProps.query, results: { data: [] } }, }; - wrapper.setProps({ query: emptyResults }); + const wrapper = styledMount( + + + , + ); expect(wrapper.find(FilterableTable)).not.toExist(); expect(wrapper.find(Alert)).toExist(); - expect(wrapper.find(Alert).shallow().text()).toBe( + expect(wrapper.find(Alert).render().text()).toBe( 'The query returned no data', ); }); diff --git a/superset-frontend/spec/javascripts/sqllab/SouthPane_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SouthPane_spec.jsx index 02184863b81b0..b7c34c0aa69c2 100644 --- a/superset-frontend/spec/javascripts/sqllab/SouthPane_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/SouthPane_spec.jsx @@ -81,12 +81,12 @@ describe('SouthPane', () => { let wrapper; it('should render offline when the state is offline', () => { - wrapper = getWrapper(); + wrapper = getWrapper().dive(); wrapper.setProps({ offline: true }); expect(wrapper.childAt(0).text()).toBe(STATUS_OPTIONS.offline); }); it('should pass latest query down to ResultSet component', () => { - wrapper = getWrapper(); + wrapper = getWrapper().dive(); expect(wrapper.find(ResultSet)).toExist(); expect(wrapper.find(ResultSet).props().query.id).toEqual( mockedProps.latestQueryId, diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton.jsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton.jsx index d5424cdfba137..8fa3bd06badc3 100644 --- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton.jsx +++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton.jsx @@ -18,7 +18,7 @@ */ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { t } from '@superset-ui/core'; import TableView from 'src/components/TableView'; @@ -61,9 +61,11 @@ const EstimateQueryCostButton = props => { const renderModalBody = () => { if (props.queryCostEstimate.error !== null) { return ( - - {props.queryCostEstimate.error} - + ); } if (props.queryCostEstimate.completed) { diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx index fc8c8ac017d60..1c94c8b015cda 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx @@ -21,7 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { t } from '@superset-ui/core'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import shortid from 'shortid'; @@ -102,25 +102,32 @@ class ExploreResultsButton extends React.PureComponent { renderTimeoutWarning() { return ( - - {t( - 'This query took %s seconds to run, ', - Math.round(this.getQueryDuration()), - ) + - t( - 'and the explore view times out at %s seconds ', - this.props.timeout, - ) + - t( - 'following this flow will most likely lead to your query timing out. ', - ) + - t( - 'We recommend your summarize your data further before following that flow. ', - ) + - t('If activated you can use the ')} - CREATE TABLE AS - {t('feature to store a summarized data set that you can then explore.')} - + + {t( + 'This query took %s seconds to run, ', + Math.round(this.getQueryDuration()), + ) + + t( + 'and the explore view times out at %s seconds ', + this.props.timeout, + ) + + t( + 'following this flow will most likely lead to your query timing out. ', + ) + + t( + 'We recommend your summarize your data further before following that flow. ', + ) + + t('If activated you can use the ')} + CREATE TABLE AS + {t( + 'feature to store a summarized data set that you can then explore.', + )} + + } + /> ); } diff --git a/superset-frontend/src/SqlLab/components/QueryHistory.jsx b/superset-frontend/src/SqlLab/components/QueryHistory.jsx index 59c1e72d9c3ef..e00dd2a9849d2 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory.jsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory.jsx @@ -18,7 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { t } from '@superset-ui/core'; import QueryTable from './QueryTable'; @@ -49,7 +49,7 @@ const QueryHistory = props => { /> ); } - return {t('No query history yet...')}; + return ; }; QueryHistory.propTypes = propTypes; diff --git a/superset-frontend/src/SqlLab/components/ResultSet.tsx b/superset-frontend/src/SqlLab/components/ResultSet.tsx index 7daae5f715207..00ab89508873a 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet.tsx @@ -17,8 +17,8 @@ * under the License. */ import React, { CSSProperties } from 'react'; -import { Alert } from 'react-bootstrap'; import ButtonGroup from 'src/components/ButtonGroup'; +import Alert from 'src/components/Alert'; import ProgressBar from 'src/common/components/ProgressBar'; import moment from 'moment'; import { RadioChangeEvent } from 'antd/lib/radio'; @@ -493,7 +493,7 @@ export default class ResultSet extends React.PureComponent< } if (query.state === 'stopped') { - return Query was stopped; + return ; } if (query.state === 'failed') { return ( @@ -517,31 +517,36 @@ export default class ResultSet extends React.PureComponent< } return (
- - {t(object)} [ - - {tempSchema ? `${tempSchema}.` : ''} - {tempTable} - - ] {t('was created')}   - - - - - + + {t(object)} [ + + {tempSchema ? `${tempSchema}.` : ''} + {tempTable} + + ] {t('was created')}   + + + + + + } + />
); } @@ -573,7 +578,7 @@ export default class ResultSet extends React.PureComponent< } if (data && data.length === 0) { return ( - {t('The query returned no data')} + ); } } @@ -637,7 +642,7 @@ export default class ResultSet extends React.PureComponent<
{!progressBar && }
- {progressMsg && {progressMsg}} + {progressMsg && }
{progressBar}
{trackingUrl}
diff --git a/superset-frontend/src/SqlLab/components/SouthPane.jsx b/superset-frontend/src/SqlLab/components/SouthPane.jsx index e2ffa0ad1e9d0..b0182eddd5879 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane.jsx +++ b/superset-frontend/src/SqlLab/components/SouthPane.jsx @@ -19,7 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import shortid from 'shortid'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import Tabs from 'src/common/components/Tabs'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -120,9 +120,12 @@ export class SouthPane extends React.PureComponent { !latestQuery.results ) { results = ( - - {t('No stored results found, you need to re-run your query')} - + ); } else if ( Date.now() - latestQuery.startDttm <= @@ -142,7 +145,7 @@ export class SouthPane extends React.PureComponent { } } else { results = ( - {t('Run a query to display results here')} + ); } const dataPreviewTabs = props.dataPreviewQueries.map(query => ( diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx index 3f1596cc45d61..520378a391251 100644 --- a/superset-frontend/src/chart/Chart.jsx +++ b/superset-frontend/src/chart/Chart.jsx @@ -18,7 +18,7 @@ */ import PropTypes from 'prop-types'; import React from 'react'; -import { Alert } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { styled, logging } from '@superset-ui/core'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; @@ -193,9 +193,11 @@ class Chart extends React.PureComponent { } if (errorMessage) { return ( - - {errorMessage} - + ); } diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index c10be850b09b4..65591e597ae85 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -28,7 +28,6 @@ import { DropDownProps } from 'antd/lib/dropdown'; */ // eslint-disable-next-line no-restricted-imports export { - Alert, AutoComplete, Avatar, Button, @@ -57,6 +56,7 @@ export { Tooltip, Input as AntdInput, } from 'antd'; +export { default as Alert, AlertProps } from 'antd/lib/alert'; export { TreeProps } from 'antd/lib/tree'; export { FormInstance } from 'antd/lib/form'; export { RadioChangeEvent } from 'antd/lib/radio'; diff --git a/superset-frontend/src/components/Alert/Alert.stories.tsx b/superset-frontend/src/components/Alert/Alert.stories.tsx new file mode 100644 index 0000000000000..4b4f86ea3db5c --- /dev/null +++ b/superset-frontend/src/components/Alert/Alert.stories.tsx @@ -0,0 +1,100 @@ +/** + * 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 Alert, { AlertProps } from './index'; + +type AlertType = Pick; +type AlertTypeValue = AlertType[keyof AlertType]; + +const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success']; + +const smallText = 'Lorem ipsum dolor sit amet'; + +const bigText = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + + 'Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet ' + + 'purus convallis placerat in at nunc. Nulla nec viverra augue.'; + +export default { + title: 'Alert', + component: Alert, +}; + +export const AlertGallery = () => ( + <> + {types.map(type => ( +
+

{type}

+ + +
+ ))} + +); + +AlertGallery.story = { + parameters: { + actions: { + disabled: true, + }, + controls: { + disabled: true, + }, + knobs: { + disabled: true, + }, + }, +}; + +export const InteractiveAlert = (args: AlertProps) => ; + +InteractiveAlert.args = { + closable: true, + type: 'info', + message: smallText, + description: bigText, + showIcon: true, +}; + +InteractiveAlert.argTypes = { + onClose: { action: 'onClose' }, + type: { + control: { type: 'select', options: types }, + }, +}; + +InteractiveAlert.story = { + parameters: { + knobs: { + disabled: true, + }, + }, +}; diff --git a/superset-frontend/src/components/Alert/Alert.test.tsx b/superset-frontend/src/components/Alert/Alert.test.tsx new file mode 100644 index 0000000000000..eeb23b3c6dd6c --- /dev/null +++ b/superset-frontend/src/components/Alert/Alert.test.tsx @@ -0,0 +1,68 @@ +/** + * 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 { render, screen } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import Alert, { AlertProps } from 'src/components/Alert'; + +type AlertType = Pick; +type AlertTypeValue = AlertType[keyof AlertType]; + +test('renders with default props', () => { + render(); + expect(screen.getByRole('alert')).toHaveTextContent('Message'); + expect(screen.queryByLabelText(`info icon`)).toBeInTheDocument(); + expect(screen.queryByLabelText('close icon')).toBeInTheDocument(); +}); + +test('renders each type', () => { + const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success']; + types.forEach(type => { + render(); + expect(screen.queryByLabelText(`${type} icon`)).toBeInTheDocument(); + }); +}); + +test('renders without close button', () => { + render(); + expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument(); +}); + +test('disappear when closed', () => { + render(); + userEvent.click(screen.queryByLabelText('close icon')!); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); +}); + +test('renders without icon', () => { + const type = 'info'; + render(); + expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument(); +}); + +test('renders message', () => { + render(); + expect(screen.getByRole('alert')).toHaveTextContent('Message'); +}); + +test('renders message and description', () => { + render(); + expect(screen.getByRole('alert')).toHaveTextContent('Message'); + expect(screen.getByRole('alert')).toHaveTextContent('Description'); +}); diff --git a/superset-frontend/src/components/Alert/index.tsx b/superset-frontend/src/components/Alert/index.tsx new file mode 100644 index 0000000000000..a80238a74f80e --- /dev/null +++ b/superset-frontend/src/components/Alert/index.tsx @@ -0,0 +1,86 @@ +/** + * 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, { PropsWithChildren } from 'react'; +import { + Alert as AntdAlert, + AlertProps as AntdAlertProps, +} from 'src/common/components'; +import { useTheme } from '@superset-ui/core'; +import Icon, { IconName } from 'src/components/Icon'; + +export type AlertProps = PropsWithChildren; + +export default function Alert(props: AlertProps) { + const { + type = 'info', + description, + showIcon = true, + closable = true, + children, + } = props; + + const theme = useTheme(); + const { colors, typography } = theme; + const { alert, error, info, success } = colors; + + let baseColor = info; + let iconName: IconName = 'info-solid'; + if (type === 'error') { + baseColor = error; + iconName = 'error-solid'; + } else if (type === 'warning') { + baseColor = alert; + iconName = 'alert-solid'; + } else if (type === 'success') { + baseColor = success; + iconName = 'circle-check-solid'; + } + + return ( + } + closeText={closable && } + css={{ + padding: '6px 10px', + alignItems: 'flex-start', + border: 0, + backgroundColor: baseColor.light2, + '& .ant-alert-icon': { + marginRight: 10, + }, + '& .ant-alert-message': { + color: baseColor.dark2, + fontSize: typography.sizes.m, + fontWeight: description + ? typography.weights.bold + : typography.weights.normal, + }, + '& .ant-alert-description': { + color: baseColor.dark2, + fontSize: typography.sizes.m, + }, + }} + {...props} + > + {children} + + ); +} diff --git a/superset-frontend/src/components/Checkbox/Checkbox.test.tsx b/superset-frontend/src/components/Checkbox/Checkbox.test.tsx index 7877b7e66f56a..5e493d3c444e3 100644 --- a/superset-frontend/src/components/Checkbox/Checkbox.test.tsx +++ b/superset-frontend/src/components/Checkbox/Checkbox.test.tsx @@ -43,7 +43,9 @@ describe('Checkbox', () => { const shallowWrapper = shallow( true} />, ); - expect(shallowWrapper.dive().dive().find(CheckboxUnchecked)).toExist(); + expect( + shallowWrapper.dive().dive().dive().find(CheckboxUnchecked), + ).toExist(); }); }); @@ -52,7 +54,9 @@ describe('Checkbox', () => { const shallowWrapper = shallow( true} />, ); - expect(shallowWrapper.dive().dive().find(CheckboxChecked)).toExist(); + expect( + shallowWrapper.dive().dive().dive().find(CheckboxChecked), + ).toExist(); }); }); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 232ec2ad0117e..8d9182ac70e4c 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -18,8 +18,8 @@ */ import { t, styled } from '@superset-ui/core'; import React, { useEffect } from 'react'; -import { Alert } from 'react-bootstrap'; import { Empty } from 'src/common/components'; +import Alert from 'src/components/Alert'; import { ReactComponent as EmptyImage } from 'images/empty.svg'; import cx from 'classnames'; import Button from 'src/components/Button'; @@ -91,15 +91,12 @@ const ListViewStyles = styled.div` const BulkSelectWrapper = styled(Alert)` border-radius: 0; margin-bottom: 0; - padding-top: 0; - padding-bottom: 0; - padding-right: ${({ theme }) => theme.gridUnit * 9}px; color: #3d3d3d; background-color: ${({ theme }) => theme.colors.primary.light4}; .selectedCopy { display: inline-block; - padding: ${({ theme }) => theme.gridUnit * 4}px 0; + padding: ${({ theme }) => theme.gridUnit * 2}px 0; } .deselect-all { @@ -117,10 +114,6 @@ const BulkSelectWrapper = styled(Alert)` vertical-align: middle; position: relative; } - - .close { - margin: ${({ theme }) => theme.gridUnit * 4}px 0; - } `; const bulkSelectColumnConfig = { @@ -330,40 +323,47 @@ function ListView({ {bulkSelectEnabled && ( -
- {renderBulkSelectCopy(selectedFlatRows)} -
- {Boolean(selectedFlatRows.length) && ( + type="info" + closable + showIcon={false} + onClose={disableBulkSelect} + message={ <> - toggleAllRowsSelected(false)} - > - {t('Deselect all')} - -
- {bulkActions.map(action => ( - - ))} +
+ {renderBulkSelectCopy(selectedFlatRows)} +
+ {Boolean(selectedFlatRows.length) && ( + <> + toggleAllRowsSelected(false)} + > + {t('Deselect all')} + +
+ {bulkActions.map(action => ( + + ))} + + )} - )} - + } + /> )} {viewMode === 'card' && ( {showRefreshWarning && ( - -
{refreshWarning}
-
- {t('Are you sure you want to proceed?')} -
+ +
{refreshWarning}
+
+ {t('Are you sure you want to proceed?')} + + } + />
)}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx index 96d1307f30b0f..875e631ca3b28 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterConfigModal/CancelConfirmationAlert.tsx @@ -17,48 +17,9 @@ * under the License. */ import React from 'react'; -import { styled, t } from '@superset-ui/core'; -import Alert from 'react-bootstrap/lib/Alert'; +import { t } from '@superset-ui/core'; +import Alert from 'src/components/Alert'; import Button from 'src/components/Button'; -import Icon from 'src/components/Icon'; - -const StyledAlert = styled(Alert)` - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; - padding: ${({ theme }) => theme.gridUnit * 2}px; -`; - -const StyledTextContainer = styled.div` - display: flex; - flex-direction: column; - text-align: left; - margin-right: ${({ theme }) => theme.gridUnit}px; -`; - -const StyledTitleBox = styled.div` - display: flex; - align-items: center; -`; - -const StyledAlertTitle = styled.span` - font-weight: ${({ theme }) => theme.typography.weights.bold}; -`; - -const StyledAlertText = styled.p` - margin-left: ${({ theme }) => theme.gridUnit * 9}px; -`; - -const StyledButtonsContainer = styled.div` - display: flex; - flex-direction: row; -`; - -const StyledAlertIcon = styled(Icon)` - color: ${({ theme }) => theme.colors.alert.base}; - margin-right: ${({ theme }) => theme.gridUnit * 3}px; -`; export interface ConfirmationAlertProps { title: string; @@ -74,32 +35,35 @@ export function CancelConfirmationAlert({ children, }: ConfirmationAlertProps) { return ( - - - - - {title} - - {children} - - - - - - + + + +
+ } + /> ); } diff --git a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx index 5c144e4d78687..0934a6d2ad515 100644 --- a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx @@ -23,7 +23,8 @@ import React, { useEffect, useCallback, } from 'react'; -import { Alert, FormControl, FormControlProps } from 'react-bootstrap'; +import { FormControl, FormControlProps } from 'react-bootstrap'; +import Alert from 'src/components/Alert'; import { SupersetClient, t, styled } from '@superset-ui/core'; import TableView, { EmptyWrapperType } from 'src/components/TableView'; import StyledModal from 'src/common/components/Modal'; @@ -246,9 +247,14 @@ const ChangeDatasourceModal: FunctionComponent = ({ <> {!confirmChange && ( <> - - {t('Warning!')} {CHANGE_WARNING_MSG} - + + {t('Warning!')} {CHANGE_WARNING_MSG} + + } + />
{ diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index 5d3cf1cd3b64e..29f5b7099595d 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -18,8 +18,9 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { Alert, Col, Well } from 'react-bootstrap'; +import { Col, Well } from 'react-bootstrap'; import { Radio } from 'src/common/components/Radio'; +import Alert from 'src/components/Alert'; import Badge from 'src/common/components/Badge'; import shortid from 'shortid'; import { styled, SupersetClient, t, supersetTheme } from '@superset-ui/core'; @@ -830,11 +831,17 @@ class DatasourceEditor extends React.PureComponent { renderErrors() { if (this.state.errors.length > 0) { return ( - - {this.state.errors.map(err => ( -
{err}
- ))} -
+ ({ marginBottom: theme.gridUnit * 4 })} + type="error" + message={ + <> + {this.state.errors.map(err => ( +
{err}
+ ))} + + } + /> ); } return null; @@ -970,14 +977,19 @@ class DatasourceEditor extends React.PureComponent { return ( {this.renderErrors()} -
- - {t('Be careful.')} - {t( - 'Changing these settings will affect all charts using this dataset, including charts owned by other people.', - )} - -
+ ({ marginBottom: theme.gridUnit * 4 })} + type="warning" + message={ + <> + {' '} + {t('Be careful.')} + {t( + 'Changing these settings will affect all charts using this dataset, including charts owned by other people.', + )} + + } + /> = ({ setErrors(err); }; - const closeDialog = () => { - dialog.current?.destroy(); - }; - const renderSaveDialog = () => (
- -
- {' '} - {t(`The dataset configuration exposed here + ({ + marginTop: theme.gridUnit * 4, + marginBottom: theme.gridUnit * 4, + })} + type="warning" + showIcon + message={t(`The dataset configuration exposed here affects all the charts using this dataset. Be mindful that changing settings here may affect other charts in undesirable ways.`)} -
-
+ /> {t('Are you sure you want to save and apply changes?')}
); diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx index b084505debc98..0a41a99aa58ad 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx @@ -21,11 +21,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { Alert } from 'react-bootstrap'; import { t, styled, getChartControlPanelRegistry } from '@superset-ui/core'; import Tabs from 'src/common/components/Tabs'; import { Collapse } from 'src/common/components'; +import Alert from 'src/components/Alert'; import { PluginContext } from 'src/components/DynamicPlugins'; import Loading from 'src/components/Loading'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; @@ -278,17 +278,12 @@ class ControlPanelsContainer extends React.Component { return ( {this.props.alert && ( - - {this.props.alert} - - + )} { >
{(this.state.alert || this.props.alert) && ( - - {this.state.alert ? this.state.alert : this.props.alert} - - + + {this.state.alert ? this.state.alert : this.props.alert} + + + } + /> )} theme.gridUnit * 5}px; +`; + interface ToastPresenterProps { toast: ToastMeta; onCloseToast: (id: string) => void; @@ -73,26 +76,34 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) { }; }, [handleClosePress, toast.duration]); + let iconName: IconName = 'circle-check-solid'; + let className = 'toast--success'; + if (toast.toastType === ToastType.WARNING) { + iconName = 'warning-solid'; + className = 'toast--warning'; + } else if (toast.toastType === ToastType.DANGER) { + iconName = 'error-solid'; + className = 'toast--danger'; + } else if (toast.toastType === ToastType.INFO) { + iconName = 'info-solid'; + className = 'toast--info'; + } + return ( - - - {toast.toastType === ToastType.SUCCESS && ( - - )} - {toast.toastType === ToastType.WARNING || - (toast.toastType === ToastType.DANGER && )} - - - + + + + ); } diff --git a/superset-frontend/src/messageToasts/components/ToastPresenter.tsx b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx index aba922c10cbda..05e3cee2c5d9f 100644 --- a/superset-frontend/src/messageToasts/components/ToastPresenter.tsx +++ b/superset-frontend/src/messageToasts/components/ToastPresenter.tsx @@ -25,8 +25,9 @@ const StyledToastPresenter = styled.div` max-width: 600px; position: fixed; bottom: 0px; - right: -110px; - transform: translate(-50%, 0); + right: 0px; + margin-right: 50px; + margin-bottom: 50px; z-index: ${({ theme }) => theme.zIndex.max}; .toast { diff --git a/superset-frontend/stylesheets/antd/index.less b/superset-frontend/stylesheets/antd/index.less index 4b1d66c11a726..cf51b165eb998 100644 --- a/superset-frontend/stylesheets/antd/index.less +++ b/superset-frontend/stylesheets/antd/index.less @@ -34,7 +34,6 @@ @processing-color: #66bcfe; @error-color: #e04355; @highlight-color: #e04355; -@warning-color: #fbc700; @normal-color: #d9d9d9; @white: #fff; @black: #000;