feat(alerts): enable tab selection for dashboard alerts/reports (#29096)
This commit is contained in:
parent
6bf8596d68
commit
d21d7591c0
|
|
@ -44,6 +44,7 @@ export {
|
|||
Steps,
|
||||
Tag,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
|
|
|
|||
|
|
@ -106,11 +106,18 @@ const ownersEndpoint = 'glob:*/api/v1/alert/related/owners?*';
|
|||
const databaseEndpoint = 'glob:*/api/v1/alert/related/database?*';
|
||||
const dashboardEndpoint = 'glob:*/api/v1/alert/related/dashboard?*';
|
||||
const chartEndpoint = 'glob:*/api/v1/alert/related/chart?*';
|
||||
const tabsEndpoint = 'glob:*/api/v1/dashboard/1/tabs';
|
||||
|
||||
fetchMock.get(ownersEndpoint, { result: [] });
|
||||
fetchMock.get(databaseEndpoint, { result: [] });
|
||||
fetchMock.get(dashboardEndpoint, { result: [] });
|
||||
fetchMock.get(chartEndpoint, { result: [{ text: 'table chart', value: 1 }] });
|
||||
fetchMock.get(tabsEndpoint, {
|
||||
result: {
|
||||
all_tabs: {},
|
||||
tab_tree: [],
|
||||
},
|
||||
});
|
||||
|
||||
// Create a valid alert with all required fields entered for validation check
|
||||
|
||||
|
|
@ -413,6 +420,21 @@ test('renders screenshot options when dashboard is selected', async () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders tab selection when Dashboard is selected', async () => {
|
||||
render(<AlertReportModal {...generateMockedProps(false, true, true)} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
userEvent.click(screen.getByTestId('contents-panel'));
|
||||
await screen.findByText(/test dashboard/i);
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: /select content type/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: /dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/select tab/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('changes to content options when chart is selected', async () => {
|
||||
render(<AlertReportModal {...generateMockedProps(false, true, true)} />, {
|
||||
useRedux: true,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import TimezoneSelector from 'src/components/TimezoneSelector';
|
|||
import { propertyComparator } from 'src/components/Select/utils';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import Owner from 'src/types/Owner';
|
||||
import { AntdCheckbox, AsyncSelect, Select } from 'src/components';
|
||||
import { AntdCheckbox, AsyncSelect, Select, TreeSelect } from 'src/components';
|
||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||
import { useCommonConf } from 'src/features/databases/state';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
|
|
@ -57,12 +57,16 @@ import {
|
|||
ChartObject,
|
||||
DashboardObject,
|
||||
DatabaseObject,
|
||||
Extra,
|
||||
MetaObject,
|
||||
Operator,
|
||||
Recipient,
|
||||
AlertsReportsConfig,
|
||||
ValidationObject,
|
||||
Sections,
|
||||
TabNode,
|
||||
SelectValue,
|
||||
ContentType,
|
||||
} from 'src/features/alerts/types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
|
|
@ -80,11 +84,6 @@ const TEXT_BASED_VISUALIZATION_TYPES = [
|
|||
'paired_ttest',
|
||||
];
|
||||
|
||||
type SelectValue = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface AlertReportModalProps {
|
||||
addSuccessToast: (msg: string) => void;
|
||||
addDangerToast: (msg: string) => void;
|
||||
|
|
@ -104,6 +103,12 @@ const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = [
|
|||
NotificationMethodOption.Email,
|
||||
];
|
||||
const DEFAULT_NOTIFICATION_FORMAT = 'PNG';
|
||||
const DEFAULT_EXTRA_DASHBOARD_OPTIONS: Extra = {
|
||||
dashboard: {
|
||||
anchor: '',
|
||||
},
|
||||
};
|
||||
|
||||
const CONDITIONS = [
|
||||
{
|
||||
label: t('< (Smaller than)'),
|
||||
|
|
@ -218,6 +223,10 @@ const StyledModal = styled(Modal)`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledTreeSelect = styled(TreeSelect)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSwitchContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -441,6 +450,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]);
|
||||
const [dashboardOptions, setDashboardOptions] = useState<MetaObject[]>([]);
|
||||
const [chartOptions, setChartOptions] = useState<MetaObject[]>([]);
|
||||
const [tabOptions, setTabOptions] = useState<TabNode[]>([]);
|
||||
|
||||
// Validation
|
||||
const [validationStatus, setValidationStatus] = useState<ValidationObject>({
|
||||
[Sections.General]: {
|
||||
|
|
@ -491,6 +502,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
const isEditMode = alert !== null;
|
||||
const formatOptionEnabled =
|
||||
isFeatureEnabled(FeatureFlag.AlertsAttachReports) || isReport;
|
||||
const tabsEnabled = isFeatureEnabled(FeatureFlag.AlertReportTabs);
|
||||
|
||||
const [notificationAddState, setNotificationAddState] =
|
||||
useState<NotificationAddStatus>('active');
|
||||
|
|
@ -547,6 +559,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
active: true,
|
||||
creation_method: 'alerts_reports',
|
||||
crontab: ALERT_REPORTS_DEFAULT_CRON_VALUE,
|
||||
extra: DEFAULT_EXTRA_DASHBOARD_OPTIONS,
|
||||
log_retention: ALERT_REPORTS_DEFAULT_RETENTION,
|
||||
working_timeout: ALERT_REPORTS_DEFAULT_WORKING_TIMEOUT,
|
||||
name: '',
|
||||
|
|
@ -595,6 +608,22 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
setNotificationAddState('active');
|
||||
};
|
||||
|
||||
const updateAnchorState = (value: any) => {
|
||||
setCurrentAlert(currentAlertData => {
|
||||
const dashboardState = currentAlertData?.extra?.dashboard;
|
||||
const extra = {
|
||||
dashboard: {
|
||||
...dashboardState,
|
||||
anchor: value,
|
||||
},
|
||||
};
|
||||
return {
|
||||
...currentAlertData,
|
||||
extra,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Alert fetch logic
|
||||
const {
|
||||
state: { loading, resource, error: fetchError },
|
||||
|
|
@ -631,7 +660,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
const shouldEnableForceScreenshot = contentType === 'chart' && !isReport;
|
||||
const shouldEnableForceScreenshot =
|
||||
contentType === ContentType.Chart && !isReport;
|
||||
const data: any = {
|
||||
...currentAlert,
|
||||
type: isReport ? 'Report' : 'Alert',
|
||||
|
|
@ -640,9 +670,12 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
validator_config_json: conditionNotNull
|
||||
? {}
|
||||
: currentAlert?.validator_config_json,
|
||||
chart: contentType === 'chart' ? currentAlert?.chart?.value : null,
|
||||
chart:
|
||||
contentType === ContentType.Chart ? currentAlert?.chart?.value : null,
|
||||
dashboard:
|
||||
contentType === 'dashboard' ? currentAlert?.dashboard?.value : null,
|
||||
contentType === ContentType.Dashboard
|
||||
? currentAlert?.dashboard?.value
|
||||
: null,
|
||||
custom_width: isScreenshot ? currentAlert?.custom_width : undefined,
|
||||
database: currentAlert?.database?.value,
|
||||
owners: (currentAlert?.owners || []).map(
|
||||
|
|
@ -650,6 +683,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
),
|
||||
recipients,
|
||||
report_format: reportFormat || DEFAULT_NOTIFICATION_FORMAT,
|
||||
extra: contentType === ContentType.Dashboard ? currentAlert?.extra : null,
|
||||
};
|
||||
|
||||
if (data.recipients && !data.recipients.length) {
|
||||
|
|
@ -657,7 +691,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
}
|
||||
|
||||
data.context_markdown = 'string';
|
||||
|
||||
if (isEditMode) {
|
||||
// Edit
|
||||
if (currentAlert?.id) {
|
||||
|
|
@ -780,6 +813,28 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
[],
|
||||
);
|
||||
|
||||
const dashboard = currentAlert?.dashboard;
|
||||
useEffect(() => {
|
||||
if (!tabsEnabled) return;
|
||||
|
||||
if (dashboard?.value) {
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/${dashboard.value}/tabs`,
|
||||
})
|
||||
.then(response => {
|
||||
const { tab_tree: tabTree, all_tabs: allTabs } = response.json.result;
|
||||
setTabOptions(tabTree);
|
||||
const anchor = currentAlert?.extra?.dashboard?.anchor;
|
||||
if (anchor && !(anchor in allTabs)) {
|
||||
updateAnchorState(undefined);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(t('There was an error retrieving dashboard tabs.'));
|
||||
});
|
||||
}
|
||||
}, [dashboard, tabsEnabled, currentAlert?.extra, addDangerToast]);
|
||||
|
||||
const databaseLabel = currentAlert?.database && !currentAlert.database.label;
|
||||
useEffect(() => {
|
||||
// Find source if current alert has one set
|
||||
|
|
@ -891,6 +946,27 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
endpoint: `/api/v1/chart/${chart.value}`,
|
||||
}).then(response => setChartVizType(response.json.result.viz_type));
|
||||
|
||||
const updateEmailSubject = () => {
|
||||
const chartLabel = currentAlert?.chart?.label;
|
||||
const dashboardLabel = currentAlert?.dashboard?.label;
|
||||
if (!currentAlert?.name) {
|
||||
setEmailSubject('');
|
||||
return;
|
||||
}
|
||||
switch (contentType) {
|
||||
case ContentType.Chart:
|
||||
setEmailSubject(`${currentAlert?.name}: ${chartLabel || ''}`);
|
||||
break;
|
||||
|
||||
case ContentType.Dashboard:
|
||||
setEmailSubject(`${currentAlert?.name}: ${dashboardLabel || ''}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
setEmailSubject('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle input/textarea updates
|
||||
const onInputChange = (
|
||||
event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
|
|
@ -943,6 +1019,10 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
const onDashboardChange = (dashboard: SelectValue) => {
|
||||
updateAlertState('dashboard', dashboard || undefined);
|
||||
updateAlertState('chart', null);
|
||||
if (tabsEnabled) {
|
||||
setTabOptions([]);
|
||||
updateAnchorState('');
|
||||
}
|
||||
};
|
||||
|
||||
const onChartChange = (chart: SelectValue) => {
|
||||
|
|
@ -1057,8 +1137,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
const errors = [];
|
||||
if (
|
||||
!(
|
||||
(contentType === 'dashboard' && !!currentAlert?.dashboard) ||
|
||||
(contentType === 'chart' && !!currentAlert?.chart)
|
||||
(contentType === ContentType.Dashboard && !!currentAlert?.dashboard) ||
|
||||
(contentType === ContentType.Chart && !!currentAlert?.chart)
|
||||
)
|
||||
) {
|
||||
errors.push(TRANSLATIONS.CONTENT_ERROR_TEXT);
|
||||
|
|
@ -1206,7 +1286,9 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
? 'hidden'
|
||||
: 'active',
|
||||
);
|
||||
setContentType(resource.chart ? 'chart' : 'dashboard');
|
||||
setContentType(
|
||||
resource.chart ? ContentType.Chart : ContentType.Dashboard,
|
||||
);
|
||||
setReportFormat(resource.report_format || DEFAULT_NOTIFICATION_FORMAT);
|
||||
const validatorConfig =
|
||||
typeof resource.validator_config_json === 'string'
|
||||
|
|
@ -1321,28 +1403,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
return titleText;
|
||||
};
|
||||
|
||||
const updateEmailSubject = () => {
|
||||
if (contentType === 'chart') {
|
||||
if (currentAlert?.name || currentAlert?.chart?.label) {
|
||||
setEmailSubject(
|
||||
`${currentAlert?.name}: ${currentAlert?.chart?.label || ''}`,
|
||||
);
|
||||
} else {
|
||||
setEmailSubject('');
|
||||
}
|
||||
} else if (contentType === 'dashboard') {
|
||||
if (currentAlert?.name || currentAlert?.dashboard?.label) {
|
||||
setEmailSubject(
|
||||
`${currentAlert?.name}: ${currentAlert?.dashboard?.label || ''}`,
|
||||
);
|
||||
} else {
|
||||
setEmailSubject('');
|
||||
}
|
||||
} else {
|
||||
setEmailSubject('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleErrorUpdate = (hasError: boolean) => {
|
||||
setEmailError(hasError);
|
||||
};
|
||||
|
|
@ -1586,7 +1646,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
/>
|
||||
</StyledInputContainer>
|
||||
<StyledInputContainer>
|
||||
{contentType === 'chart' ? (
|
||||
{contentType === ContentType.Chart ? (
|
||||
<>
|
||||
<div className="control-label">
|
||||
{t('Select chart')}
|
||||
|
|
@ -1649,7 +1709,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
onChange={onFormatChange}
|
||||
value={reportFormat}
|
||||
options={
|
||||
contentType === 'dashboard'
|
||||
contentType === ContentType.Dashboard
|
||||
? ['pdf', 'png'].map(key => FORMAT_OPTIONS[key])
|
||||
: /* If chart is of text based viz type: show text
|
||||
format option */
|
||||
|
|
@ -1662,9 +1722,25 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
</>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
{tabsEnabled && contentType === ContentType.Dashboard && (
|
||||
<StyledInputContainer>
|
||||
<>
|
||||
<div className="control-label">{t('Select tab')}</div>
|
||||
<StyledTreeSelect
|
||||
disabled={tabOptions?.length === 0}
|
||||
treeData={tabOptions}
|
||||
value={currentAlert?.extra?.dashboard?.anchor}
|
||||
onSelect={updateAnchorState}
|
||||
placeholder={t('Select a tab')}
|
||||
/>
|
||||
</>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
{isScreenshot && (
|
||||
<StyledInputContainer
|
||||
css={!isReport && contentType === 'chart' && noMarginBottom}
|
||||
css={
|
||||
!isReport && contentType === ContentType.Chart && noMarginBottom
|
||||
}
|
||||
>
|
||||
<div className="control-label">{t('Screenshot width')}</div>
|
||||
<div className="input-container">
|
||||
|
|
@ -1680,7 +1756,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
</div>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
{(isReport || contentType === 'dashboard') && (
|
||||
{(isReport || contentType === ContentType.Dashboard) && (
|
||||
<div className="inline-container">
|
||||
<StyledCheckbox
|
||||
data-test="bypass-cache"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ export enum NotificationMethodOption {
|
|||
SlackV2 = 'SlackV2',
|
||||
}
|
||||
|
||||
export type SelectValue = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type NotificationSetting = {
|
||||
method?: NotificationMethodOption;
|
||||
recipients: string;
|
||||
|
|
@ -62,6 +67,12 @@ export type SlackChannel = {
|
|||
is_private: boolean;
|
||||
};
|
||||
|
||||
export type TabNode = {
|
||||
title: string;
|
||||
value: string;
|
||||
children?: TabNode[];
|
||||
};
|
||||
|
||||
export type Recipient = {
|
||||
recipient_config_json: {
|
||||
target: string;
|
||||
|
|
@ -77,6 +88,16 @@ export type MetaObject = {
|
|||
value?: number | string;
|
||||
};
|
||||
|
||||
export type DashboardState = {
|
||||
activeTabs?: Array<string>;
|
||||
dataMask?: Object;
|
||||
anchor?: string;
|
||||
};
|
||||
|
||||
export type Extra = {
|
||||
dashboard?: DashboardState;
|
||||
};
|
||||
|
||||
export type Operator = '<' | '>' | '<=' | '>=' | '==' | '!=' | 'not null';
|
||||
|
||||
export type AlertObject = {
|
||||
|
|
@ -96,6 +117,7 @@ export type AlertObject = {
|
|||
description?: string;
|
||||
email_subject?: string;
|
||||
error?: string;
|
||||
extra?: Extra;
|
||||
force_screenshot: boolean;
|
||||
grace_period?: number;
|
||||
id: number;
|
||||
|
|
@ -164,3 +186,8 @@ export enum Sections {
|
|||
Schedule = 'scheduleSection',
|
||||
Notification = 'notificationSection',
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
Dashboard = 'dashboard',
|
||||
Chart = 'chart',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ function AlertList({
|
|||
toggleBulkSelect,
|
||||
} = useListViewResource<AlertObject>(
|
||||
'report',
|
||||
t('reports'),
|
||||
t('report'),
|
||||
addDangerToast,
|
||||
true,
|
||||
undefined,
|
||||
|
|
|
|||
|
|
@ -207,15 +207,15 @@ class BaseReportState:
|
|||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# If we need to render dashboard in a specific state, use stateful permalink
|
||||
if dashboard_state := self._report_schedule.extra.get("dashboard"):
|
||||
if (
|
||||
dashboard_state := self._report_schedule.extra.get("dashboard")
|
||||
) and feature_flag_manager.is_feature_enabled("ALERT_REPORT_TABS"):
|
||||
permalink_key = CreateDashboardPermalinkCommand(
|
||||
dashboard_id=str(self._report_schedule.dashboard.uuid),
|
||||
state=dashboard_state,
|
||||
).run()
|
||||
return get_url_path("Superset.dashboard_permalink", key=permalink_key)
|
||||
|
||||
dashboard = self._report_schedule.dashboard
|
||||
dashboard_id_or_slug = (
|
||||
dashboard.uuid if dashboard and dashboard.uuid else dashboard.id
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ class TabSchema(Schema):
|
|||
children = fields.List(fields.Nested(lambda: TabSchema()))
|
||||
value = fields.Str()
|
||||
title = fields.Str()
|
||||
parents = fields.List(fields.Str())
|
||||
|
||||
|
||||
class TabsPayloadSchema(Schema):
|
||||
|
|
|
|||
|
|
@ -248,7 +248,6 @@ class DashboardScreenshot(BaseScreenshot):
|
|||
url,
|
||||
standalone=DashboardStandaloneMode.REPORT.value,
|
||||
)
|
||||
|
||||
super().__init__(url, digest)
|
||||
self.window_size = window_size or DEFAULT_DASHBOARD_WINDOW_SIZE
|
||||
self.thumb_size = thumb_size or DEFAULT_DASHBOARD_THUMBNAIL_SIZE
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ from datetime import datetime
|
|||
from unittest.mock import MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from flask import current_app
|
||||
|
||||
from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
|
||||
from superset.commands.report.execute import AsyncExecuteReportScheduleCommand
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.reports.models import ReportSourceFormat
|
||||
from superset.utils.urls import get_url_path
|
||||
from tests.integration_tests.fixtures.tabbed_dashboard import (
|
||||
tabbed_dashboard, # noqa: F401
|
||||
)
|
||||
|
|
@ -34,22 +36,21 @@ from tests.integration_tests.reports.utils import create_dashboard_report
|
|||
@patch(
|
||||
"superset.commands.report.execute.DashboardScreenshot",
|
||||
)
|
||||
@patch(
|
||||
"superset.commands.dashboard.permalink.create.CreateDashboardPermalinkCommand.run"
|
||||
@patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags", ALERT_REPORT_TABS=True
|
||||
)
|
||||
@pytest.mark.usefixtures("login_as_admin")
|
||||
def test_report_for_dashboard_with_tabs(
|
||||
create_dashboard_permalink_mock: MagicMock,
|
||||
dashboard_screenshot_mock: MagicMock,
|
||||
send_email_smtp_mock: MagicMock,
|
||||
tabbed_dashboard: Dashboard, # noqa: F811
|
||||
) -> None:
|
||||
create_dashboard_permalink_mock.return_value = "permalink"
|
||||
dashboard_screenshot_mock.get_screenshot.return_value = b"test-image"
|
||||
current_app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"] = False
|
||||
|
||||
with create_dashboard_report(
|
||||
dashboard=tabbed_dashboard,
|
||||
extra={"active_tabs": ["TAB-L1B", "TAB-L2BB"]},
|
||||
extra={"dashboard": {"active_tabs": ["TAB-L1B", "TAB-L2BB"]}},
|
||||
name="test report tabbed dashboard",
|
||||
) as report_schedule:
|
||||
dashboard: Dashboard = report_schedule.dashboard
|
||||
|
|
@ -61,9 +62,12 @@ def test_report_for_dashboard_with_tabs(
|
|||
str(dashboard.id), dashboard_state
|
||||
).run()
|
||||
|
||||
expected_url = get_url_path("Superset.dashboard_permalink", key=permalink_key)
|
||||
|
||||
assert dashboard_screenshot_mock.call_count == 1
|
||||
url = dashboard_screenshot_mock.call_args.args[0]
|
||||
assert url.endswith(f"/superset/dashboard/p/{permalink_key}/")
|
||||
called_url = dashboard_screenshot_mock.call_args.args[0]
|
||||
|
||||
assert called_url == expected_url
|
||||
assert send_email_smtp_mock.call_count == 1
|
||||
assert len(send_email_smtp_mock.call_args.kwargs["images"]) == 1
|
||||
|
||||
|
|
@ -72,22 +76,21 @@ def test_report_for_dashboard_with_tabs(
|
|||
@patch(
|
||||
"superset.commands.report.execute.DashboardScreenshot",
|
||||
)
|
||||
@patch(
|
||||
"superset.commands.dashboard.permalink.create.CreateDashboardPermalinkCommand.run"
|
||||
@patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags", ALERT_REPORT_TABS=True
|
||||
)
|
||||
@pytest.mark.usefixtures("login_as_admin")
|
||||
def test_report_with_header_data(
|
||||
create_dashboard_permalink_mock: MagicMock,
|
||||
dashboard_screenshot_mock: MagicMock,
|
||||
send_email_smtp_mock: MagicMock,
|
||||
tabbed_dashboard: Dashboard, # noqa: F811
|
||||
) -> None:
|
||||
create_dashboard_permalink_mock.return_value = "permalink"
|
||||
dashboard_screenshot_mock.get_screenshot.return_value = b"test-image"
|
||||
current_app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"] = False
|
||||
|
||||
with create_dashboard_report(
|
||||
dashboard=tabbed_dashboard,
|
||||
extra={"active_tabs": ["TAB-L1B"]},
|
||||
extra={"dashboard": {"active_tabs": ["TAB-L1B", "TAB-L2BB"]}},
|
||||
name="test report tabbed dashboard",
|
||||
) as report_schedule:
|
||||
dashboard: Dashboard = report_schedule.dashboard
|
||||
|
|
@ -101,6 +104,7 @@ def test_report_with_header_data(
|
|||
|
||||
assert dashboard_screenshot_mock.call_count == 1
|
||||
url = dashboard_screenshot_mock.call_args.args[0]
|
||||
|
||||
assert url.endswith(f"/superset/dashboard/p/{permalink_key}/")
|
||||
assert send_email_smtp_mock.call_count == 1
|
||||
header_data = send_email_smtp_mock.call_args.kwargs["header_data"]
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ from superset.commands.report.execute import (
|
|||
)
|
||||
from superset.commands.report.log_prune import AsyncPruneReportScheduleLogCommand
|
||||
from superset.exceptions import SupersetException
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
|
|
@ -82,6 +83,9 @@ from tests.integration_tests.fixtures.birth_names_dashboard import (
|
|||
load_birth_names_dashboard_with_slices, # noqa: F401
|
||||
load_birth_names_data, # noqa: F401
|
||||
)
|
||||
from tests.integration_tests.fixtures.tabbed_dashboard import (
|
||||
tabbed_dashboard, # noqa: F401
|
||||
)
|
||||
from tests.integration_tests.fixtures.world_bank_dashboard import (
|
||||
load_world_bank_dashboard_with_slices_module_scope, # noqa: F401
|
||||
load_world_bank_data, # noqa: F401
|
||||
|
|
@ -91,6 +95,7 @@ from tests.integration_tests.reports.utils import (
|
|||
create_report_notification,
|
||||
CSV_FILE,
|
||||
DEFAULT_OWNER_EMAIL,
|
||||
reset_key_values,
|
||||
SCREENSHOT_FILE,
|
||||
TEST_ID,
|
||||
)
|
||||
|
|
@ -1170,6 +1175,93 @@ def test_email_dashboard_report_schedule(
|
|||
statsd_mock.assert_called_once_with("reports.email.send.ok", 1)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tabbed_dashboard")
|
||||
@patch("superset.utils.screenshots.DashboardScreenshot.get_screenshot")
|
||||
@patch("superset.reports.notifications.email.send_email_smtp")
|
||||
@patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags", ALERT_REPORT_TABS=True
|
||||
)
|
||||
def test_email_dashboard_report_schedule_with_tab_anchor(
|
||||
_email_mock,
|
||||
_screenshot_mock,
|
||||
):
|
||||
"""
|
||||
ExecuteReport Command: Test dashboard email report schedule with tab metadata
|
||||
"""
|
||||
with freeze_time("2020-01-01T00:00:00Z"):
|
||||
with patch.object(current_app.config["STATS_LOGGER"], "gauge") as statsd_mock:
|
||||
# get tabbed dashboard fixture
|
||||
dashboard = db.session.query(Dashboard).all()[1]
|
||||
# build report_schedule
|
||||
report_schedule = create_report_notification(
|
||||
email_target="target@email.com",
|
||||
dashboard=dashboard,
|
||||
extra={"dashboard": {"anchor": "TAB-L2AB"}},
|
||||
)
|
||||
AsyncExecuteReportScheduleCommand(
|
||||
TEST_ID, report_schedule.id, datetime.utcnow()
|
||||
).run()
|
||||
|
||||
# Assert logs are correct
|
||||
assert_log(ReportState.SUCCESS)
|
||||
statsd_mock.assert_called_once_with("reports.email.send.ok", 1)
|
||||
|
||||
pl = (
|
||||
db.session.query(KeyValueEntry)
|
||||
.order_by(KeyValueEntry.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
value = json.loads(pl.value)
|
||||
# test that report schedule extra json matches permalink state
|
||||
assert report_schedule.extra["dashboard"] == value["state"]
|
||||
|
||||
# remove report_schedule
|
||||
cleanup_report_schedule(report_schedule)
|
||||
# remove permalink kvalues
|
||||
reset_key_values()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tabbed_dashboard")
|
||||
@patch("superset.utils.screenshots.DashboardScreenshot.get_screenshot")
|
||||
@patch("superset.reports.notifications.email.send_email_smtp")
|
||||
@patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags", ALERT_REPORT_TABS=False
|
||||
)
|
||||
def test_email_dashboard_report_schedule_disabled_tabs(
|
||||
_email_mock,
|
||||
_screenshot_mock,
|
||||
):
|
||||
"""
|
||||
ExecuteReport Command: Test dashboard email report schedule with tab metadata
|
||||
"""
|
||||
with freeze_time("2020-01-01T00:00:00Z"):
|
||||
with patch.object(current_app.config["STATS_LOGGER"], "gauge") as statsd_mock:
|
||||
# get tabbed dashboard fixture
|
||||
dashboard = db.session.query(Dashboard).all()[1]
|
||||
# build report_schedule
|
||||
report_schedule = create_report_notification(
|
||||
email_target="target@email.com",
|
||||
dashboard=dashboard,
|
||||
extra={"dashboard": {"anchor": "TAB-L2AB"}},
|
||||
)
|
||||
AsyncExecuteReportScheduleCommand(
|
||||
TEST_ID, report_schedule.id, datetime.utcnow()
|
||||
).run()
|
||||
|
||||
# Assert logs are correct
|
||||
assert_log(ReportState.SUCCESS)
|
||||
statsd_mock.assert_called_once_with("reports.email.send.ok", 1)
|
||||
|
||||
permalinks = db.session.query(KeyValueEntry).all()
|
||||
|
||||
# test that report schedule extra json matches permalink state
|
||||
assert len(permalinks) == 0
|
||||
|
||||
# remove report_schedule
|
||||
cleanup_report_schedule(report_schedule)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"load_birth_names_dashboard_with_slices",
|
||||
"create_report_email_dashboard_force_screenshot",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from uuid import uuid4
|
|||
from flask_appbuilder.security.sqla.models import User
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
from superset.models.core import Database
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
|
|
@ -203,3 +204,8 @@ def create_dashboard_report(dashboard, extra, **kwargs):
|
|||
|
||||
if error:
|
||||
raise error
|
||||
|
||||
|
||||
def reset_key_values() -> None:
|
||||
db.session.query(KeyValueEntry).delete()
|
||||
db.session.commit()
|
||||
|
|
|
|||
Loading…
Reference in New Issue