mirror of
https://github.com/apache/superset.git
synced 2026-07-16 11:46:09 +00:00
Compare commits
17 Commits
fix-ecs-ta
...
msyavuz/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d200b24ed2 | ||
|
|
e1678fd5c7 | ||
|
|
818a089412 | ||
|
|
ef37a06e43 | ||
|
|
80dd5f8e6d | ||
|
|
ca1ad9282b | ||
|
|
3bf2c3a536 | ||
|
|
e7b3ab7f32 | ||
|
|
afeee08b49 | ||
|
|
c5ba4ab530 | ||
|
|
e9400222aa | ||
|
|
076e477fd4 | ||
|
|
1e4bc6ee78 | ||
|
|
db178cf527 | ||
|
|
5901320933 | ||
|
|
23bb4f88c0 | ||
|
|
4130b92966 |
4
.github/workflows/ecs-task-definition.json
vendored
4
.github/workflows/ecs-task-definition.json
vendored
@@ -31,10 +31,6 @@
|
||||
{
|
||||
"name": "TALISMAN_ENABLED",
|
||||
"value": "False"
|
||||
},
|
||||
{
|
||||
"name": "SUPERSET__SQLALCHEMY_EXAMPLES_URI",
|
||||
"value": "duckdb:////app/data/examples.duckdb?access_mode=read_only"
|
||||
}
|
||||
],
|
||||
"mountPoints": [],
|
||||
|
||||
@@ -76,7 +76,7 @@ dependencies = [
|
||||
"packaging",
|
||||
# --------------------------
|
||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||
"pandas[excel]>=2.0.3, <2.1",
|
||||
"pandas[excel]>=2.0.3, <2.2",
|
||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||
# --------------------------
|
||||
"parsedatetime",
|
||||
|
||||
@@ -160,6 +160,7 @@ greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
@@ -266,7 +267,7 @@ packaging==25.0
|
||||
# limits
|
||||
# marshmallow
|
||||
# shillelagh
|
||||
pandas==2.0.3
|
||||
pandas==2.1.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
paramiko==3.5.1
|
||||
# via
|
||||
|
||||
@@ -331,6 +331,7 @@ greenlet==3.1.1
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -536,7 +537,7 @@ packaging==25.0
|
||||
# pytest
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
pandas==2.0.3
|
||||
pandas==2.1.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover, type PopoverProps } from '@superset-ui/core/components';
|
||||
import type ReactAce from 'react-ace';
|
||||
import {
|
||||
Popover,
|
||||
type PopoverProps,
|
||||
SQLEditor,
|
||||
} from '@superset-ui/core/components';
|
||||
import { CalculatorOutlined } from '@ant-design/icons';
|
||||
import { css, styled, useTheme, t } from '@superset-ui/core';
|
||||
|
||||
@@ -35,24 +37,10 @@ const StyledCalculatorIcon = styled(CalculatorOutlined)`
|
||||
|
||||
export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||
const theme = useTheme();
|
||||
const [AceEditor, setAceEditor] = useState<typeof ReactAce | null>(null);
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
import('react-ace'),
|
||||
import('ace-builds/src-min-noconflict/mode-sql'),
|
||||
]).then(([reactAceModule]) => {
|
||||
setAceEditor(() => reactAceModule.default);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!AceEditor) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
<SQLEditor
|
||||
value={props.sqlExpression}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{
|
||||
@@ -65,7 +53,6 @@ export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||
wrapEnabled
|
||||
style={{
|
||||
border: `1px solid ${theme.colorBorder}`,
|
||||
background: theme.colorPrimaryBg,
|
||||
maxWidth: theme.sizeUnit * 100,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -161,7 +161,6 @@ export {
|
||||
export { Image, type ImageProps } from './Image';
|
||||
export { Popconfirm, type PopconfirmProps } from './Popconfirm';
|
||||
export { Upload, type UploadFile, type UploadChangeParam } from './Upload';
|
||||
// Add these to your index.ts
|
||||
export * from './Menu';
|
||||
export * from './Popover';
|
||||
export * from './Radio';
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import { t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlStateMapping,
|
||||
ControlSubSectionHeader,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
@@ -197,15 +196,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit(state: ControlStateMapping) {
|
||||
return {
|
||||
...state,
|
||||
row_limit: {
|
||||
...state.row_limit,
|
||||
value: state.row_limit.default,
|
||||
},
|
||||
};
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
|
||||
@@ -325,6 +325,7 @@ export default function transformProps(chartProps: EchartsGanttChartProps) {
|
||||
show: true,
|
||||
position: 'start',
|
||||
formatter: '{b}',
|
||||
color: theme.colorText,
|
||||
},
|
||||
data: categoryLines,
|
||||
},
|
||||
|
||||
@@ -47,7 +47,10 @@ import {
|
||||
isDerivedSeries,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
import type { LineStyleOption } from 'echarts/types/src/util/types';
|
||||
import type {
|
||||
LineStyleOption,
|
||||
CallbackDataParams,
|
||||
} from 'echarts/types/src/util/types';
|
||||
import type { SeriesOption } from 'echarts';
|
||||
import {
|
||||
EchartsTimeseriesChartProps,
|
||||
@@ -575,16 +578,31 @@ export default function transformProps(
|
||||
const xValue: number = richTooltip
|
||||
? params[0].value[xIndex]
|
||||
: params.value[xIndex];
|
||||
const forecastValue: any[] = richTooltip ? params : [params];
|
||||
const forecastValue: CallbackDataParams[] = richTooltip
|
||||
? params
|
||||
: [params];
|
||||
const sortedKeys = extractTooltipKeys(
|
||||
forecastValue,
|
||||
yIndex,
|
||||
richTooltip,
|
||||
tooltipSortByMetric,
|
||||
);
|
||||
const filteredForecastValue = forecastValue.filter(
|
||||
(item: CallbackDataParams) =>
|
||||
!annotationLayers.some(
|
||||
(annotation: AnnotationLayer) =>
|
||||
item.seriesName === annotation.name,
|
||||
),
|
||||
);
|
||||
const forecastValues: Record<string, ForecastValue> =
|
||||
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
|
||||
|
||||
const filteredForecastValues: Record<string, ForecastValue> =
|
||||
extractForecastValuesFromTooltipParams(
|
||||
filteredForecastValue,
|
||||
isHorizontal,
|
||||
);
|
||||
|
||||
const isForecast = Object.values(forecastValues).some(
|
||||
value =>
|
||||
value.forecastTrend || value.forecastLower || value.forecastUpper,
|
||||
@@ -595,7 +613,7 @@ export default function transformProps(
|
||||
: (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter);
|
||||
|
||||
const rows: string[][] = [];
|
||||
const total = Object.values(forecastValues).reduce(
|
||||
const total = Object.values(filteredForecastValues).reduce(
|
||||
(acc, value) =>
|
||||
value.observation !== undefined ? acc + value.observation : acc,
|
||||
0,
|
||||
@@ -617,7 +635,16 @@ export default function transformProps(
|
||||
seriesName: key,
|
||||
formatter,
|
||||
});
|
||||
if (showPercentage && value.observation !== undefined) {
|
||||
|
||||
const annotationRow = annotationLayers.some(
|
||||
item => item.name === key,
|
||||
);
|
||||
|
||||
if (
|
||||
showPercentage &&
|
||||
value.observation !== undefined &&
|
||||
!annotationRow
|
||||
) {
|
||||
row.push(
|
||||
percentFormatter.format(value.observation / (total || 1)),
|
||||
);
|
||||
|
||||
@@ -257,6 +257,7 @@ describe('Gantt transformProps', () => {
|
||||
show: true,
|
||||
position: 'start',
|
||||
formatter: '{b}',
|
||||
color: 'rgba(0,0,0,0.88)',
|
||||
},
|
||||
lineStyle: expect.objectContaining({
|
||||
color: '#00000000',
|
||||
|
||||
@@ -44,6 +44,7 @@ import { configureStore, Store } from '@reduxjs/toolkit';
|
||||
import { api } from 'src/hooks/apiResources/queryApi';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
|
||||
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||
|
||||
type Options = Omit<RenderOptions, 'queries'> & {
|
||||
useRedux?: boolean;
|
||||
@@ -87,7 +88,9 @@ export function createWrapper(options?: Options) {
|
||||
return ({ children }: { children?: ReactNode }) => {
|
||||
let result = (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<ExtensionsProvider>{children}</ExtensionsProvider>
|
||||
<ExtensionsProvider>
|
||||
<NotificationProvider>{children}</NotificationProvider>
|
||||
</ExtensionsProvider>
|
||||
</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
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import ToastPresenter from './ToastPresenter';
|
||||
|
||||
import { Interweave } from 'interweave';
|
||||
import { useNotification } from './NotificationProvider';
|
||||
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(
|
||||
({ 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.
|
||||
*/
|
||||
import { nanoid } from 'nanoid';
|
||||
import { ToastType, ToastMeta } from './types';
|
||||
|
||||
type ToastOptions = Partial<Omit<ToastMeta, 'id' | 'toastType' | 'text'>>;
|
||||
import { ToastType, ToastMeta, ToastOptions } from './types';
|
||||
|
||||
export function getToastUuid(type: ToastType) {
|
||||
return `${type}-${nanoid()}`;
|
||||
|
||||
@@ -33,4 +33,13 @@ export interface ToastMeta {
|
||||
noDuplicate?: boolean;
|
||||
/** For security reasons, HTML rendering is disabled by default. Use this property to enable it. */
|
||||
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 { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
||||
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||
import { ThemeController } from 'src/theme/ThemeController';
|
||||
@@ -71,18 +72,20 @@ export const EmbeddedContextProviders: React.FC = ({ children }) => {
|
||||
<FlashProvider messages={common.flash_messages}>
|
||||
<EmbeddedUiConfigProvider>
|
||||
<DynamicPluginProvider>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
{RootContextProviderExtension ? (
|
||||
<RootContextProviderExtension>
|
||||
{children}
|
||||
</RootContextProviderExtension>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</QueryParamProvider>
|
||||
<NotificationProvider>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
{RootContextProviderExtension ? (
|
||||
<RootContextProviderExtension>
|
||||
{children}
|
||||
</RootContextProviderExtension>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</QueryParamProvider>
|
||||
</NotificationProvider>
|
||||
</DynamicPluginProvider>
|
||||
</EmbeddedUiConfigProvider>
|
||||
</FlashProvider>
|
||||
|
||||
@@ -96,7 +96,7 @@ const EmbeddedRoute = () => (
|
||||
<ErrorBoundary>
|
||||
<EmbededLazyDashboardPage />
|
||||
</ErrorBoundary>
|
||||
<ToastContainer position="top" />
|
||||
<ToastContainer />
|
||||
</Suspense>
|
||||
</EmbeddedContextProviders>
|
||||
);
|
||||
|
||||
@@ -460,48 +460,26 @@ const ExtraOptions = ({
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
<StyledInputContainer>
|
||||
<div className="control-label">{t('Secure extra')}</div>
|
||||
<StyledInputContainer
|
||||
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
|
||||
>
|
||||
<div className="input-container">
|
||||
<StyledJsonEditor
|
||||
name="masked_encrypted_extra"
|
||||
value={db?.masked_encrypted_extra || ''}
|
||||
placeholder={t('Secure extra')}
|
||||
onChange={(json: string) =>
|
||||
onEditorChange({ json, name: 'masked_encrypted_extra' })
|
||||
}
|
||||
width="100%"
|
||||
height="160px"
|
||||
annotations={secureExtraAnnotations}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
<div>
|
||||
{t(
|
||||
'JSON string containing additional connection configuration. ' +
|
||||
'This is used to provide connection information for systems ' +
|
||||
'like Hive, Presto and BigQuery which do not conform to the ' +
|
||||
'username:password syntax normally used by SQLAlchemy.',
|
||||
<Checkbox
|
||||
id="per_user_caching"
|
||||
name="per_user_caching"
|
||||
indeterminate={false}
|
||||
checked={!!extraJson?.per_user_caching}
|
||||
onChange={onExtraInputChange}
|
||||
>
|
||||
{t('Per user caching')}
|
||||
</Checkbox>
|
||||
<InfoTooltip
|
||||
tooltip={t(
|
||||
'Cache data separately for each user based on their data access roles and permissions. ' +
|
||||
'When disabled, a single cache will be used for all users.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="control-label">{t('Root certificate')}</div>
|
||||
<div className="input-container">
|
||||
<Input.TextArea
|
||||
name="server_cert"
|
||||
value={db?.server_cert || ''}
|
||||
placeholder={t('Enter CA_BUNDLE')}
|
||||
onChange={onTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{t(
|
||||
'Optional CA_BUNDLE contents to validate HTTPS requests. Only ' +
|
||||
'available on certain database engines.',
|
||||
)}
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer
|
||||
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
|
||||
@@ -569,6 +547,49 @@ const ExtraOptions = ({
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
<StyledInputContainer>
|
||||
<div className="control-label">{t('Secure extra')}</div>
|
||||
<div className="input-container">
|
||||
<StyledJsonEditor
|
||||
name="masked_encrypted_extra"
|
||||
value={db?.masked_encrypted_extra || ''}
|
||||
placeholder={t('Secure extra')}
|
||||
onChange={(json: string) =>
|
||||
onEditorChange({ json, name: 'masked_encrypted_extra' })
|
||||
}
|
||||
width="100%"
|
||||
height="160px"
|
||||
annotations={secureExtraAnnotations}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
<div>
|
||||
{t(
|
||||
'JSON string containing additional connection configuration. ' +
|
||||
'This is used to provide connection information for systems ' +
|
||||
'like Hive, Presto and BigQuery which do not conform to the ' +
|
||||
'username:password syntax normally used by SQLAlchemy.',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
<div className="control-label">{t('Root certificate')}</div>
|
||||
<div className="input-container">
|
||||
<Input.TextArea
|
||||
name="server_cert"
|
||||
value={db?.server_cert || ''}
|
||||
placeholder={t('Enter CA_BUNDLE')}
|
||||
onChange={onTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
{t(
|
||||
'Optional CA_BUNDLE contents to validate HTTPS requests. Only ' +
|
||||
'available on certain database engines.',
|
||||
)}
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -246,6 +246,7 @@ export interface ExtraJson {
|
||||
disable_data_preview?: boolean; // in SQL Lab
|
||||
disable_drill_to_detail?: boolean;
|
||||
allow_multi_catalog?: boolean;
|
||||
per_user_caching?: boolean; // in Security
|
||||
engine_params?: {
|
||||
catalog?: Record<string, string>;
|
||||
connect_args?: {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
unsetSystemDefaultTheme,
|
||||
unsetSystemDarkTheme,
|
||||
} from 'src/features/themes/api';
|
||||
import { ToastTriggerFunction } from 'src/components/MessageToasts/types';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
@@ -79,8 +80,8 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
|
||||
);
|
||||
|
||||
interface ThemesListProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
addDangerToast: ToastTriggerFunction;
|
||||
addSuccessToast: ToastTriggerFunction;
|
||||
user: {
|
||||
userId: string | number;
|
||||
firstName: string;
|
||||
|
||||
@@ -25,6 +25,7 @@ import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import { FlashProvider, DynamicPluginProvider } from 'src/components';
|
||||
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
|
||||
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
|
||||
import { ThemeController } from 'src/theme/ThemeController';
|
||||
@@ -48,20 +49,22 @@ export const RootContextProviders: React.FC = ({ children }) => {
|
||||
<FlashProvider messages={common.flash_messages}>
|
||||
<EmbeddedUiConfigProvider>
|
||||
<DynamicPluginProvider>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
<ExtensionsProvider>
|
||||
{RootContextProviderExtension ? (
|
||||
<RootContextProviderExtension>
|
||||
{children}
|
||||
</RootContextProviderExtension>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ExtensionsProvider>
|
||||
</QueryParamProvider>
|
||||
<NotificationProvider>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
<ExtensionsProvider>
|
||||
{RootContextProviderExtension ? (
|
||||
<RootContextProviderExtension>
|
||||
{children}
|
||||
</RootContextProviderExtension>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ExtensionsProvider>
|
||||
</QueryParamProvider>
|
||||
</NotificationProvider>
|
||||
</DynamicPluginProvider>
|
||||
</EmbeddedUiConfigProvider>
|
||||
</FlashProvider>
|
||||
|
||||
@@ -29,6 +29,7 @@ import createCache from '@emotion/cache';
|
||||
import { ThemeProvider, theme } from '@superset-ui/core';
|
||||
import Menu from 'src/features/home/Menu';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import { NotificationProvider } from 'src/components/MessageToasts/NotificationProvider';
|
||||
import { setupStore } from './store';
|
||||
|
||||
// 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
|
||||
<CacheProvider value={emotionCache}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
<Menu data={menu} />
|
||||
</QueryParamProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
<NotificationProvider>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<QueryParamProvider
|
||||
ReactRouterRoute={Route}
|
||||
stringifyOptions={{ encode: false }}
|
||||
>
|
||||
<Menu data={menu} />
|
||||
</QueryParamProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</NotificationProvider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ class ExcelReader(BaseDataReader):
|
||||
"na_values": self._options.get("null_values")
|
||||
if self._options.get("null_values") # None if an empty list
|
||||
else None,
|
||||
"parse_dates": self._options.get("column_dates"),
|
||||
"parse_dates": self._options.get("column_dates") or False,
|
||||
"skiprows": self._options.get("skip_rows", 0),
|
||||
"sheet_name": self._options.get("sheet_name", 0),
|
||||
"nrows": self._options.get("rows_to_read"),
|
||||
|
||||
@@ -454,13 +454,19 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
cache_dict["annotation_layers"] = annotation_layers
|
||||
|
||||
# Add an impersonation key to cache if impersonation is enabled on the db
|
||||
# or if the CACHE_QUERY_BY_USER flag is on
|
||||
# or if the CACHE_QUERY_BY_USER flag is on or per_user_caching is enabled on
|
||||
# the database
|
||||
try:
|
||||
database = self.datasource.database # type: ignore
|
||||
extra = json.loads(database.extra or "{}")
|
||||
if (
|
||||
feature_flag_manager.is_feature_enabled("CACHE_IMPERSONATION")
|
||||
and database.impersonate_user
|
||||
) or feature_flag_manager.is_feature_enabled("CACHE_QUERY_BY_USER"):
|
||||
(
|
||||
feature_flag_manager.is_feature_enabled("CACHE_IMPERSONATION")
|
||||
and database.impersonate_user
|
||||
)
|
||||
or feature_flag_manager.is_feature_enabled("CACHE_QUERY_BY_USER")
|
||||
or extra.get("per_user_caching", False)
|
||||
):
|
||||
if key := database.db_engine_spec.get_impersonation_key(
|
||||
getattr(g, "user", None)
|
||||
):
|
||||
|
||||
@@ -831,6 +831,7 @@ class ImportV1DatabaseExtraSchema(Schema):
|
||||
disable_data_preview = fields.Boolean(required=False)
|
||||
disable_drill_to_detail = fields.Boolean(required=False)
|
||||
allow_multi_catalog = fields.Boolean(required=False)
|
||||
per_user_caching = fields.Boolean(required=False)
|
||||
version = fields.String(required=False, allow_none=True)
|
||||
schema_options = fields.Dict(keys=fields.Str(), values=fields.Raw())
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ def data_loader(
|
||||
pandas_loader_configuration: PandasLoaderConfigurations,
|
||||
table_to_df_convertor: TableToDfConvertor,
|
||||
) -> DataLoader:
|
||||
if example_db_engine.dialect.name == PRESTO:
|
||||
example_db_engine.dialect.get_view_names = Mock(return_value=[])
|
||||
return PandasDataLoader(
|
||||
example_db_engine, pandas_loader_configuration, table_to_df_convertor
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user