mirror of
https://github.com/apache/superset.git
synced 2026-07-13 10:15:58 +00:00
Compare commits
11 Commits
fix-report
...
msyavuz/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d200b24ed2 | ||
|
|
e1678fd5c7 | ||
|
|
818a089412 | ||
|
|
ef37a06e43 | ||
|
|
80dd5f8e6d | ||
|
|
ca1ad9282b | ||
|
|
3bf2c3a536 | ||
|
|
e7b3ab7f32 | ||
|
|
afeee08b49 | ||
|
|
c5ba4ab530 | ||
|
|
e9400222aa |
@@ -161,7 +161,6 @@ export {
|
|||||||
export { Image, type ImageProps } from './Image';
|
export { Image, type ImageProps } from './Image';
|
||||||
export { Popconfirm, type PopconfirmProps } from './Popconfirm';
|
export { Popconfirm, type PopconfirmProps } from './Popconfirm';
|
||||||
export { Upload, type UploadFile, type UploadChangeParam } from './Upload';
|
export { Upload, type UploadFile, type UploadChangeParam } from './Upload';
|
||||||
// Add these to your index.ts
|
|
||||||
export * from './Menu';
|
export * from './Menu';
|
||||||
export * from './Popover';
|
export * from './Popover';
|
||||||
export * from './Radio';
|
export * from './Radio';
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { configureStore, Store } from '@reduxjs/toolkit';
|
|||||||
import { api } from 'src/hooks/apiResources/queryApi';
|
import { api } from 'src/hooks/apiResources/queryApi';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
|
import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
|
||||||
|
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||||
|
|
||||||
type Options = Omit<RenderOptions, 'queries'> & {
|
type Options = Omit<RenderOptions, 'queries'> & {
|
||||||
useRedux?: boolean;
|
useRedux?: boolean;
|
||||||
@@ -87,7 +88,9 @@ export function createWrapper(options?: Options) {
|
|||||||
return ({ children }: { children?: ReactNode }) => {
|
return ({ children }: { children?: ReactNode }) => {
|
||||||
let result = (
|
let result = (
|
||||||
<ThemeProvider theme={supersetTheme}>
|
<ThemeProvider theme={supersetTheme}>
|
||||||
<ExtensionsProvider>{children}</ExtensionsProvider>
|
<ExtensionsProvider>
|
||||||
|
<NotificationProvider>{children}</NotificationProvider>
|
||||||
|
</ExtensionsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* 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 { notification as antdNotification } from 'antd';
|
||||||
|
import { createContext, useContext, useMemo, ReactNode } from 'react';
|
||||||
|
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||||
|
|
||||||
|
export type NotificationType = 'success' | 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
|
export type NotificationPlacement =
|
||||||
|
| 'top'
|
||||||
|
| 'topLeft'
|
||||||
|
| 'topRight'
|
||||||
|
| 'bottom'
|
||||||
|
| 'bottomLeft'
|
||||||
|
| 'bottomRight';
|
||||||
|
|
||||||
|
export interface NotificationArgsProps {
|
||||||
|
message: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
|
btn?: ReactNode;
|
||||||
|
key?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
duration?: number | null;
|
||||||
|
icon?: ReactNode;
|
||||||
|
placement?: NotificationPlacement;
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
closeIcon?: boolean | ReactNode;
|
||||||
|
role?: 'alert' | 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationApi = NotificationInstance;
|
||||||
|
|
||||||
|
export interface NotificationContextType {
|
||||||
|
api: NotificationApi;
|
||||||
|
success: (args: NotificationArgsProps) => void;
|
||||||
|
error: (args: NotificationArgsProps) => void;
|
||||||
|
warning: (args: NotificationArgsProps) => void;
|
||||||
|
info: (args: NotificationArgsProps) => void;
|
||||||
|
open: (args: NotificationArgsProps & { type?: NotificationType }) => void;
|
||||||
|
destroy: (key?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationContext = createContext<NotificationContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [api, contextHolder] = antdNotification.useNotification();
|
||||||
|
|
||||||
|
const value = useMemo<NotificationContextType>(
|
||||||
|
() => ({
|
||||||
|
api,
|
||||||
|
success: api.success,
|
||||||
|
error: api.error,
|
||||||
|
warning: api.warning,
|
||||||
|
info: api.info,
|
||||||
|
open: api.open,
|
||||||
|
destroy: api.destroy,
|
||||||
|
}),
|
||||||
|
[api],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={value}>
|
||||||
|
{contextHolder}
|
||||||
|
{children}
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNotification = (): NotificationContextType => {
|
||||||
|
const context = useContext(NotificationContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useNotification must be used within a NotificationProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,46 +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 { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
|
||||||
import Toast from 'src/components/MessageToasts/Toast';
|
|
||||||
import mockMessageToasts from './mockMessageToasts';
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
toast: mockMessageToasts[0],
|
|
||||||
onCloseToast() {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const setup = overrideProps => render(<Toast {...props} {...overrideProps} />);
|
|
||||||
|
|
||||||
test('should render', () => {
|
|
||||||
const { getByTestId } = setup();
|
|
||||||
expect(getByTestId('toast-container')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render toastText within the div', () => {
|
|
||||||
const { getByTestId } = setup();
|
|
||||||
expect(getByTestId('toast-container')).toHaveTextContent(props.toast.text);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should call onCloseToast upon toast dismissal', async () => {
|
|
||||||
const onCloseToast = jest.fn();
|
|
||||||
const { getByTestId } = setup({ onCloseToast });
|
|
||||||
fireEvent.click(getByTestId('close-button'));
|
|
||||||
await waitFor(() => expect(onCloseToast).toHaveBeenCalledTimes(1));
|
|
||||||
expect(onCloseToast).toHaveBeenCalledWith(props.toast.id);
|
|
||||||
});
|
|
||||||
@@ -1,150 +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 { styled, css, SupersetTheme, useTheme } from '@superset-ui/core';
|
|
||||||
import cx from 'classnames';
|
|
||||||
import { Interweave } from 'interweave';
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import { Icons } from '@superset-ui/core/components/Icons';
|
|
||||||
import { ToastType, ToastMeta } from './types';
|
|
||||||
|
|
||||||
const ToastContainer = styled.div`
|
|
||||||
${({ theme }) => css`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between; // Changed from center to space-between
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
// Content container for icon and text
|
|
||||||
.toast__content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1; // Take available space
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
padding: 0 ${theme.sizeUnit}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast__close,
|
|
||||||
.toast__close span {
|
|
||||||
padding-left: ${theme.sizeUnit * 4}px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const notificationStyledIcon = (theme: SupersetTheme) => css`
|
|
||||||
min-width: ${theme.sizeUnit * 5}px;
|
|
||||||
color: ${theme.colorTextLightSolid};
|
|
||||||
margin-right: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ToastPresenterProps {
|
|
||||||
toast: ToastMeta;
|
|
||||||
onCloseToast: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
|
|
||||||
const hideTimer = useRef<ReturnType<typeof setTimeout>>();
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
|
|
||||||
const showToast = () => {
|
|
||||||
setVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClosePress = useCallback(() => {
|
|
||||||
if (hideTimer.current) {
|
|
||||||
clearTimeout(hideTimer.current);
|
|
||||||
}
|
|
||||||
// Wait for the transition
|
|
||||||
setVisible(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
onCloseToast(toast.id);
|
|
||||||
}, 150);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}, [onCloseToast, toast.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(showToast);
|
|
||||||
if (toast.duration > 0) {
|
|
||||||
hideTimer.current = setTimeout(handleClosePress, toast.duration);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (hideTimer.current) {
|
|
||||||
clearTimeout(hideTimer.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [handleClosePress, toast.duration]);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
let className = 'toast--success';
|
|
||||||
let icon = (
|
|
||||||
<Icons.CheckCircleFilled
|
|
||||||
css={theme => notificationStyledIcon(theme)}
|
|
||||||
iconColor={theme.colorSuccess}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (toast.toastType === ToastType.Warning) {
|
|
||||||
icon = (
|
|
||||||
<Icons.ExclamationCircleFilled
|
|
||||||
css={notificationStyledIcon}
|
|
||||||
iconColor={theme.colorWarning}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
className = 'toast--warning';
|
|
||||||
} else if (toast.toastType === ToastType.Danger) {
|
|
||||||
icon = (
|
|
||||||
<Icons.ExclamationCircleFilled
|
|
||||||
css={notificationStyledIcon}
|
|
||||||
iconColor={theme.colorError}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
className = 'toast--danger';
|
|
||||||
} else if (toast.toastType === ToastType.Info) {
|
|
||||||
icon = (
|
|
||||||
<Icons.InfoCircleFilled
|
|
||||||
css={notificationStyledIcon}
|
|
||||||
iconColor={theme.colorInfo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
className = 'toast--info';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToastContainer
|
|
||||||
className={cx('alert', 'toast', visible && 'toast--visible', className)}
|
|
||||||
data-test="toast-container"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<div className="toast__content">
|
|
||||||
{icon}
|
|
||||||
<Interweave content={toast.text} noHtml={!toast.allowHtml} />
|
|
||||||
</div>
|
|
||||||
<Icons.CloseOutlined
|
|
||||||
iconSize="m"
|
|
||||||
className="toast__close pointer"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={handleClosePress}
|
|
||||||
aria-label="Close"
|
|
||||||
data-test="close-button"
|
|
||||||
/>
|
|
||||||
</ToastContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -16,11 +16,85 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ToastPresenter from './ToastPresenter';
|
import { Interweave } from 'interweave';
|
||||||
|
import { useNotification } from './NotificationProvider';
|
||||||
import { removeToast } from './actions';
|
import { removeToast } from './actions';
|
||||||
|
import { ToastMeta, ToastType } from './types';
|
||||||
|
|
||||||
|
interface ToastContainerProps {
|
||||||
|
toasts: ToastMeta[];
|
||||||
|
removeToast: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToastPresenter({ toasts, removeToast }: ToastContainerProps) {
|
||||||
|
const notification = useNotification();
|
||||||
|
const toastKeyMap = useRef(new Map<string, string>());
|
||||||
|
const displayedToasts = useRef(new Set<string>());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Process new toasts
|
||||||
|
toasts.forEach(toast => {
|
||||||
|
if (!displayedToasts.current.has(toast.id)) {
|
||||||
|
displayedToasts.current.add(toast.id);
|
||||||
|
|
||||||
|
const notificationKey = `toast-${toast.id}`;
|
||||||
|
toastKeyMap.current.set(toast.id, notificationKey);
|
||||||
|
|
||||||
|
const message = toast.allowHtml ? (
|
||||||
|
<Interweave content={toast.text} />
|
||||||
|
) : (
|
||||||
|
toast.text
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
key: notificationKey,
|
||||||
|
message,
|
||||||
|
duration: toast.duration > 0 ? toast.duration / 1000 : 0,
|
||||||
|
placement: 'bottomRight' as const,
|
||||||
|
onClose: () => {
|
||||||
|
toastKeyMap.current.delete(toast.id);
|
||||||
|
displayedToasts.current.delete(toast.id);
|
||||||
|
removeToast(toast.id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (toast.toastType) {
|
||||||
|
case ToastType.Success:
|
||||||
|
notification.success(config);
|
||||||
|
break;
|
||||||
|
case ToastType.Warning:
|
||||||
|
notification.warning(config);
|
||||||
|
break;
|
||||||
|
case ToastType.Danger:
|
||||||
|
notification.error(config);
|
||||||
|
break;
|
||||||
|
case ToastType.Info:
|
||||||
|
default:
|
||||||
|
notification.info(config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up removed toasts
|
||||||
|
const currentToastIds = new Set(toasts.map(t => t.id));
|
||||||
|
displayedToasts.current.forEach(id => {
|
||||||
|
if (!currentToastIds.has(id)) {
|
||||||
|
const key = toastKeyMap.current.get(id);
|
||||||
|
if (key) {
|
||||||
|
notification.destroy(key);
|
||||||
|
toastKeyMap.current.delete(id);
|
||||||
|
}
|
||||||
|
displayedToasts.current.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [toasts, removeToast, notification]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const ToastContainer = connect(
|
const ToastContainer = connect(
|
||||||
({ messageToasts: toasts }: any) => ({ toasts }),
|
({ messageToasts: toasts }: any) => ({ toasts }),
|
||||||
|
|||||||
@@ -1,48 +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 { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
|
|
||||||
|
|
||||||
import ToastPresenter from 'src/components/MessageToasts/ToastPresenter';
|
|
||||||
import mockMessageToasts from './mockMessageToasts';
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
toasts: mockMessageToasts,
|
|
||||||
removeToast() {},
|
|
||||||
};
|
|
||||||
|
|
||||||
function setup(overrideProps) {
|
|
||||||
return render(<ToastPresenter {...props} {...overrideProps} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
test('should render a div with id toast-presenter', () => {
|
|
||||||
const { container } = setup();
|
|
||||||
expect(container.querySelector('#toast-presenter')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render a Toast for each toast object', () => {
|
|
||||||
const { getAllByRole } = setup();
|
|
||||||
expect(getAllByRole('alert')).toHaveLength(props.toasts.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should pass removeToast to the Toast component', async () => {
|
|
||||||
const removeToast = jest.fn();
|
|
||||||
const { getAllByTestId } = setup({ removeToast });
|
|
||||||
fireEvent.click(getAllByTestId('close-button')[0]);
|
|
||||||
await waitFor(() => expect(removeToast).toHaveBeenCalledTimes(1));
|
|
||||||
});
|
|
||||||
@@ -1,98 +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 { styled } from '@superset-ui/core';
|
|
||||||
import { ToastMeta } from 'src/components/MessageToasts/types';
|
|
||||||
import Toast from './Toast';
|
|
||||||
|
|
||||||
export interface VisualProps {
|
|
||||||
position: 'bottom' | 'top';
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledToastPresenter = styled.div<VisualProps>(
|
|
||||||
({ theme, position }) =>
|
|
||||||
// Single access to theme, using dot notation
|
|
||||||
`
|
|
||||||
max-width: 600px;
|
|
||||||
position: fixed;
|
|
||||||
${position === 'bottom' ? 'bottom' : 'top'}: 0px;
|
|
||||||
right: 0px;
|
|
||||||
margin-right: 50px;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
z-index: ${theme.zIndexPopupBase + 1};
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
padding: ${theme.sizeUnit * 4}px;
|
|
||||||
margin: ${theme.sizeUnit * 4}px;
|
|
||||||
background: ${theme.colorBgSpotlight};
|
|
||||||
border-radius: ${theme.borderRadius}px;
|
|
||||||
box-shadow: ${theme.boxShadow};
|
|
||||||
color: ${theme.colorTextLightSolid};
|
|
||||||
opacity: 0;
|
|
||||||
position: relative;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
white-space: pre-line;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
transition:
|
|
||||||
transform ${theme.motionDurationMid},
|
|
||||||
opacity ${theme.motionDurationMid};
|
|
||||||
&:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 6px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast > button {
|
|
||||||
color: ${theme.colorTextLightSolid};
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast--visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
type ToastPresenterProps = Partial<VisualProps> & {
|
|
||||||
toasts: Array<ToastMeta>;
|
|
||||||
removeToast: () => any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ToastPresenter({
|
|
||||||
toasts,
|
|
||||||
removeToast,
|
|
||||||
position = 'bottom',
|
|
||||||
}: ToastPresenterProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{toasts.length > 0 && (
|
|
||||||
<StyledToastPresenter id="toast-presenter" position={position}>
|
|
||||||
{toasts.map(toast => (
|
|
||||||
<Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
|
|
||||||
))}
|
|
||||||
</StyledToastPresenter>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { ToastType, ToastMeta } from './types';
|
import { ToastType, ToastMeta, ToastOptions } from './types';
|
||||||
|
|
||||||
type ToastOptions = Partial<Omit<ToastMeta, 'id' | 'toastType' | 'text'>>;
|
|
||||||
|
|
||||||
export function getToastUuid(type: ToastType) {
|
export function getToastUuid(type: ToastType) {
|
||||||
return `${type}-${nanoid()}`;
|
return `${type}-${nanoid()}`;
|
||||||
|
|||||||
@@ -33,4 +33,13 @@ export interface ToastMeta {
|
|||||||
noDuplicate?: boolean;
|
noDuplicate?: boolean;
|
||||||
/** For security reasons, HTML rendering is disabled by default. Use this property to enable it. */
|
/** For security reasons, HTML rendering is disabled by default. Use this property to enable it. */
|
||||||
allowHtml?: boolean;
|
allowHtml?: boolean;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
export type ToastOptions = Partial<
|
||||||
|
Omit<ToastMeta, 'id' | 'toastType' | 'text'>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ToastTriggerFunction = (
|
||||||
|
message: string,
|
||||||
|
options?: ToastOptions,
|
||||||
|
) => void;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { QueryParamProvider } from 'use-query-params';
|
|||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
||||||
|
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||||
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
||||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||||
import { ThemeController } from 'src/theme/ThemeController';
|
import { ThemeController } from 'src/theme/ThemeController';
|
||||||
@@ -71,18 +72,20 @@ export const EmbeddedContextProviders: React.FC = ({ children }) => {
|
|||||||
<FlashProvider messages={common.flash_messages}>
|
<FlashProvider messages={common.flash_messages}>
|
||||||
<EmbeddedUiConfigProvider>
|
<EmbeddedUiConfigProvider>
|
||||||
<DynamicPluginProvider>
|
<DynamicPluginProvider>
|
||||||
<QueryParamProvider
|
<NotificationProvider>
|
||||||
ReactRouterRoute={Route}
|
<QueryParamProvider
|
||||||
stringifyOptions={{ encode: false }}
|
ReactRouterRoute={Route}
|
||||||
>
|
stringifyOptions={{ encode: false }}
|
||||||
{RootContextProviderExtension ? (
|
>
|
||||||
<RootContextProviderExtension>
|
{RootContextProviderExtension ? (
|
||||||
{children}
|
<RootContextProviderExtension>
|
||||||
</RootContextProviderExtension>
|
{children}
|
||||||
) : (
|
</RootContextProviderExtension>
|
||||||
children
|
) : (
|
||||||
)}
|
children
|
||||||
</QueryParamProvider>
|
)}
|
||||||
|
</QueryParamProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</DynamicPluginProvider>
|
</DynamicPluginProvider>
|
||||||
</EmbeddedUiConfigProvider>
|
</EmbeddedUiConfigProvider>
|
||||||
</FlashProvider>
|
</FlashProvider>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const EmbeddedRoute = () => (
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<EmbededLazyDashboardPage />
|
<EmbededLazyDashboardPage />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ToastContainer position="top" />
|
<ToastContainer />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</EmbeddedContextProviders>
|
</EmbeddedContextProviders>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
unsetSystemDefaultTheme,
|
unsetSystemDefaultTheme,
|
||||||
unsetSystemDarkTheme,
|
unsetSystemDarkTheme,
|
||||||
} from 'src/features/themes/api';
|
} from 'src/features/themes/api';
|
||||||
|
import { ToastTriggerFunction } from 'src/components/MessageToasts/types';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
@@ -79,8 +80,8 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface ThemesListProps {
|
interface ThemesListProps {
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: ToastTriggerFunction;
|
||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: ToastTriggerFunction;
|
||||||
user: {
|
user: {
|
||||||
userId: string | number;
|
userId: string | number;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { DndProvider } from 'react-dnd';
|
|||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
||||||
|
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||||
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
||||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||||
import { ThemeController } from 'src/theme/ThemeController';
|
import { ThemeController } from 'src/theme/ThemeController';
|
||||||
@@ -48,20 +49,22 @@ export const RootContextProviders: React.FC = ({ children }) => {
|
|||||||
<FlashProvider messages={common.flash_messages}>
|
<FlashProvider messages={common.flash_messages}>
|
||||||
<EmbeddedUiConfigProvider>
|
<EmbeddedUiConfigProvider>
|
||||||
<DynamicPluginProvider>
|
<DynamicPluginProvider>
|
||||||
<QueryParamProvider
|
<NotificationProvider>
|
||||||
ReactRouterRoute={Route}
|
<QueryParamProvider
|
||||||
stringifyOptions={{ encode: false }}
|
ReactRouterRoute={Route}
|
||||||
>
|
stringifyOptions={{ encode: false }}
|
||||||
<ExtensionsProvider>
|
>
|
||||||
{RootContextProviderExtension ? (
|
<ExtensionsProvider>
|
||||||
<RootContextProviderExtension>
|
{RootContextProviderExtension ? (
|
||||||
{children}
|
<RootContextProviderExtension>
|
||||||
</RootContextProviderExtension>
|
{children}
|
||||||
) : (
|
</RootContextProviderExtension>
|
||||||
children
|
) : (
|
||||||
)}
|
children
|
||||||
</ExtensionsProvider>
|
)}
|
||||||
</QueryParamProvider>
|
</ExtensionsProvider>
|
||||||
|
</QueryParamProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</DynamicPluginProvider>
|
</DynamicPluginProvider>
|
||||||
</EmbeddedUiConfigProvider>
|
</EmbeddedUiConfigProvider>
|
||||||
</FlashProvider>
|
</FlashProvider>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import createCache from '@emotion/cache';
|
|||||||
import { ThemeProvider, theme } from '@superset-ui/core';
|
import { ThemeProvider, theme } from '@superset-ui/core';
|
||||||
import Menu from 'src/features/home/Menu';
|
import Menu from 'src/features/home/Menu';
|
||||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||||
|
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||||
import { setupStore } from './store';
|
import { setupStore } from './store';
|
||||||
|
|
||||||
// Disable connecting to redux debugger so that the React app injected
|
// Disable connecting to redux debugger so that the React app injected
|
||||||
@@ -45,16 +46,18 @@ const app = (
|
|||||||
// @ts-ignore: emotion types defs are incompatible between core and cache
|
// @ts-ignore: emotion types defs are incompatible between core and cache
|
||||||
<CacheProvider value={emotionCache}>
|
<CacheProvider value={emotionCache}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Provider store={store}>
|
<NotificationProvider>
|
||||||
<BrowserRouter>
|
<Provider store={store}>
|
||||||
<QueryParamProvider
|
<BrowserRouter>
|
||||||
ReactRouterRoute={Route}
|
<QueryParamProvider
|
||||||
stringifyOptions={{ encode: false }}
|
ReactRouterRoute={Route}
|
||||||
>
|
stringifyOptions={{ encode: false }}
|
||||||
<Menu data={menu} />
|
>
|
||||||
</QueryParamProvider>
|
<Menu data={menu} />
|
||||||
</BrowserRouter>
|
</QueryParamProvider>
|
||||||
</Provider>
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
</NotificationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user