Skip to content

feat: Add Dashboard Filter Support for Alert Reports #32196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 91 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
38bb661
placeholder dropdowns
hughhhh Jan 15, 2025
61d7c4f
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Jan 23, 2025
86ae7cc
filter dropdown working
hughhhh Jan 24, 2025
ecee690
saving ui work for now
hughhhh Jan 29, 2025
25d2850
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Jan 29, 2025
ef0a7ee
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Feb 3, 2025
16751c0
save
hughhhh Feb 7, 2025
47fc7de
remove print
hughhhh Feb 9, 2025
daf48cc
fixed params
hughhhh Feb 9, 2025
9b59d3a
allow for multiselect
hughhhh Feb 9, 2025
5fd0517
set edit
hughhhh Feb 10, 2025
689605d
update
hughhhh Feb 10, 2025
bf31b9d
ok add label
hughhhh Feb 10, 2025
2c06217
add filter additions
hughhhh Feb 13, 2025
c5c6d53
working multiple
hughhhh Feb 15, 2025
5202544
proper handle edit
hughhhh Feb 16, 2025
70ac1e7
ok
hughhhh Feb 16, 2025
24db175
save
hughhhh Feb 17, 2025
3da7656
fix colors
hughhhh Feb 22, 2025
81ace0f
update test
hughhhh Feb 22, 2025
1a41780
fix alignment
hughhhh Mar 1, 2025
45e1407
add tooltip
hughhhh Mar 1, 2025
10f2e0e
update filters
hughhhh Mar 3, 2025
082461b
ok
hughhhh Mar 3, 2025
6ab40e3
moved to emotion
hughhhh Mar 9, 2025
276d0f6
working
hughhhh Mar 9, 2025
a115b17
fix merge
hughhhh Mar 9, 2025
dae9428
fix linting
hughhhh Mar 9, 2025
263b290
lint
hughhhh Mar 9, 2025
def42d0
fix linting
hughhhh Mar 9, 2025
d20b764
linting
hughhhh Mar 10, 2025
f29c7e1
ignores on mypy
hughhhh Mar 11, 2025
a09b2a5
ok
hughhhh Mar 16, 2025
5d228f0
fix ci
hughhhh Mar 17, 2025
64bc357
ok
hughhhh Mar 18, 2025
3d40ece
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Mar 18, 2025
3efac9e
fix index
hughhhh Mar 18, 2025
945c467
fix duplicates on edit
hughhhh Mar 18, 2025
ebbe945
fix test
hughhhh Mar 18, 2025
f03cf9f
bad import
hughhhh Mar 18, 2025
b61e361
Merge branch 'master' into hm/ar-filters
eschutho Mar 18, 2025
e07fdb3
fix test
hughhhh Mar 19, 2025
951205f
fix ci fe build
hughhhh Mar 19, 2025
ee8862f
guard statement
hughhhh Mar 20, 2025
ae03821
fix Icons update
hughhhh Mar 25, 2025
4ce0b97
fix disables states
hughhhh Mar 27, 2025
243ce8d
allow filter selection without tabs
hughhhh Mar 27, 2025
32afe30
allowing for all filters to be query for better state
hughhhh Apr 2, 2025
8afdf82
ok
hughhhh Apr 3, 2025
b645634
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Apr 3, 2025
fde45f0
ok
hughhhh Apr 4, 2025
bd25d3c
update state management for adding new filters
hughhhh Apr 7, 2025
424d59f
add feature flag
hughhhh Apr 8, 2025
0d1422b
fix state with edit
hughhhh Apr 8, 2025
f2af658
add guard for .all
hughhhh Apr 8, 2025
cfce539
saving here for time_grain + time_column options
hughhhh Apr 15, 2025
3947ed3
remove console.logs
hughhhh Apr 17, 2025
bfd8694
fix encoding problem
hughhhh Apr 18, 2025
14d341a
fix unit test
hughhhh Apr 18, 2025
62b6b88
fix test
hughhhh Apr 19, 2025
0118aa2
fix test
hughhhh Apr 19, 2025
727d361
fix linting
hughhhh Apr 20, 2025
82c4d3e
restrict number of filters based on options
hughhhh Apr 20, 2025
3ce834e
fix length check
hughhhh Apr 21, 2025
7a043a8
fix ts-ignore
hughhhh Apr 28, 2025
83a1b99
fix more linting
hughhhh Apr 29, 2025
1b4e734
ok
hughhhh Apr 29, 2025
b0bd21d
transfer
hughhhh Apr 29, 2025
4763cd6
add small render test for dropdown
hughhhh May 1, 2025
029b682
change value
hughhhh May 7, 2025
2e6b69a
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh May 7, 2025
519185c
do not allow filter_range type
hughhhh May 9, 2025
0b644bd
trash size
hughhhh May 9, 2025
3b14e11
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh May 9, 2025
be87858
fix select
hughhhh May 9, 2025
667be47
fix linting
hughhhh May 9, 2025
d1e0e04
fix edit
hughhhh May 20, 2025
e5c463a
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh May 22, 2025
b47fac6
Merge branch 'master' of https://github.com/apache/superset into hm/a…
hughhhh Jun 2, 2025
f1280c2
add filter for timecolumn
hughhhh Jun 2, 2025
e6caa17
fix hardcoded
hughhhh Jun 24, 2025
178b08c
Merge remote-tracking branch 'origin/master' into hm/ar-filters
hughhhh Jun 24, 2025
7dd4b5d
convert gridUnit to sizeUnit
hughhhh Jun 24, 2025
107e269
cleanup
hughhhh Jun 24, 2025
3c86c5f
Merge branch 'master' of https://github.com/apache/superset into hm/a…
Jul 11, 2025
22df03f
add adhoc filters to values query
Jul 11, 2025
90a29fe
add error states
Jul 11, 2025
4f5b21f
filter range v1
Jul 12, 2025
c1a215f
fix linting
Jul 12, 2025
3026fcc
fix test
Jul 12, 2025
7e0faad
o
Jul 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion superset-frontend/src/features/alerts/AlertReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import { NotificationMethod } from './components/NotificationMethod';
import ValidatedPanelHeader from './components/ValidatedPanelHeader';
import StyledPanel from './components/StyledPanel';
import { buildErrorTooltipMessage } from './buildErrorTooltipMessage';
import { getChartDataRequest } from 'src/components/Chart/chartAction';
import { getFormData } from '../../utils';

