mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat: Add Dashboard Filter Support for Alert Reports (#32196)
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com> Co-authored-by: Hugh A Miles II <hugh@Mac.home>
This commit is contained in:
@@ -452,7 +452,7 @@ test('renders tab selection when Dashboard is selected', async () => {
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: /dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/select tab/i)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/select tab/i)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('changes to content options when chart is selected', async () => {
|
||||
@@ -666,3 +666,15 @@ test('removes notification method on clicking trash can', async () => {
|
||||
screen.getAllByRole('combobox', { name: /delivery method/i }).length,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('renders dashboard filter dropdowns', async () => {
|
||||
render(<AlertReportModal {...generateMockedProps(true, true)} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByTestId('contents-panel'));
|
||||
const filterOptionDropdown = screen.getByRole('combobox', {
|
||||
name: /select filter/i,
|
||||
});
|
||||
expect(filterOptionDropdown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -39,11 +39,16 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import Owner from 'src/types/Owner';
|
||||
// import { Form as AntdForm } from 'src/components/Form';
|
||||
import { propertyComparator } from '@superset-ui/core/components/Select/utils';
|
||||
import {
|
||||
AsyncSelect,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
CollapseLabelInModal,
|
||||
Form as AntdForm,
|
||||
InfoTooltip,
|
||||
Input,
|
||||
InputNumber,
|
||||
@@ -52,10 +57,8 @@ import {
|
||||
TreeSelect,
|
||||
type CheckboxChangeEvent,
|
||||
} from '@superset-ui/core/components';
|
||||
|
||||
import TimezoneSelector from '@superset-ui/core/components/TimezoneSelector';
|
||||
import { propertyComparator } from '@superset-ui/core/components/Select/utils';
|
||||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import Owner from 'src/types/Owner';
|
||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||
import { useCommonConf } from 'src/features/databases/state';
|
||||
import {
|
||||
@@ -75,9 +78,14 @@ import {
|
||||
TabNode,
|
||||
SelectValue,
|
||||
ContentType,
|
||||
ExtraNativeFilter,
|
||||
NativeFilterObject,
|
||||
} from 'src/features/alerts/types';
|
||||
import { StatusMessage } from 'src/filters/components/common';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { StandardModal, ModalFormField } from 'src/components/Modal';
|
||||
import NumberInput from './components/NumberInput';
|
||||
@@ -92,6 +100,14 @@ const TEXT_BASED_VISUALIZATION_TYPES = [
|
||||
VizType.PairedTTest,
|
||||
];
|
||||
|
||||
const StyledDivider = styled.span`
|
||||
margin: 0 ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
color: ${({ theme }) => theme.colorSplit};
|
||||
font-weight: ${({ theme }) => theme.fontWeightStrong};
|
||||
font-size: ${({ theme }) => theme.fontSize}px;
|
||||
align-content: center;
|
||||
`;
|
||||
|
||||
export interface AlertReportModalProps {
|
||||
addSuccessToast: (msg: string) => void;
|
||||
addDangerToast: (msg: string) => void;
|
||||
@@ -274,6 +290,94 @@ export const StyledInputContainer = styled.div`
|
||||
textarea {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: ${theme.colorTextDisabled};
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 300px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: ${theme.colorTextPlaceholder};
|
||||
}
|
||||
|
||||
textarea,
|
||||
input[type='text'],
|
||||
input[type='number'] {
|
||||
padding: ${theme.sizeUnit}px ${theme.sizeUnit * 2}px;
|
||||
border-style: none;
|
||||
border: 1px solid ${theme.colorBorder};
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
|
||||
&[name='description'] {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.input-label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: ${theme.sizeUnit * 3}px 0;
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
margin: ${theme.sizeUnit * 2}px 0;
|
||||
}
|
||||
|
||||
.filters-dash-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 174px;
|
||||
flex: 1;
|
||||
margin-right: ${theme.sizeUnit * 4}px;
|
||||
|
||||
.control-label {
|
||||
flex: 1;
|
||||
margin-bottom: ${theme.sizeUnit * 2}px;
|
||||
|
||||
.label-with-tooltip {
|
||||
margin-right: ${theme.sizeUnit * 2}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters-dash-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filters-dashvalue-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filters-delete {
|
||||
display: flex;
|
||||
margin-top: ${theme.sizeUnit * 8}px;
|
||||
margin-left: ${theme.sizeUnit * 4}px;
|
||||
}
|
||||
|
||||
.filters-trashcan {
|
||||
width: ${theme.sizeUnit * 10}px;
|
||||
display: 'flex';
|
||||
color: ${theme.colorIcon};
|
||||
}
|
||||
.filters-add-container {
|
||||
flex: '.25';
|
||||
padding: '${theme.sizeUnit * 3} 0';
|
||||
|
||||
.filters-add-btn {
|
||||
padding: ${theme.sizeUnit * 2}px;
|
||||
color: ${theme.colorWhite};
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -334,6 +438,8 @@ export const TRANSLATIONS = {
|
||||
ERROR_TOOLTIP_MESSAGE: t(
|
||||
'Not all required fields are complete. Please provide the following:',
|
||||
),
|
||||
NATIVE_FILTER_COLUMN_ERROR_TEXT: t('Native filter column is required'),
|
||||
NATIVE_FILTER_NO_VALUES_ERROR_TEXT: t('Native filter values has no values'),
|
||||
};
|
||||
|
||||
const NotificationMethodAdd: FunctionComponent<NotificationMethodAddProps> = ({
|
||||
@@ -400,6 +506,25 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
const [dashboardOptions, setDashboardOptions] = useState<MetaObject[]>([]);
|
||||
const [chartOptions, setChartOptions] = useState<MetaObject[]>([]);
|
||||
const [tabOptions, setTabOptions] = useState<TabNode[]>([]);
|
||||
const [nativeFilterOptions, setNativeFilterOptions] = useState<
|
||||
{
|
||||
value: string;
|
||||
label: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [tabNativeFilters, setTabNativeFilters] = useState<object>({});
|
||||
const [nativeFilterData, setNativeFilterData] = useState<ExtraNativeFilter[]>(
|
||||
[
|
||||
{
|
||||
nativeFilterId: null,
|
||||
filterName: '',
|
||||
filterType: '',
|
||||
columnLabel: '',
|
||||
columnName: '',
|
||||
filterValues: [],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Validation
|
||||
const [validationStatus, setValidationStatus] = useState<ValidationObject>({
|
||||
@@ -452,6 +577,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
const formatOptionEnabled =
|
||||
isFeatureEnabled(FeatureFlag.AlertsAttachReports) || isReport;
|
||||
const tabsEnabled = isFeatureEnabled(FeatureFlag.AlertReportTabs);
|
||||
const filtersEnabled = isFeatureEnabled(FeatureFlag.AlertReportsFilter);
|
||||
|
||||
const [notificationAddState, setNotificationAddState] =
|
||||
useState<NotificationAddStatus>('active');
|
||||
@@ -522,6 +648,116 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
grace_period: undefined,
|
||||
};
|
||||
|
||||
const fetchDashboardFilterValues = async (
|
||||
dashboardId: number | string | undefined,
|
||||
columnName: string,
|
||||
datasetId: number | string,
|
||||
vizType = 'filter_select',
|
||||
adhocFilters = [],
|
||||
) => {
|
||||
if (vizType === 'filter_time') {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterValues = {
|
||||
formData: {
|
||||
datasource: `${datasetId}__table`,
|
||||
groupby: [columnName],
|
||||
metrics: ['count'],
|
||||
row_limit: 1000,
|
||||
showSearch: true,
|
||||
viz_type: vizType,
|
||||
type: 'NATIVE_FILTER',
|
||||
dashboardId,
|
||||
adhoc_filters: adhocFilters,
|
||||
},
|
||||
force: false,
|
||||
ownState: {},
|
||||
};
|
||||
|
||||
const data = await getChartDataRequest(filterValues).then(response => {
|
||||
const rawData = response.json.result[0].data;
|
||||
let filteredData = rawData;
|
||||
|
||||
if (vizType === 'filter_timecolumn') {
|
||||
// filter for time columns types
|
||||
filteredData = rawData.filter((item: any) => item.dtype === 2);
|
||||
}
|
||||
|
||||
return filteredData.map((item: any) => {
|
||||
if (vizType === 'filter_timegrain') {
|
||||
return {
|
||||
value: item.duration,
|
||||
label: item.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (vizType === 'filter_timecolumn') {
|
||||
return {
|
||||
value: item.column_name,
|
||||
label: item.verbose_name || item.column_name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: item[columnName],
|
||||
label: item[columnName],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return data;
|
||||
};
|
||||
|
||||
const addNativeFilterOptions = (nativeFilters: NativeFilterObject[]) => {
|
||||
nativeFilterData.map(nativeFilter => {
|
||||
if (!nativeFilter.nativeFilterId) return;
|
||||
const filter = nativeFilters.filter(
|
||||
(f: any) => f.id === nativeFilter.nativeFilterId,
|
||||
)[0];
|
||||
|
||||
const { datasetId } = filter.targets[0];
|
||||
const filterName = filter.name;
|
||||
const columnName = filter.targets[0].column?.name || filterName;
|
||||
const dashboardId = currentAlert?.dashboard?.value;
|
||||
const { filterType } = filter;
|
||||
|
||||
if (filterType === 'filter_time') {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return fetchDashboardFilterValues(
|
||||
dashboardId,
|
||||
columnName,
|
||||
datasetId,
|
||||
filterType,
|
||||
).then(optionFilterValues => {
|
||||
setNativeFilterData(prev =>
|
||||
prev.map(filter =>
|
||||
filter.nativeFilterId === nativeFilter.nativeFilterId
|
||||
? {
|
||||
...filter,
|
||||
filterType,
|
||||
filterName,
|
||||
optionFilterValues,
|
||||
}
|
||||
: filter,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const filterNativeFilterOptions = () =>
|
||||
nativeFilterOptions.filter(
|
||||
option =>
|
||||
!nativeFilterData.some(
|
||||
filter => filter.nativeFilterId === option.value,
|
||||
),
|
||||
);
|
||||
|
||||
const updateNotificationSetting = (
|
||||
index: number,
|
||||
setting: NotificationSetting,
|
||||
@@ -548,7 +784,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
setNotificationSettings(settings);
|
||||
}
|
||||
};
|
||||
|
||||
const removeNotificationSetting = (index: number) => {
|
||||
const settings = notificationSettings.slice();
|
||||
|
||||
@@ -611,6 +846,37 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
|
||||
const shouldEnableForceScreenshot =
|
||||
contentType === ContentType.Chart && !isReport;
|
||||
|
||||
if (currentAlert?.extra?.dashboard) {
|
||||
// Filter out empty native filters (where both filter name and values are empty/null)
|
||||
const validNativeFilters = nativeFilterData.filter(filter => {
|
||||
const hasFilterName =
|
||||
filter.filterName && filter.filterName.trim() !== '';
|
||||
const hasFilterValues =
|
||||
filter.filterValues && filter.filterValues.length > 0;
|
||||
// Keep filter if it has either a name or values (or both)
|
||||
return hasFilterName || hasFilterValues;
|
||||
});
|
||||
|
||||
currentAlert.extra.dashboard.nativeFilters = validNativeFilters.map(
|
||||
({
|
||||
columnName,
|
||||
columnLabel,
|
||||
nativeFilterId,
|
||||
filterValues,
|
||||
filterType,
|
||||
filterName,
|
||||
}) => ({
|
||||
filterName,
|
||||
filterType,
|
||||
columnName,
|
||||
columnLabel,
|
||||
nativeFilterId,
|
||||
filterValues,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const data: any = {
|
||||
...currentAlert,
|
||||
type: isReport ? 'Report' : 'Alert',
|
||||
@@ -771,7 +1037,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,
|
||||
);
|
||||
@@ -786,11 +1056,32 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
}
|
||||
|
||||
setTabOptions(tabTree);
|
||||
setTabNativeFilters(nativeFilters);
|
||||
|
||||
if (isEditMode && nativeFilters.all) {
|
||||
// update options for all filters
|
||||
addNativeFilterOptions(nativeFilters.all);
|
||||
// Also set the available filter options for the add button
|
||||
setNativeFilterOptions(
|
||||
nativeFilters.all.map((filter: any) => ({
|
||||
value: filter.id,
|
||||
label: filter.name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
const anchor = currentAlert?.extra?.dashboard?.anchor;
|
||||
if (anchor) {
|
||||
try {
|
||||
const parsedAnchor = JSON.parse(anchor);
|
||||
if (!Array.isArray(parsedAnchor)) {
|
||||
// only show filters scoped to anchor
|
||||
setNativeFilterOptions(
|
||||
nativeFilters[anchor].map((filter: any) => ({
|
||||
value: filter.id,
|
||||
label: filter.name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (Array.isArray(parsedAnchor)) {
|
||||
// Check if all elements in parsedAnchor list are in allTabs
|
||||
const isValidSubset = parsedAnchor.every(tab => tab in allTabs);
|
||||
@@ -805,9 +1096,16 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
updateAnchorState(undefined);
|
||||
}
|
||||
}
|
||||
} else if (nativeFilters.all) {
|
||||
setNativeFilterOptions(
|
||||
nativeFilters.all.map((filter: any) => ({
|
||||
value: filter.id,
|
||||
label: filter.name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(e => {
|
||||
addDangerToast(t('There was an error retrieving dashboard tabs.'));
|
||||
});
|
||||
}
|
||||
@@ -961,6 +1259,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 ||
|
||||
@@ -1005,8 +1321,21 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
updateAlertState('chart', null);
|
||||
if (tabsEnabled) {
|
||||
setTabOptions([]);
|
||||
setNativeFilterOptions([]);
|
||||
updateAnchorState('');
|
||||
}
|
||||
if (filtersEnabled) {
|
||||
setNativeFilterData([
|
||||
{
|
||||
filterName: '',
|
||||
filterType: '',
|
||||
nativeFilterId: null,
|
||||
columnLabel: '',
|
||||
columnName: '',
|
||||
filterValues: [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const onChartChange = (chart: SelectValue) => {
|
||||
@@ -1062,6 +1391,164 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
setForceScreenshot(e.target.checked);
|
||||
};
|
||||
|
||||
const onChangeDashboardFilter = (idx: number, nativeFilterId: string) => {
|
||||
if (
|
||||
!nativeFilterId ||
|
||||
nativeFilterId === 'undefined' ||
|
||||
nativeFilterId === 'null'
|
||||
)
|
||||
return;
|
||||
|
||||
// find specific filter tied to the selected filter
|
||||
const filters = Object.values(tabNativeFilters).flat();
|
||||
const filter = filters.filter((f: any) => f.id === nativeFilterId)[0];
|
||||
|
||||
const { filterType, adhoc_filters: adhocFilters } = filter;
|
||||
const filterAlreadyExist = nativeFilterData.some(
|
||||
filter => filter.nativeFilterId === nativeFilterId,
|
||||
);
|
||||
|
||||
if (filterAlreadyExist) {
|
||||
addDangerToast(t('This filter already exist on the report'));
|
||||
return;
|
||||
}
|
||||
|
||||
const filterName = filter.name;
|
||||
|
||||
let columnName: string;
|
||||
if (
|
||||
filterType === 'filter_time' ||
|
||||
filterType === 'filter_timecolumn' ||
|
||||
filterType === 'filter_timegrain'
|
||||
) {
|
||||
columnName = filter.name;
|
||||
} else {
|
||||
columnName = filter.targets[0].column.name;
|
||||
}
|
||||
|
||||
const datasetId = filter.targets[0].datasetId || null;
|
||||
|
||||
const columnLabel = nativeFilterOptions.filter(
|
||||
filter => filter.value === nativeFilterId,
|
||||
)[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,
|
||||
adhoc_filters: adhocFilters,
|
||||
},
|
||||
force: false,
|
||||
ownState: {},
|
||||
};
|
||||
|
||||
// todo(hugh): put this into another function
|
||||
if (
|
||||
filterType === 'filter_time' ||
|
||||
filterType === 'filter_timecolumn' ||
|
||||
filterType === 'filter_timegrain'
|
||||
) {
|
||||
fetchDashboardFilterValues(
|
||||
dashboardId,
|
||||
columnName,
|
||||
datasetId,
|
||||
filterType,
|
||||
adhocFilters,
|
||||
).then(optionFilterValues => {
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((filter, index) =>
|
||||
index === idx
|
||||
? {
|
||||
...filter,
|
||||
filterName,
|
||||
filterType,
|
||||
nativeFilterId,
|
||||
columnLabel,
|
||||
columnName,
|
||||
optionFilterValues,
|
||||
filterValues: [], // reset filter values on filter change
|
||||
}
|
||||
: filter,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((filter, index) =>
|
||||
index === idx
|
||||
? {
|
||||
...filter,
|
||||
filterName,
|
||||
filterType,
|
||||
nativeFilterId,
|
||||
columnLabel,
|
||||
columnName,
|
||||
optionFilterValues: [],
|
||||
filterValues: [], // reset filter values on filter change
|
||||
}
|
||||
: filter,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
getChartDataRequest(filterValues).then(response => {
|
||||
const newFilterValues = response.json.result[0].data.map((item: any) => ({
|
||||
value: item[columnName],
|
||||
label: item[columnName],
|
||||
}));
|
||||
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((filter, index) =>
|
||||
index === idx
|
||||
? {
|
||||
...filter,
|
||||
filterName,
|
||||
filterType,
|
||||
nativeFilterId,
|
||||
columnLabel,
|
||||
columnName,
|
||||
optionFilterValues: newFilterValues,
|
||||
filterValues: [], // reset filter values on filter change
|
||||
}
|
||||
: filter,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeDashboardFilterValue = (
|
||||
idx: number,
|
||||
filterValues:
|
||||
| SelectValue
|
||||
| SelectValue[]
|
||||
| string
|
||||
| string[]
|
||||
| number
|
||||
| number[],
|
||||
) => {
|
||||
let values: any;
|
||||
if (typeof filterValues === 'string') {
|
||||
values = [filterValues];
|
||||
} else {
|
||||
values = filterValues;
|
||||
}
|
||||
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((filter, index) =>
|
||||
index === idx ? { ...filter, filterValues: values } : filter,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// Make sure notification settings has the required info
|
||||
const checkNotificationSettings = () => {
|
||||
if (!notificationSettings.length) {
|
||||
@@ -1104,6 +1591,105 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const renderFilterValueSelect = (filter: ExtraNativeFilter, idx: number) => {
|
||||
if (!filter) return null;
|
||||
const { filterType, filterValues } = filter;
|
||||
let mode = 'multiple';
|
||||
if (filterType === 'filter_time') {
|
||||
return (
|
||||
<DateFilterControl
|
||||
name="time_range"
|
||||
onChange={timeRange => {
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((f: any) =>
|
||||
filter.nativeFilterId === f.nativeFilterId
|
||||
? {
|
||||
...f,
|
||||
filterValues: [timeRange],
|
||||
}
|
||||
: f,
|
||||
),
|
||||
);
|
||||
}}
|
||||
value={filterValues?.[0]} // only showing first value in the array for filter_time
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filterType === 'filter_range') {
|
||||
const min = filterValues?.[0];
|
||||
const max = filterValues?.[1];
|
||||
return (
|
||||
<div>
|
||||
<div className="inline-container">
|
||||
<InputNumber
|
||||
value={min}
|
||||
onChange={value => {
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((f: any) =>
|
||||
f.nativeFilterId === filter.nativeFilterId
|
||||
? { ...f, filterValues: [value, filterValues?.[1]] }
|
||||
: f,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<StyledDivider>-</StyledDivider>
|
||||
<InputNumber
|
||||
value={max}
|
||||
onChange={value => {
|
||||
setNativeFilterData(
|
||||
nativeFilterData.map((f: any) =>
|
||||
f.nativeFilterId === filter.nativeFilterId
|
||||
? { ...f, filterValues: [filterValues?.[0], value] }
|
||||
: f,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<StatusMessage status="help">
|
||||
{t('Enter minimum and maximum values for the range filter')}
|
||||
</StatusMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
filterType === 'filter_timegrain' ||
|
||||
filterType === 'filter_timecolumn'
|
||||
) {
|
||||
mode = 'single';
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
ariaLabel={t('Select Value')}
|
||||
placeholder={t('Select Value')}
|
||||
disabled={!filter?.optionFilterValues}
|
||||
value={filter?.filterValues}
|
||||
options={filter?.optionFilterValues || []}
|
||||
onChange={value =>
|
||||
onChangeDashboardFilterValue(
|
||||
idx,
|
||||
value as
|
||||
| string
|
||||
| string[]
|
||||
| number
|
||||
| number[]
|
||||
| SelectValue
|
||||
| SelectValue[],
|
||||
)
|
||||
}
|
||||
mode={mode as 'multiple' | 'single'}
|
||||
onClear={() => {
|
||||
// reset filter values on filter clear
|
||||
onChangeDashboardFilterValue(idx, []);
|
||||
}}
|
||||
allowClear
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const validateGeneralSection = () => {
|
||||
const errors = [];
|
||||
if (!currentAlert?.name?.length) {
|
||||
@@ -1124,6 +1710,28 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
) {
|
||||
errors.push(TRANSLATIONS.CONTENT_ERROR_TEXT);
|
||||
}
|
||||
|
||||
// validate native filter
|
||||
nativeFilterData.forEach(filter => {
|
||||
const columnNameCheck = !filter.columnName || filter.columnName === '';
|
||||
const filterValuesCheck =
|
||||
!filter.filterValues || filter.filterValues.length === 0;
|
||||
|
||||
if (columnNameCheck && filterValuesCheck) {
|
||||
// if both columnName and filterValues are null or empty, skip validation
|
||||
return;
|
||||
}
|
||||
|
||||
// check if native filter columnName is null or empty
|
||||
if (columnNameCheck) {
|
||||
errors.push(TRANSLATIONS.NATIVE_FILTER_COLUMN_ERROR_TEXT);
|
||||
}
|
||||
// check if native filter values is null or empty
|
||||
if (filterValuesCheck) {
|
||||
errors.push(TRANSLATIONS.NATIVE_FILTER_NO_VALUES_ERROR_TEXT);
|
||||
}
|
||||
});
|
||||
|
||||
updateValidationStatus(Sections.Content, errors);
|
||||
};
|
||||
const validateAlertSection = () => {
|
||||
@@ -1245,6 +1853,12 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (resource) {
|
||||
// Add native filter settings
|
||||
if (resource.extra?.dashboard?.nativeFilters) {
|
||||
const filters = resource.extra.dashboard.nativeFilters;
|
||||
setNativeFilterData(filters);
|
||||
}
|
||||
|
||||
// Add notification settings
|
||||
const settings = (resource.recipients || []).map(setting => {
|
||||
const config =
|
||||
@@ -1336,6 +1950,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
currentAlertSafe.dashboard,
|
||||
currentAlertSafe.chart,
|
||||
contentType,
|
||||
nativeFilterData,
|
||||
notificationSettings,
|
||||
conditionNotNull,
|
||||
emailError,
|
||||
@@ -1481,8 +2096,9 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...(!isReport
|
||||
? [
|
||||
...(isReport
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: 'condition',
|
||||
label: (
|
||||
@@ -1599,8 +2215,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
{
|
||||
key: 'contents',
|
||||
label: (
|
||||
@@ -1733,6 +2348,111 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
||||
</>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
{filtersEnabled && contentType === ContentType.Dashboard && (
|
||||
<StyledInputContainer>
|
||||
<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: idx }) => (
|
||||
<div className="filters-container" key={key}>
|
||||
<div className="filters-dash-container">
|
||||
<div className="control-label">
|
||||
<span className="label-with-tooltip">
|
||||
{t('Dashboard Filter')}
|
||||
</span>
|
||||
<InfoTooltip
|
||||
tooltip={t(
|
||||
'Choose from existing dashboard filters and select a value to refine your report results.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
disabled={
|
||||
nativeFilterOptions?.length < 1 &&
|
||||
!nativeFilterData[idx]?.filterName
|
||||
}
|
||||
ariaLabel={t('Select Filter')}
|
||||
placeholder={t('Select Filter')}
|
||||
value={nativeFilterData[idx]?.filterName}
|
||||
options={filterNativeFilterOptions()}
|
||||
onChange={value =>
|
||||
onChangeDashboardFilter(
|
||||
idx,
|
||||
String(value),
|
||||
)
|
||||
}
|
||||
onClear={() => {
|
||||
// reset filter values on filter clear
|
||||
nativeFilterData[idx].columnName = '';
|
||||
nativeFilterData[idx].filterName = '';
|
||||
nativeFilterData[idx].filterValues = [];
|
||||
}}
|
||||
css={css`
|
||||
flex: 1;
|
||||
`}
|
||||
oneLine
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<div className="filters-dashvalue-container">
|
||||
<div className="control-label">
|
||||
{t('Value')}
|
||||
</div>
|
||||
{renderFilterValueSelect(
|
||||
nativeFilterData[idx],
|
||||
idx,
|
||||
)}
|
||||
</div>
|
||||
{(idx !== 0 || isEditMode) && (
|
||||
<div className="filters-delete">
|
||||
<Icons.DeleteOutlined
|
||||
iconSize="xl"
|
||||
className="filters-trashcan"
|
||||
onClick={() => {
|
||||
handleRemoveFilterField(idx);
|
||||
remove(idx);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="filters-add-container">
|
||||
{filterNativeFilterOptions().length > 0 && (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a
|
||||
className="filters-add-btn"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
handleAddFilterField();
|
||||
add();
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleAddFilterField();
|
||||
add();
|
||||
}
|
||||
}}
|
||||
>
|
||||
+ {t('Apply another dashboard filter')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AntdForm.List>
|
||||
</AntdForm>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
{isScreenshot && (
|
||||
<StyledInputContainer
|
||||
css={
|
||||
|
||||
@@ -92,6 +92,17 @@ export type DashboardState = {
|
||||
activeTabs?: Array<string>;
|
||||
dataMask?: Object;
|
||||
anchor?: string;
|
||||
nativeFilters?: Array<ExtraNativeFilter>;
|
||||
};
|
||||
|
||||
export type ExtraNativeFilter = {
|
||||
filterName?: string;
|
||||
filterType?: string;
|
||||
columnName?: string;
|
||||
columnLabel?: string;
|
||||
filterValues?: Array<any> | [];
|
||||
nativeFilterId?: string | null;
|
||||
optionFilterValues?: Array<any> | [];
|
||||
};
|
||||
|
||||
export type Extra = {
|
||||
@@ -191,3 +202,36 @@ export enum ContentType {
|
||||
Dashboard = 'dashboard',
|
||||
Chart = 'chart',
|
||||
}
|
||||
|
||||
export type NativeFilterObject = {
|
||||
cascadeParentIds: any[];
|
||||
chartsInScope: number[];
|
||||
controlValues: {
|
||||
defaultToFirstItem: boolean;
|
||||
enableEmptyFilter: boolean;
|
||||
inverseSelection: boolean;
|
||||
multiSelect: boolean;
|
||||
searchAllOptions: boolean;
|
||||
};
|
||||
defaultDataMask: {
|
||||
extraFormData: Record<string, any>;
|
||||
filterState: Record<string, any>;
|
||||
ownState: Record<string, any>;
|
||||
};
|
||||
description: string;
|
||||
filterType: string;
|
||||
id: string;
|
||||
name: string;
|
||||
scope: {
|
||||
excluded: any[];
|
||||
rootPath: string[];
|
||||
};
|
||||
tabsInScope: string[];
|
||||
targets: Array<{
|
||||
column: {
|
||||
name: string;
|
||||
};
|
||||
datasetId: number;
|
||||
}>;
|
||||
type: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user