-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
base: master
Are you sure you want to change the base?
Changes from 43 commits
38bb661
61d7c4f
86ae7cc
ecee690
25d2850
ef0a7ee
16751c0
47fc7de
daf48cc
9b59d3a
5fd0517
689605d
bf31b9d
2c06217
c5c6d53
5202544
70ac1e7
24db175
3da7656
81ace0f
1a41780
45e1407
10f2e0e
082461b
6ab40e3
276d0f6
a115b17
dae9428
263b290
def42d0
d20b764
f29c7e1
a09b2a5
5d228f0
64bc357
3d40ece
3efac9e
945c467
ebbe945
f03cf9f
b61e361
e07fdb3
951205f
ee8862f
ae03821
4ce0b97
243ce8d
32afe30
8afdf82
b645634
fde45f0
bd25d3c
424d59f
0d1422b
f2af658
cfce539
3947ed3
bfd8694
14d341a
62b6b88
0118aa2
727d361
82c4d3e
3ce834e
7a043a8
83a1b99
1b4e734
b0bd21d
4763cd6
029b682
2e6b69a
519185c
0b644bd
3b14e11
be87858
667be47
d1e0e04
e5c463a
b47fac6
f1280c2
e6caa17
178b08c
7dd4b5d
107e269
3c86c5f
22df03f
90a29fe
4f5b21f
c1a215f
3026fcc
7e0faad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ import { | |
} from '@superset-ui/core'; | ||
import rison from 'rison'; | ||
import { useSingleViewResource } from 'src/views/CRUD/hooks'; | ||
import Button from 'src/components/Button'; | ||
|
||
import { InputNumber } from 'src/components/Input'; | ||
import { Switch } from 'src/components/Switch'; | ||
|
@@ -47,7 +48,14 @@ 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, TreeSelect } from 'src/components'; | ||
// todo(hughhh): migrate to src/components/Form | ||
import { | ||
AntdCheckbox, | ||
AsyncSelect, | ||
Select, | ||
TreeSelect, | ||
AntdForm, | ||
} 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'; | ||
|
@@ -68,9 +76,11 @@ import { | |
TabNode, | ||
SelectValue, | ||
ContentType, | ||
ExtraNativeFilter, | ||
} from 'src/features/alerts/types'; | ||
import { useSelector } from 'react-redux'; | ||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; | ||
import { getChartDataRequest } from 'src/components/Chart/chartAction'; | ||
import Icons from 'src/components/Icons'; | ||
import NumberInput from './components/NumberInput'; | ||
import { AlertReportCronScheduler } from './components/AlertReportCronScheduler'; | ||
|
@@ -322,6 +332,55 @@ export const StyledInputContainer = styled.div` | |
.input-label { | ||
margin-left: 10px; | ||
} | ||
|
||
.filters { | ||
margin: 5px 0; | ||
hughhhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.filters-container { | ||
display: flex; | ||
} | ||
|
||
.filters-dash-container { | ||
display: flex; | ||
flex-direction: column; | ||
max-width: 174px; | ||
flex: 1; | ||
margin-right: 16px; | ||
|
||
.control-label { | ||
flex: 1; | ||
} | ||
} | ||
|
||
.filters-dash-select { | ||
flex: 1; | ||
} | ||
|
||
.filters-dashvalue-container { | ||
display: flex; | ||
flex-direction: column; | ||
flex: 1; | ||
} | ||
|
||
.filters-delete { | ||
display: flex; | ||
margin-top: 23px; | ||
} | ||
|
||
.filters-trashcan { | ||
display: 'flex'; | ||
color: ${theme.colors.grayscale.light1}; | ||
} | ||
.filters-add-container { | ||
flex: '.25'; | ||
padding: '7px 0'; | ||
|
||
.filters-add-btn { | ||
padding: 0; | ||
color: ${theme.colors.primary.base}; | ||
} | ||
} | ||
} | ||
`} | ||
`; | ||
|
||
|
@@ -461,6 +520,13 @@ 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 [tabNativeFilters, setTabNativeFilters] = useState<object>({}); | ||
hughhhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const [nativeFilterData, setNativeFilterData] = useState<ExtraNativeFilter[]>( | ||
[{}], | ||
); | ||
|
||
console.log(nativeFilterData); | ||
|
||
// Validation | ||
const [validationStatus, setValidationStatus] = useState<ValidationObject>({ | ||
|
@@ -672,6 +738,18 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
|
||
const shouldEnableForceScreenshot = | ||
contentType === ContentType.Chart && !isReport; | ||
|
||
if (currentAlert?.extra?.dashboard) { | ||
currentAlert.extra.dashboard.nativeFilters = nativeFilterData.map( | ||
({ columnName, columnLabel, nativeFilterId, filterValues }) => ({ | ||
columnName, | ||
columnLabel, | ||
nativeFilterId, | ||
filterValues, | ||
}), | ||
); | ||
} | ||
|
||
const data: any = { | ||
...currentAlert, | ||
type: isReport ? 'Report' : 'Alert', | ||
|
@@ -832,7 +910,11 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
endpoint: `/api/v1/dashboard/${dashboard.value}/tabs`, | ||
}) | ||
.then(response => { | ||
const { tab_tree: tabTree, all_tabs: allTabs } = response.json.result; | ||
const { | ||
tab_tree: tabTree, | ||
all_tabs: allTabs, | ||
native_filters: nativeFilters, | ||
} = response.json.result; | ||
const allTabsWithOrder = tabTree.map( | ||
(tab: { value: string }) => tab.value, | ||
); | ||
|
@@ -847,9 +929,16 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
} | ||
|
||
setTabOptions(tabTree); | ||
setTabNativeFilters(nativeFilters); | ||
|
||
const anchor = currentAlert?.extra?.dashboard?.anchor; | ||
if (anchor) { | ||
setNativeFilterOptions( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to handle both |
||
nativeFilters[anchor].map((filter: any) => ({ | ||
value: filter.id, | ||
label: filter.name, | ||
})), | ||
); | ||
try { | ||
const parsedAnchor = JSON.parse(anchor); | ||
if (Array.isArray(parsedAnchor)) { | ||
|
@@ -1022,6 +1111,24 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
} | ||
}; | ||
|
||
const handleAddFilterField = () => { | ||
setNativeFilterData([ | ||
...nativeFilterData, | ||
{ | ||
nativeFilterId: null, | ||
columnLabel: '', | ||
columnName: '', | ||
filterValues: [], | ||
}, | ||
]); | ||
}; | ||
|
||
const handleRemoveFilterField = (filterIdx: number) => { | ||
const filters = nativeFilterData || []; | ||
filters.splice(filterIdx, 1); | ||
setNativeFilterData(filters); | ||
}; | ||
|
||
const onCustomWidthChange = (value: number | string | null | undefined) => { | ||
const numValue = | ||
value === null || | ||
|
@@ -1066,6 +1173,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
updateAlertState('chart', null); | ||
if (tabsEnabled) { | ||
setTabOptions([]); | ||
setNativeFilterOptions([]); | ||
updateAnchorState(''); | ||
} | ||
}; | ||
|
@@ -1126,6 +1234,79 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
setForceScreenshot(event.target.checked); | ||
}; | ||
|
||
const onChangeDashboardFilter = (idx: number, nativeFilterId: string) => { | ||
// set dashboardFilter | ||
const anchor = currentAlert?.extra?.dashboard?.anchor; | ||
|
||
// @ts-ignore | ||
const inScopeFilters = tabNativeFilters[anchor]; | ||
const filter = inScopeFilters.filter( | ||
(f: any) => f.id === nativeFilterId, | ||
)[0]; | ||
|
||
const { datasetId } = filter.targets[0]; | ||
const columnName = filter.targets[0].column.name; | ||
|
||
const columnLabel = nativeFilterOptions.filter( | ||
// @ts-ignore | ||
hughhhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
filter => filter.value === nativeFilterId, | ||
// @ts-ignore | ||
)[0].label; | ||
const dashboardId = currentAlert?.dashboard?.value; | ||
|
||
// Get values tied to the selected filter | ||
const filterValues = { | ||
formData: { | ||
datasource: `${datasetId}__table`, | ||
groupby: [columnName], | ||
metrics: ['count'], | ||
row_limit: 1000, | ||
showSearch: true, | ||
viz_type: 'filter_select', | ||
type: 'NATIVE_FILTER', | ||
dashboardId, | ||
}, | ||
force: false, | ||
ownState: {}, | ||
}; | ||
|
||
getChartDataRequest(filterValues).then(response => { | ||
const newFilterValues = response.json.result[0].data.map((item: any) => ({ | ||
value: item[columnName], | ||
label: item[columnName], | ||
})); | ||
|
||
console.log('setting filter values', nativeFilterData); | ||
hughhhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
setNativeFilterData( | ||
nativeFilterData.map((filter, index) => | ||
index === idx | ||
? { | ||
...filter, | ||
nativeFilterId, | ||
columnLabel, | ||
columnName, | ||
optionFilterValues: newFilterValues, | ||
filterValues: [], // reset filter values on filter change | ||
} | ||
: filter, | ||
), | ||
); | ||
}); | ||
}; | ||
|
||
const onChangeDashboardFilterValue = ( | ||
idx: number, | ||
filterValues: string[], | ||
) => { | ||
// todo(hughhh): refactor to handle multiple native filters | ||
setNativeFilterData( | ||
nativeFilterData.map((filter, index) => | ||
index === idx ? { ...filter, filterValues } : filter, | ||
), | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inefficient Array State Updates
Tell me moreWhat is the issue?State updates using array mapping operations inside setNativeFilterData are potentially inefficient for large datasets, as they create new array copies on each update. Why this mattersFor large arrays, this pattern causes unnecessary memory allocation and GC pressure due to creating intermediate arrays on each state update. Suggested change ∙ Feature PreviewUse a more efficient update pattern that directly modifies only the changed element: setNativeFilterData(prevData => {
const newData = [...prevData];
newData[idx] = { ...newData[idx], filterValues };
return newData;
}); 💬 Chat with Korbit by mentioning @korbit-ai. |
||
// setSelectedNativeFilter({ ...nativeFilter, filterValues }); | ||
}; | ||
|
||
// Make sure notification settings has the required info | ||
const checkNotificationSettings = () => { | ||
if (!notificationSettings.length) { | ||
|
@@ -1309,6 +1490,10 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
|
||
useEffect(() => { | ||
if (resource) { | ||
// Add native filter settings | ||
// @ts-ignore | ||
setNativeFilterData(resource.extra?.dashboard.nativeFilters); | ||
|
||
// Add notification settings | ||
const settings = (resource.recipients || []).map(setting => { | ||
const config = | ||
|
@@ -1771,9 +1956,10 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
</> | ||
)} | ||
</StyledInputContainer> | ||
|
||
{tabsEnabled && contentType === ContentType.Dashboard && ( | ||
<StyledInputContainer> | ||
<> | ||
<div> | ||
<div className="control-label">{t('Select tab')}</div> | ||
<StyledTreeSelect | ||
disabled={tabOptions?.length === 0} | ||
|
@@ -1782,7 +1968,88 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
onSelect={updateAnchorState} | ||
placeholder={t('Select a tab')} | ||
/> | ||
</> | ||
</div> | ||
<AntdForm className="filters" name="form" autoComplete="off"> | ||
<AntdForm.List | ||
name="filters" | ||
initialValue={nativeFilterData} // only show one filter field on create | ||
> | ||
{(fields, { add, remove }) => ( | ||
<div> | ||
{fields.map(({ key, name }) => ( | ||
<div className="filters-container" key={key}> | ||
{console.log(name)} | ||
<div className="filters-dash-container"> | ||
<div className="control-label"> | ||
<span>{t('Select Dashboard Filter')}</span> | ||
<StyledTooltip | ||
tooltip={t( | ||
'Choose from existing dashboard filters and select a value to refine your report results.', | ||
)} | ||
/> | ||
</div> | ||
<Select | ||
className="filters-dash-select" | ||
disabled={nativeFilterOptions?.length < 1} | ||
ariaLabel={t('Select Filter')} | ||
placeholder={t('Select Filter')} | ||
// @ts-ignore | ||
value={nativeFilterData[key]?.nativeFilterId} | ||
// @ts-ignore | ||
options={nativeFilterOptions} | ||
onChange={value => | ||
// @ts-ignore | ||
onChangeDashboardFilter(key, value) | ||
} | ||
oneLine | ||
/> | ||
</div> | ||
<div className="filters-dashvalue-container"> | ||
<div className="control-label">{t('Value')}</div> | ||
<Select | ||
ariaLabel={t('Value')} | ||
placeholder={t('Select Value')} | ||
value={nativeFilterData[key]?.filterValues} | ||
// @ts-ignore | ||
options={ | ||
nativeFilterData[key]?.optionFilterValues | ||
} | ||
onChange={value => | ||
// @ts-ignore | ||
onChangeDashboardFilterValue(key, value) | ||
} | ||
mode="multiple" | ||
/> | ||
</div> | ||
{name !== 0 && ( | ||
<div className="filters-delete"> | ||
<Icons.Trash | ||
className="filters-trashcan" | ||
onClick={() => { | ||
handleRemoveFilterField(name); | ||
remove(name); | ||
}} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
))} | ||
<div className="filters-add-container"> | ||
<Button | ||
className="filters-add-btn" | ||
type="link" | ||
onClick={() => { | ||
handleAddFilterField(); | ||
add(); | ||
}} | ||
> | ||
+ {t('Apply another dashboard filter')} | ||
</Button> | ||
</div> | ||
</div> | ||
)} | ||
</AntdForm.List> | ||
</AntdForm> | ||
</StyledInputContainer> | ||
)} | ||
{isScreenshot && ( | ||
|
@@ -1805,6 +2072,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ | |
</div> | ||
</StyledInputContainer> | ||
)} | ||
|
||
{(isReport || contentType === ContentType.Dashboard) && ( | ||
<div className="inline-container"> | ||
<StyledCheckbox | ||
|
Uh oh!
There was an error while loading. Please reload this page.