const TIMEOUT_MIN = 1;
const TEXT_BASED_VISUALIZATION_TYPES = [
Expand Down Expand Up @@ -454,6 +456,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const [dashboardOptions, setDashboardOptions] = useState<MetaObject[]>([]);
const [chartOptions, setChartOptions] = useState<MetaObject[]>([]);
const [tabOptions, setTabOptions] = useState<TabNode[]>([]);
const [nativeFilterOptions, setNativeFilterOptions] = useState<object>([]);
const [nativeFilterValues, setNativeFilterValues] = useState<object>([]);

// todo(hughhh): refactor to handle multiple native filters
const [nativeFilter, setSelectedNativeFilter] = useState<object>({});
const [nativeFilters, setGlobalNativeFilters] = useState<object>({});

console.log('okkkkk', nativeFilter)

// Validation
const [validationStatus, setValidationStatus] = useState<ValidationObject>({
Expand Down Expand Up @@ -665,6 +675,11 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({

const shouldEnableForceScreenshot =
contentType === ContentType.Chart && !isReport;


// todo(hughhh): refactor to handle multiple native filters
currentAlert.extra.dashboard.nativeFilters = [nativeFilter]

const data: any = {
...currentAlert,
type: isReport ? 'Report' : 'Alert',
Expand Down Expand Up @@ -825,16 +840,24 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
endpoint: `/api/v1/dashboard/${dashboard.value}/tabs`,
})
.then(response => {
const { tab_tree: tabTree, all_tabs: allTabs } = response.json.result;
console.log(response.json.result);
const { tab_tree: tabTree, all_tabs: allTabs, native_filters: nativeFilters } = response.json.result;
tabTree.push({
title: 'All Tabs',
// select tree only works with string value
value: JSON.stringify(Object.keys(allTabs)),
});
setTabOptions(tabTree);
setGlobalNativeFilters(nativeFilters);

const anchor = currentAlert?.extra?.dashboard?.anchor;
if (anchor) {
setNativeFilterOptions(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to handle both setTabNativeFilters and setNativeFilterOptions? It seems that nativeFiltersOptions can be extracted from the former

nativeFilters[anchor].map((filter: any) => ({
value: filter.id,
label: filter.name,
})),
);
try {
const parsedAnchor = JSON.parse(anchor);
if (Array.isArray(parsedAnchor)) {
Expand Down Expand Up @@ -878,6 +901,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
return SupersetClient.get({
endpoint: `/api/v1/report/related/dashboard?q=${query}`,
}).then(response => {
console.log('dashboards', response.json.result);
const list = response.json.result.map(
(item: { value: number; text: string }) => ({
value: item.value,
Expand Down Expand Up @@ -1051,6 +1075,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
updateAlertState('chart', null);
if (tabsEnabled) {
setTabOptions([]);
setNativeFilterOptions([]);
updateAnchorState('');
}
};
Expand Down Expand Up @@ -1111,6 +1136,61 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
setForceScreenshot(event.target.checked);
};

const onChangeDashboardFilter = (nativeFilterId: string) => {
console.log('dashboardFilter', nativeFilterId);
// set dashboardFilter
const anchor = currentAlert?.extra?.dashboard?.anchor
const inScopeFilters = nativeFilters[anchor];
const filter = inScopeFilters.filter((f: any) => f.id === nativeFilterId)[0]
console.log(filter) // use filter to grab values from API
console.log('about to get filter values', nativeFilterId);

const datasetId = filter.targets[0].datasetId;
const columnName = filter.targets[0].column.name;
const dashboardId = currentAlert?.dashboard?.value;

// Get values tied to the selected filter
const filterValues = {
formData: {
// enableEmptyFilter: false,
// defaultToFirstItem: false,
// multiSelect: true,
// searchAllOptions: false,
// inverseSelection: false,
datasource: `${datasetId}__table`,
groupby: [columnName],
// adhoc_filters: [],
// extra_filters: [],
// extra_form_data: {},
metrics: ["count"],
row_limit: 1000,
showSearch: true,
// url_params: {
// native_filters_key: "0ktJOz1FTTo"
// },
// inView: true,
viz_type: "filter_select",
// type: "NATIVE_FILTER",
dashboardId: dashboardId,
// native_filter_id: "NATIVE_FILTER-8jS1fx4hl"
},
force: false,
ownState: {}
}
setSelectedNativeFilter({...nativeFilter, columnName, nativeFilterId})
getChartDataRequest(filterValues).then(response => {
setNativeFilterValues(response.json.result[0].data.map((item: any) => ({value: item[columnName], label: item[columnName]})))
});
}

const onChangeDashboardFilterValue = (filterValue: any) => {
console.log('dashboardValue', filterValue);

// todo(hughhh): refactor to handle multiple native filters
// once you have multiselect
setSelectedNativeFilter({...nativeFilter, filterValues: [filterValue]});
}

// Make sure notification settings has the required info
const checkNotificationSettings = () => {
if (!notificationSettings.length) {
Expand Down Expand Up @@ -1756,6 +1836,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</>
)}
</StyledInputContainer>

{tabsEnabled && contentType === ContentType.Dashboard && (
<StyledInputContainer>
<>
Expand All @@ -1768,6 +1849,23 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
placeholder={t('Select a tab')}
/>
</>
<div style={{marginTop: '10px', display: 'flex', flexDirection: 'column'}}>
<div className="control-label">{t('Select Dashboard Filter')}</div>
<Select
disabled={nativeFilterOptions?.length < 1}
ariaLabel={t('Select Filter')}
value={nativeFilter.nativeFilter}
options={nativeFilterOptions}
onChange={onChangeDashboardFilter}
/>
<div className="control-label">{t('Select Dashboard Value')}</div>
<Select
ariaLabel={t('Value')}
value={nativeFilter.nativeFilterValue}
options={nativeFilterValues}
onChange={onChangeDashboardFilterValue}
/>
</div>
</StyledInputContainer>
)}
{isScreenshot && (
Expand All @@ -1790,6 +1888,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
</StyledInputContainer>
)}

{(isReport || contentType === ContentType.Dashboard) && (
<div className="inline-container">
<StyledCheckbox
Expand Down
7 changes: 7 additions & 0 deletions superset-frontend/src/features/alerts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,15 @@ export type DashboardState = {
anchor?: string;
};

export type ExtraNativeFilter = {
columnName?: string;
filterValues?: Array<any>;
// filterOp?: string; // assuming all operators are 'IN' for now
};

export type Extra = {
dashboard?: DashboardState;
nativeFilters?: Array<ExtraNativeFilter>;
};

export type Operator = '<' | '>' | '<=' | '>=' | '==' | '!=' | 'not null';
Expand Down
19 changes: 17 additions & 2 deletions superset/commands/dashboard/permalink/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,21 @@ def __init__(
def run(self) -> str:
self.validate()
dashboard = DashboardDAO.get_by_id_or_slug(self.dashboard_id)
print("creating permalink...")

value = {
"dashboardId": str(dashboard.uuid),
"state": self.state,
"state": {**self.state, "urlParams": [['native_filter', '(NATIVE_FILTER-8jS1fx4hl:(extraFormData:(filters:!((col:country_name,op:IN,val:!(Brazil)))),filterState:(label:country_name,validateStatus:!f,value:!(Brazil)),id:NATIVE_FILTER-8jS1fx4hl,ownState:()))']]},
}
user_id = get_user_id()

print('.' * 10)
print(self.resource)
print(user_id)
print(value)
print(self.codec)
print('.' * 10)

entry = KeyValueDAO.upsert_entry(
resource=self.resource,
key=get_deterministic_uuid(self.salt, (user_id, value)),
Expand All @@ -79,7 +89,12 @@ def run(self) -> str:
)
db.session.flush()
assert entry.id # for type checks
return encode_permalink_key(key=entry.id, salt=self.salt)

hash = encode_permalink_key(key=entry.id, salt=self.salt)
print("permalink created")
print(hash)

return hash

def validate(self) -> None:
pass
31 changes: 27 additions & 4 deletions superset/commands/report/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,16 +228,27 @@ def get_dashboard_urls(
Retrieve the URL for the dashboard tabs, or return the dashboard URL if no tabs are available.
""" # noqa: E501
force = "true" if self._report_schedule.force_screenshot else "false"


if (
dashboard_state := self._report_schedule.extra.get("dashboard")
) and feature_flag_manager.is_feature_enabled("ALERT_REPORT_TABS"):
if anchor := dashboard_state.get("anchor"):
try:
anchor_list: list[str] = json.loads(anchor)
return self._get_tabs_urls(anchor_list, user_friendly=user_friendly)
urls = self._get_tabs_urls(anchor_list, user_friendly=user_friendly)
return urls
except json.JSONDecodeError:
logger.debug("Anchor value is not a list, Fall back to single tab")
return [self._get_tab_url(dashboard_state)]

print('returning single tab url....')
native_filter_params = self._report_schedule.get_native_filters_params()
return [self._get_tab_url({
'anchor': anchor,
'urlParams': {'native_filters': native_filter_params},
'dataMask': None,
'activeTabs': None},
user_friendly=user_friendly)]

dashboard = self._report_schedule.dashboard
dashboard_id_or_slug = (
Expand All @@ -260,29 +271,35 @@ def _get_tab_url(
"""
Get one tab url
"""
print('in single tab....')
print(dashboard_state)
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,
user_friendly=user_friendly,
)

def _get_tabs_urls(
self, tab_anchors: list[str], user_friendly: bool = False
self,
tab_anchors: list[str],
user_friendly: bool = False
) -> list[str]:
"""
Get multple tabs urls
"""
print('hello5')
return [
self._get_tab_url(
{
"anchor": tab_anchor,
"dataMask": None,
"activeTabs": None,
"urlParams": None,
"urlParams": url_params,
},
user_friendly=user_friendly,
)
Expand Down Expand Up @@ -320,6 +337,7 @@ def _get_screenshots(self) -> list[bytes]:
]
else:
urls = self.get_dashboard_urls()
print('urls', urls)

window_width, window_height = app.config["WEBDRIVER_WINDOW"]["dashboard"]
width = min(max_width, self._report_schedule.custom_width or window_width)
Expand Down Expand Up @@ -481,6 +499,11 @@ def _get_notification_content(self) -> NotificationContent: # noqa: C901
error_text = None
header_data = self._get_log_data()
url = self._get_url(user_friendly=True)

print("*"*100)
print(url)
print("*"*100)

if (
feature_flag_manager.is_feature_enabled("ALERTS_ATTACH_REPORTS")
or self._report_schedule.type == ReportScheduleType.REPORT
Expand Down
2 changes: 1 addition & 1 deletion superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ class D3TimeFormat(TypedDict, total=False):
"EMBEDDED_SUPERSET": False,
# Enables Alerts and reports new implementation
"ALERT_REPORTS": False,
"ALERT_REPORT_TABS": False,
"ALERT_REPORT_TABS": True,

This comment was marked as resolved.

"ALERT_REPORT_SLACK_V2": False,
"DASHBOARD_RBAC": False,
"ENABLE_ADVANCED_DATA_TYPES": False,
Expand Down
16 changes: 16 additions & 0 deletions superset/daos/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging
from datetime import datetime
from typing import Any
from collections import defaultdict

from flask import g
from flask_appbuilder.models.sqla.interface import SQLAInterface
Expand Down Expand Up @@ -319,6 +320,21 @@ def copy_dashboard(
cls.set_dash_metadata(dash, metadata, old_to_new_slice_ids)
db.session.add(dash)
return dash

@classmethod
def get_native_filter_configuration(cls, id: int):
dashboard = cls.get_by_id_or_slug(id)
metadata = json.loads(dashboard.json_metadata or "{}")
native_filter_configuration = metadata.get(
"native_filter_configuration", []
)

tab_filters = defaultdict(list)
for filter in native_filter_configuration:
for tab_key in filter.get("tabsInScope", []):
tab_filters[tab_key].append(filter)

return tab_filters

@classmethod
def update_native_filters_config(
Expand Down
4 changes: 4 additions & 0 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,11 @@ def get_tabs(self, id_or_slug: str) -> Response:
""" # noqa: E501
try:
tabs = DashboardDAO.get_tabs_for_dashboard(id_or_slug)
native_filters = DashboardDAO.get_native_filter_configuration(id_or_slug)
Comment on lines 474 to +475
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient Sequential Database Queries category Performance

Tell me more
What is the issue?

Sequential database queries are being made to fetch tabs and native filters separately, which could be combined into a single database operation.

Why this matters

Multiple sequential database round-trips increase latency and database load. This is especially important in API endpoints where response time is critical.

Suggested change ∙ Feature Preview

Modify DashboardDAO to provide a single method that fetches both tabs and native filters in one query:

@classmethod
def get_tabs_and_filters(cls, id_or_slug: str) -> Tuple[Dict, Dict]:
    # Combine the queries using appropriate joins
    return tabs, native_filters
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.


result = self.tab_schema.dump(tabs)
result['native_filters'] = native_filters

return self.response(200, result=result)

except (TypeError, ValueError) as err:
Expand Down
Loading
Loading