diff --git a/superset-frontend/src/components/index.ts b/superset-frontend/src/components/index.ts index 4c425b053464e..9e50f5e71b348 100644 --- a/superset-frontend/src/components/index.ts +++ b/superset-frontend/src/components/index.ts @@ -44,6 +44,7 @@ export { Steps, Tag, Tree, + TreeSelect, Typography, Upload, } from 'antd'; diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index 6d8c1df65abcc..dbd55841f4e2b 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -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(, { + 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(, { useRedux: true, diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx b/superset-frontend/src/features/alerts/AlertReportModal.tsx index aad229955212e..1b56ed19d633e 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx @@ -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 = ({ const [sourceOptions, setSourceOptions] = useState([]); const [dashboardOptions, setDashboardOptions] = useState([]); const [chartOptions, setChartOptions] = useState([]); + const [tabOptions, setTabOptions] = useState([]); + // Validation const [validationStatus, setValidationStatus] = useState({ [Sections.General]: { @@ -491,6 +502,7 @@ const AlertReportModal: FunctionComponent = ({ const isEditMode = alert !== null; const formatOptionEnabled = isFeatureEnabled(FeatureFlag.AlertsAttachReports) || isReport; + const tabsEnabled = isFeatureEnabled(FeatureFlag.AlertReportTabs); const [notificationAddState, setNotificationAddState] = useState('active'); @@ -547,6 +559,7 @@ const AlertReportModal: FunctionComponent = ({ 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 = ({ 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 = ({ } }); - 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 = ({ 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 = ({ ), 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 = ({ } data.context_markdown = 'string'; - if (isEditMode) { // Edit if (currentAlert?.id) { @@ -780,6 +813,28 @@ const AlertReportModal: FunctionComponent = ({ [], ); + 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 = ({ 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, @@ -943,6 +1019,10 @@ const AlertReportModal: FunctionComponent = ({ 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 = ({ 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 = ({ ? '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 = ({ 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 = ({ /> - {contentType === 'chart' ? ( + {contentType === ContentType.Chart ? ( <>
{t('Select chart')} @@ -1649,7 +1709,7 @@ const AlertReportModal: FunctionComponent = ({ 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 = ({ )} + {tabsEnabled && contentType === ContentType.Dashboard && ( + + <> +
{t('Select tab')}
+ + +
+ )} {isScreenshot && (
{t('Screenshot width')}
@@ -1680,7 +1756,7 @@ const AlertReportModal: FunctionComponent = ({
)} - {(isReport || contentType === 'dashboard') && ( + {(isReport || contentType === ContentType.Dashboard) && (
; + 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', +} diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx b/superset-frontend/src/pages/AlertReportList/index.tsx index 9bb3705f1fd61..bb932f29bef1a 100644 --- a/superset-frontend/src/pages/AlertReportList/index.tsx +++ b/superset-frontend/src/pages/AlertReportList/index.tsx @@ -137,7 +137,7 @@ function AlertList({ toggleBulkSelect, } = useListViewResource( 'report', - t('reports'), + t('report'), addDangerToast, true, undefined, diff --git a/superset/commands/report/execute.py b/superset/commands/report/execute.py index 3ec5bdfa97b22..bfeb2a678746a 100644 --- a/superset/commands/report/execute.py +++ b/superset/commands/report/execute.py @@ -207,15 +207,15 @@ def _get_url( 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 diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index 1f78a22358490..b0a47aba414ce 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -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): diff --git a/superset/utils/screenshots.py b/superset/utils/screenshots.py index bf6ed0f9e8493..a7411ef781607 100644 --- a/superset/utils/screenshots.py +++ b/superset/utils/screenshots.py @@ -248,7 +248,6 @@ def __init__( 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 diff --git a/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py b/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py index f2722596f803f..82021a5468eff 100644 --- a/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py +++ b/tests/integration_tests/reports/commands/execute_dashboard_report_tests.py @@ -18,12 +18,14 @@ 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 @@ @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"] diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py index 773073d4db0f8..74268d8be3411 100644 --- a/tests/integration_tests/reports/commands_tests.py +++ b/tests/integration_tests/reports/commands_tests.py @@ -60,6 +60,7 @@ ) 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 @@ 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 @@ 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", diff --git a/tests/integration_tests/reports/utils.py b/tests/integration_tests/reports/utils.py index 6cd90b769df8d..d0f00270b9cd2 100644 --- a/tests/integration_tests/reports/utils.py +++ b/tests/integration_tests/reports/utils.py @@ -22,6 +22,7 @@ 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()