Skip to content

Commit

Permalink
feat(alerts): enable tab selection for dashboard alerts/reports (#29096)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisjac authored Jul 30, 2024
1 parent 6bf8596 commit d21d759
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 56 deletions.
1 change: 1 addition & 0 deletions superset-frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export {
Steps,
Tag,
Tree,
TreeSelect,
Typography,
Upload,
} from 'antd';
Expand Down
22 changes: 22 additions & 0 deletions superset-frontend/src/features/alerts/AlertReportModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
154 changes: 115 additions & 39 deletions superset-frontend/src/features/alerts/AlertReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand All @@ -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)'),
Expand Down Expand Up @@ -218,6 +223,10 @@ const StyledModal = styled(Modal)`
}
`;

const StyledTreeSelect = styled(TreeSelect)`
width: 100%;
`;

const StyledSwitchContainer = styled.div`
display: flex;
align-items: center;
Expand Down Expand Up @@ -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]: {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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: '',
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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',
Expand All @@ -640,24 +670,27 @@ 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(
owner => (owner as MetaObject).value || owner.id,
),
recipients,
report_format: reportFormat || DEFAULT_NOTIFICATION_FORMAT,
extra: contentType === ContentType.Dashboard ? currentAlert?.extra : null,
};

if (data.recipients && !data.recipients.length) {
delete data.recipients;
}

data.context_markdown = 'string';

if (isEditMode) {
// Edit
if (currentAlert?.id) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -1586,7 +1646,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
/>
</StyledInputContainer>
<StyledInputContainer>
{contentType === 'chart' ? (
{contentType === ContentType.Chart ? (
<>
<div className="control-label">
{t('Select chart')}
Expand Down Expand Up @@ -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 */
Expand All @@ -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">
Expand All @@ -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"
Expand Down
Loading

0 comments on commit d21d759

Please sign in to comment.