mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat(alerts): enable tab selection for dashboard alerts/reports (#29096)
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user