Compare commits

...

8 Commits

Author SHA1 Message Date
Geido
c4e0007bf6 fix(dashboard): Fill form with the latest values when undo in native filters (#16851)
* Set undoFormValues

* Reorganize

* Revert check

* Fix and clean up

* Fix pre-filter and sort values

(cherry picked from commit d3f6145aba)
2021-09-27 11:13:54 -07:00
Michael S. Molina
b27631da22 fix: Updates the selected values when changing the native filter type, column or default value (#16833)
(cherry picked from commit 1ff682f3a9)
2021-09-27 11:13:32 -07:00
Ville Brofeldt
233a52c5d8 fix(native-filters): emitted filter label format (#16828)
(cherry picked from commit 0a8d0c6e7f)
2021-09-27 11:13:15 -07:00
Ville Brofeldt
b6e3af8e09 fix(native-filters): filter indicator stale state (#16831)
(cherry picked from commit 42fa54881a)
2021-09-27 11:12:40 -07:00
Steven Uray
d96b7ad0c3 Revert "fix: Ensure alerts & reports aren't schduled when flag is off (#16639)"
This reverts commit 4dc859f89e.
2021-09-22 16:48:27 -07:00
Elizabeth Thompson
4f7f5f3f5c fix shared query (#16753)
(cherry picked from commit f032cc254c)
2021-09-22 16:48:11 -07:00
Elizabeth Thompson
8ac598dfff only fetch db function when db exists in sql lab (#16754)
(cherry picked from commit d375538671)
2021-09-22 16:47:54 -07:00
Elizabeth Thompson
054d294a85 update execution logs and states for alerts (#16736)
(cherry picked from commit 2a25e2d7ca)
2021-09-22 16:47:30 -07:00
16 changed files with 264 additions and 235 deletions

View File

@@ -612,44 +612,44 @@ describe('async actions', () => {
}); });
describe('queryEditorSetSql', () => { describe('queryEditorSetSql', () => {
const sql = 'SELECT * ';
const expectedActions = [
{
type: actions.QUERY_EDITOR_SET_SQL,
queryEditor,
sql,
},
];
describe('with backend persistence flag on', () => { describe('with backend persistence flag on', () => {
it('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const sql = 'SELECT * ';
const store = mockStore({}); const store = mockStore({});
return store return store
.dispatch(actions.queryEditorSetSql(queryEditor, sql)) .dispatch(actions.queryEditorSetSql(queryEditor, sql))
.then(() => { .then(() => {
expect(store.getActions()).toHaveLength(0); expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1); expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
}); });
}); });
}); });
}); describe('with backend persistence flag off', () => {
describe('with backend persistence flag off', () => { it('does not update the tab state in the backend', () => {
it('does not update the tab state in the backend', () => { const backendPersistenceOffMock = jest
const backendPersistenceOffMock = jest .spyOn(featureFlags, 'isFeatureEnabled')
.spyOn(featureFlags, 'isFeatureEnabled') .mockImplementation(
.mockImplementation( feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), );
);
const sql = 'SELECT * ';
const store = mockStore({});
const expectedActions = [
{
type: actions.QUERY_EDITOR_SET_SQL,
queryEditor,
sql,
},
];
store.dispatch(actions.queryEditorSetSql(queryEditor, sql)); const store = mockStore({});
expect(store.getActions()).toEqual(expectedActions); store.dispatch(actions.queryEditorSetSql(queryEditor, sql));
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
backendPersistenceOffMock.mockRestore(); expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
backendPersistenceOffMock.mockRestore();
});
}); });
}); });

View File

@@ -898,6 +898,8 @@ export function updateSavedQuery(query) {
export function queryEditorSetSql(queryEditor, sql) { export function queryEditorSetSql(queryEditor, sql) {
return function (dispatch) { return function (dispatch) {
// saved query and set tab state use this action
dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql });
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) { if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
return SupersetClient.put({ return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`), endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
@@ -914,7 +916,7 @@ export function queryEditorSetSql(queryEditor, sql) {
), ),
); );
} }
return dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql }); return Promise.resolve();
}; };
} }

View File

@@ -30,6 +30,7 @@ import {
AceCompleterKeyword, AceCompleterKeyword,
FullSQLEditor as AceEditor, FullSQLEditor as AceEditor,
} from 'src/components/AsyncAceEditor'; } from 'src/components/AsyncAceEditor';
import { QueryEditor } from '../types';
type HotKey = { type HotKey = {
key: string; key: string;
@@ -51,7 +52,7 @@ interface Props {
tables: any[]; tables: any[];
functionNames: string[]; functionNames: string[];
extendedTables: Array<{ name: string; columns: any[] }>; extendedTables: Array<{ name: string; columns: any[] }>;
queryEditor: any; queryEditor: QueryEditor;
height: string; height: string;
hotkeys: HotKey[]; hotkeys: HotKey[];
onChange: (sql: string) => void; onChange: (sql: string) => void;
@@ -86,10 +87,12 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
// Making sure no text is selected from previous mount // Making sure no text is selected from previous mount
this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null); this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null);
this.props.actions.queryEditorSetFunctionNames( if (this.props.queryEditor.dbId) {
this.props.queryEditor, this.props.actions.queryEditorSetFunctionNames(
this.props.queryEditor.dbId, this.props.queryEditor,
); this.props.queryEditor.dbId,
);
}
this.setAutoCompleter(this.props); this.setAutoCompleter(this.props);
} }
@@ -228,8 +231,8 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
getAceAnnotations() { getAceAnnotations() {
const { validationResult } = this.props.queryEditor; const { validationResult } = this.props.queryEditor;
const resultIsReady = validationResult && validationResult.completed; const resultIsReady = validationResult?.completed;
if (resultIsReady && validationResult.errors.length > 0) { if (resultIsReady && validationResult?.errors?.length) {
const errors = validationResult.errors.map((err: any) => ({ const errors = validationResult.errors.map((err: any) => ({
type: 'error', type: 'error',
row: err.line_number - 1, row: err.line_number - 1,

View File

@@ -26,16 +26,10 @@ import CopyToClipboard from 'src/components/CopyToClipboard';
import { storeQuery } from 'src/utils/common'; import { storeQuery } from 'src/utils/common';
import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import { QueryEditor } from '../types';
interface ShareSqlLabQueryPropTypes { interface ShareSqlLabQueryPropTypes {
queryEditor: { queryEditor: QueryEditor;
dbId: number;
title: string;
schema: string;
autorun: boolean;
sql: string;
remoteId: number | null;
};
addDangerToast: (msg: string) => void; addDangerToast: (msg: string) => void;
} }

View File

@@ -69,3 +69,16 @@ export type Query = {
queryLimit: number; queryLimit: number;
limitingFactor: string; limitingFactor: string;
}; };
export interface QueryEditor {
dbId?: number;
title: string;
schema: string;
autorun: boolean;
sql: string;
remoteId: number | null;
validationResult?: {
completed: boolean;
errors: SupersetError[];
};
}

View File

@@ -212,7 +212,7 @@ export const selectIndicatorsForChart = (
}; };
const cachedNativeIndicatorsForChart = {}; const cachedNativeIndicatorsForChart = {};
let cachedNativeFilterDataForChart: any = {}; const cachedNativeFilterDataForChart: any = {};
const defaultChartConfig = {}; const defaultChartConfig = {};
export const selectNativeIndicatorsForChart = ( export const selectNativeIndicatorsForChart = (
nativeFilters: Filters, nativeFilters: Filters,
@@ -230,10 +230,10 @@ export const selectNativeIndicatorsForChart = (
cachedNativeIndicatorsForChart[chartId] && cachedNativeIndicatorsForChart[chartId] &&
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) && areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) && areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
cachedNativeFilterDataForChart?.nativeFilters === nativeFilters && cachedFilterData?.nativeFilters === nativeFilters &&
cachedNativeFilterDataForChart?.dashboardLayout === dashboardLayout && cachedFilterData?.dashboardLayout === dashboardLayout &&
cachedNativeFilterDataForChart?.chartConfiguration === chartConfiguration && cachedFilterData?.chartConfiguration === chartConfiguration &&
cachedNativeFilterDataForChart?.dataMask === dataMask cachedFilterData?.dataMask === dataMask
) { ) {
return cachedNativeIndicatorsForChart[chartId]; return cachedNativeIndicatorsForChart[chartId];
} }
@@ -326,14 +326,11 @@ export const selectNativeIndicatorsForChart = (
} }
const indicators = crossFilterIndicators.concat(nativeFilterIndicators); const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
cachedNativeIndicatorsForChart[chartId] = indicators; cachedNativeIndicatorsForChart[chartId] = indicators;
cachedNativeFilterDataForChart = { cachedNativeFilterDataForChart[chartId] = {
...cachedNativeFilterDataForChart,
nativeFilters, nativeFilters,
dashboardLayout, dashboardLayout,
chartConfiguration, chartConfiguration,
dataMask, dataMask,
};
cachedNativeFilterDataForChart[chartId] = {
appliedColumns, appliedColumns,
rejectedColumns, rejectedColumns,
}; };

View File

@@ -31,6 +31,7 @@ import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask, clearDataMask } from 'src/dataMask/actions'; import { updateDataMask, clearDataMask } from 'src/dataMask/actions';
import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types'; import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types';
import { useImmer } from 'use-immer'; import { useImmer } from 'use-immer';
import { isEmpty, isEqual } from 'lodash';
import { testWithId } from 'src/utils/testUtils'; import { testWithId } from 'src/utils/testUtils';
import { Filter } from 'src/dashboard/components/nativeFilters/types'; import { Filter } from 'src/dashboard/components/nativeFilters/types';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
@@ -162,28 +163,6 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const filterValues = Object.values<Filter>(filters); const filterValues = Object.values<Filter>(filters);
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false); const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
useEffect(() => {
setDataMaskSelected(() => dataMaskApplied);
}, [JSON.stringify(dataMaskApplied), setDataMaskSelected]);
// reset filter state if filter type changes
useEffect(() => {
setDataMaskSelected(draft => {
Object.values(filters).forEach(filter => {
if (
filter.filterType !== previousFilters?.[filter.id]?.filterType &&
previousFilters?.[filter.id]?.filterType !== undefined
) {
draft[filter.id] = getInitialDataMask(filter.id) as DataMaskWithId;
}
});
});
}, [
JSON.stringify(filters),
JSON.stringify(previousFilters),
setDataMaskSelected,
]);
const handleFilterSelectionChange = ( const handleFilterSelectionChange = (
filter: Pick<Filter, 'id'> & Partial<Filter>, filter: Pick<Filter, 'id'> & Partial<Filter>,
dataMask: Partial<DataMask>, dataMask: Partial<DataMask>,
@@ -232,6 +211,37 @@ const FilterBar: React.FC<FiltersBarProps> = ({
[history], [history],
); );
useEffect(() => {
if (previousFilters) {
const updates = {};
Object.values(filters).forEach(currentFilter => {
const currentType = currentFilter.filterType;
const currentTargets = currentFilter.targets;
const currentDataMask = currentFilter.defaultDataMask;
const previousFilter = previousFilters?.[currentFilter.id];
const previousType = previousFilter?.filterType;
const previousTargets = previousFilter?.targets;
const previousDataMask = previousFilter?.defaultDataMask;
const typeChanged = currentType !== previousType;
const targetsChanged = !isEqual(currentTargets, previousTargets);
const dataMaskChanged = !isEqual(currentDataMask, previousDataMask);
if (typeChanged || targetsChanged || dataMaskChanged) {
updates[currentFilter.id] = getInitialDataMask(currentFilter.id);
}
});
if (!isEmpty(updates)) {
setDataMaskSelected(draft => ({ ...draft, ...updates }));
Object.keys(updates).forEach(key => dispatch(clearDataMask(key)));
}
}
}, [JSON.stringify(filters), JSON.stringify(previousFilters)]);
useEffect(() => {
setDataMaskSelected(() => dataMaskApplied);
}, [JSON.stringify(dataMaskApplied), setDataMaskSelected]);
const dataMaskAppliedText = JSON.stringify(dataMaskApplied); const dataMaskAppliedText = JSON.stringify(dataMaskApplied);
useEffect(() => { useEffect(() => {
publishDataMask(dataMaskApplied); publishDataMask(dataMaskApplied);

View File

@@ -315,12 +315,15 @@ const FiltersConfigForm = (
const [activeFilterPanelKey, setActiveFilterPanelKey] = useState< const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
string | string[] string | string[]
>(FilterPanels.basic.key); >(FilterPanels.basic.key);
const [undoFormValues, setUndoFormValues] = useState<Record<
string,
any
> | null>(null);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>(); const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>();
const defaultFormFilter = useMemo(() => ({}), []); const defaultFormFilter = useMemo(() => ({}), []);
const formFilter = const formValues = form.getFieldValue('filters')?.[filterId];
form.getFieldValue('filters')?.[filterId] || defaultFormFilter; const formFilter = formValues || undoFormValues || defaultFormFilter;
const nativeFilterItems = getChartMetadataRegistry().items; const nativeFilterItems = getChartMetadataRegistry().items;
const nativeFilterVizTypes = Object.entries(nativeFilterItems) const nativeFilterVizTypes = Object.entries(nativeFilterItems)
@@ -346,11 +349,11 @@ const FiltersConfigForm = (
const showTimeRangePicker = useMemo(() => { const showTimeRangePicker = useMemo(() => {
const currentDataset = Object.values(loadedDatasets).find( const currentDataset = Object.values(loadedDatasets).find(
dataset => dataset.id === formFilter.dataset?.value, dataset => dataset.id === formFilter?.dataset?.value,
); );
return currentDataset ? hasTemporalColumns(currentDataset) : true; return currentDataset ? hasTemporalColumns(currentDataset) : true;
}, [formFilter.dataset?.value, loadedDatasets]); }, [formFilter?.dataset?.value, loadedDatasets]);
// @ts-ignore // @ts-ignore
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
@@ -368,7 +371,7 @@ const FiltersConfigForm = (
forceUpdate, forceUpdate,
form, form,
filterId, filterId,
filterType: formFilter.filterType, filterType: formFilter?.filterType,
filterToEdit, filterToEdit,
formFilter, formFilter,
removed, removed,
@@ -380,31 +383,6 @@ const FiltersConfigForm = (
// @ts-ignore // @ts-ignore
const enableNoResults = !!nativeFilterItem.value?.enableNoResults; const enableNoResults = !!nativeFilterItem.value?.enableNoResults;
useEffect(() => {
if (datasetId) {
cachedSupersetGet({
endpoint: `/api/v1/dataset/${datasetId}`,
})
.then((response: JsonResponse) => {
setMetrics(response.json?.result?.metrics);
const dataset = response.json?.result;
// modify the response to fit structure expected by AdhocFilterControl
dataset.type = dataset.datasource_type;
dataset.filter_select = true;
setDatasetDetails(dataset);
})
.catch((response: SupersetApiError) => {
addDangerToast(response.message);
});
}
}, [datasetId]);
useImperativeHandle(ref, () => ({
changeTab(tab: 'configuration' | 'scoping') {
setActiveTabKey(tab);
},
}));
const hasMetrics = hasColumn && !!metrics.length; const hasMetrics = hasColumn && !!metrics.length;
const hasFilledDataset = const hasFilledDataset =
@@ -418,8 +396,6 @@ const FiltersConfigForm = (
const isDataDirty = formFilter?.isDataDirty ?? true; const isDataDirty = formFilter?.isDataDirty ?? true;
useBackendFormUpdate(form, filterId);
const setNativeFilterFieldValuesWrapper = (values: object) => { const setNativeFilterFieldValuesWrapper = (values: object) => {
setNativeFilterFieldValues(form, filterId, values); setNativeFilterFieldValues(form, filterId, values);
setError(''); setError('');
@@ -513,20 +489,6 @@ const FiltersConfigForm = (
const showDataset = const showDataset =
!datasetId || datasetDetails || formFilter?.dataset?.label; !datasetId || datasetDetails || formFilter?.dataset?.label;
useEffect(() => {
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
refreshHandler();
}
}, [
hasDataset,
hasFilledDataset,
hasDefaultValue,
formFilter,
isDataDirty,
refreshHandler,
showDataset,
]);
const formChanged = useCallback(() => { const formChanged = useCallback(() => {
form.setFields([ form.setFields([
{ {
@@ -550,15 +512,21 @@ const FiltersConfigForm = (
})); }));
const parentFilter = parentFilterOptions.find( const parentFilter = parentFilterOptions.find(
({ value }) => value === filterToEdit?.cascadeParentIds[0], ({ value }) =>
value === formFilter?.parentFilter?.value ||
value === filterToEdit?.cascadeParentIds?.[0],
); );
const hasParentFilter = !!parentFilter; const hasParentFilter = !!parentFilter;
const hasPreFilter = const hasPreFilter =
!!filterToEdit?.adhoc_filters || !!filterToEdit?.time_range; !!formFilter?.adhoc_filters ||
!!formFilter?.time_range ||
!!filterToEdit?.adhoc_filters?.length ||
!!filterToEdit?.time_range;
const hasSorting = const hasSorting =
typeof formFilter?.controlValues?.sortAscending === 'boolean' ||
typeof filterToEdit?.controlValues?.sortAscending === 'boolean'; typeof filterToEdit?.controlValues?.sortAscending === 'boolean';
let sort = filterToEdit?.controlValues?.sortAscending; let sort = filterToEdit?.controlValues?.sortAscending;
@@ -604,7 +572,7 @@ const FiltersConfigForm = (
formFilter?.filterType === 'filter_range'; formFilter?.filterType === 'filter_range';
const initialDefaultValue = const initialDefaultValue =
formFilter.filterType === filterToEdit?.filterType formFilter?.filterType === filterToEdit?.filterType
? filterToEdit?.defaultDataMask ? filterToEdit?.defaultDataMask
: null; : null;
@@ -622,6 +590,62 @@ const FiltersConfigForm = (
.some(key => controlItems[key].checked); .some(key => controlItems[key].checked);
} }
const ParentSelect = ({
value,
...rest
}: {
value?: { value: string | number };
}) => (
<Select
ariaLabel={t('Parent filter')}
placeholder={t('None')}
options={parentFilterOptions}
allowClear
value={value?.value}
{...rest}
/>
);
useEffect(() => {
if (datasetId) {
cachedSupersetGet({
endpoint: `/api/v1/dataset/${datasetId}`,
})
.then((response: JsonResponse) => {
setMetrics(response.json?.result?.metrics);
const dataset = response.json?.result;
// modify the response to fit structure expected by AdhocFilterControl
dataset.type = dataset.datasource_type;
dataset.filter_select = true;
setDatasetDetails(dataset);
})
.catch((response: SupersetApiError) => {
addDangerToast(response.message);
});
}
}, [datasetId]);
useImperativeHandle(ref, () => ({
changeTab(tab: 'configuration' | 'scoping') {
setActiveTabKey(tab);
},
}));
useBackendFormUpdate(form, filterId);
useEffect(() => {
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
refreshHandler();
}
}, [
hasDataset,
hasFilledDataset,
hasDefaultValue,
isDataDirty,
refreshHandler,
showDataset,
]);
useEffect(() => { useEffect(() => {
const activeFilterPanelKey = [FilterPanels.basic.key]; const activeFilterPanelKey = [FilterPanels.basic.key];
if (hasCheckedAdvancedControl) { if (hasCheckedAdvancedControl) {
@@ -652,21 +676,20 @@ const FiltersConfigForm = (
JSON.stringify(loadedDatasets), JSON.stringify(loadedDatasets),
]); ]);
const ParentSelect = ({ useEffect(() => {
value, // just removed, saving current form items for eventual undo
...rest if (removed) {
}: { setUndoFormValues(formValues);
value?: { value: string | number }; }
}) => ( }, [removed]);
<Select
ariaLabel={t('Parent filter')} useEffect(() => {
placeholder={t('None')} // the filter was just restored after undo
options={parentFilterOptions} if (undoFormValues && !removed) {
allowClear setNativeFilterFieldValues(form, filterId, undoFormValues);
value={value?.value} setUndoFormValues(null);
{...rest} }
/> }, [formValues, filterId, form, removed, undoFormValues]);
);
if (removed) { if (removed) {
return <RemovedFilter onClick={() => restoreFilter(filterId)} />; return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
@@ -814,7 +837,7 @@ const FiltersConfigForm = (
formChanged(); formChanged();
}} }}
> >
{formFilter.filterType && ( {!removed && (
<StyledRowSubFormItem <StyledRowSubFormItem
name={['filters', filterId, 'defaultDataMask']} name={['filters', filterId, 'defaultDataMask']}
initialValue={initialDefaultValue} initialValue={initialDefaultValue}

View File

@@ -71,14 +71,7 @@ export function updateDataMask(
} }
export function clearDataMask(filterId: string | number) { export function clearDataMask(filterId: string | number) {
return updateDataMask( return updateDataMask(filterId, getInitialDataMask(filterId));
filterId,
getInitialDataMask(filterId, {
filterState: {
value: null,
},
}),
);
} }
export type AnyDataMaskAction = export type AnyDataMaskAction =

View File

@@ -56,9 +56,7 @@ export function getInitialDataMask(
return { return {
...otherProps, ...otherProps,
extraFormData: {}, extraFormData: {},
filterState: { filterState: {},
value: undefined,
},
ownState: {}, ownState: {},
...moreProps, ...moreProps,
} as DataMaskWithId; } as DataMaskWithId;

View File

@@ -37,11 +37,7 @@ import { useImmerReducer } from 'use-immer';
import { FormItemProps } from 'antd/lib/form'; import { FormItemProps } from 'antd/lib/form';
import { PluginFilterSelectProps, SelectValue } from './types'; import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common'; import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
import { import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
formatFilterValue,
getDataRecordFormatter,
getSelectExtraFormData,
} from '../../utils';
type DataMaskAction = type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject } | { type: 'ownState'; ownState: JsonObject }
@@ -104,6 +100,15 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
extraFormData: {}, extraFormData: {},
filterState, filterState,
}); });
const datatype: GenericDataType = coltypeMap[col];
const labelFormatter = useMemo(
() =>
getDataRecordFormatter({
timeFormatter: smartDateDetailedFormatter,
}),
[],
);
const updateDataMask = useCallback( const updateDataMask = useCallback(
(values: SelectValue) => { (values: SelectValue) => {
const emptyFilter = const emptyFilter =
@@ -124,7 +129,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
filterState: { filterState: {
...filterState, ...filterState,
label: values?.length label: values?.length
? `${(values || []).map(formatFilterValue).join(', ')}${suffix}` ? `${(values || [])
.map(value => labelFormatter(value, datatype))
.join(', ')}${suffix}`
: undefined, : undefined,
value: value:
appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
@@ -133,14 +140,17 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
}, },
}); });
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[ [
appSection, appSection,
col, col,
datatype,
defaultToFirstItem, defaultToFirstItem,
dispatchDataMask, dispatchDataMask,
enableEmptyFilter, enableEmptyFilter,
inverseSelection, inverseSelection,
JSON.stringify(filterState), JSON.stringify(filterState),
labelFormatter,
], ],
); );
@@ -187,15 +197,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
unsetFocusedFilter(); unsetFocusedFilter();
}; };
const datatype: GenericDataType = coltypeMap[col];
const labelFormatter = useMemo(
() =>
getDataRecordFormatter({
timeFormatter: smartDateDetailedFormatter,
}),
[],
);
const handleChange = (value?: SelectValue | number | string) => { const handleChange = (value?: SelectValue | number | string) => {
const values = ensureIsArray(value); const values = ensureIsArray(value);
if (values.length === 0) { if (values.length === 0) {

View File

@@ -117,18 +117,3 @@ export function getDataRecordFormatter({
return String(value); return String(value);
}; };
} }
export function formatFilterValue(
value: string | number | boolean | null,
): string {
if (value === null) {
return NULL_STRING;
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return String(value);
}
return value ? TRUE_STRING : FALSE_STRING;
}

View File

@@ -40,7 +40,6 @@ from superset.models.reports import (
) )
from superset.reports.commands.alert import AlertCommand from superset.reports.commands.alert import AlertCommand
from superset.reports.commands.exceptions import ( from superset.reports.commands.exceptions import (
ReportScheduleAlertEndGracePeriodError,
ReportScheduleAlertGracePeriodError, ReportScheduleAlertGracePeriodError,
ReportScheduleCsvFailedError, ReportScheduleCsvFailedError,
ReportScheduleCsvTimeout, ReportScheduleCsvTimeout,
@@ -403,7 +402,7 @@ class BaseReportState:
def is_in_grace_period(self) -> bool: def is_in_grace_period(self) -> bool:
""" """
Checks if an alert is on it's grace period Checks if an alert is in it's grace period
""" """
last_success = ReportScheduleDAO.find_last_success_log( last_success = ReportScheduleDAO.find_last_success_log(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -418,7 +417,7 @@ class BaseReportState:
def is_in_error_grace_period(self) -> bool: def is_in_error_grace_period(self) -> bool:
""" """
Checks if an alert/report on error is on it's notification grace period Checks if an alert/report on error is in it's notification grace period
""" """
last_success = ReportScheduleDAO.find_last_error_notification( last_success = ReportScheduleDAO.find_last_error_notification(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -435,7 +434,7 @@ class BaseReportState:
def is_on_working_timeout(self) -> bool: def is_on_working_timeout(self) -> bool:
""" """
Checks if an alert is on a working timeout Checks if an alert is in a working timeout
""" """
last_working = ReportScheduleDAO.find_last_entered_working_log( last_working = ReportScheduleDAO.find_last_entered_working_log(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -533,7 +532,6 @@ class ReportSuccessState(BaseReportState):
current_states = [ReportState.SUCCESS, ReportState.GRACE] current_states = [ReportState.SUCCESS, ReportState.GRACE]
def next(self) -> None: def next(self) -> None:
self.set_state_and_log(ReportState.WORKING)
if self._report_schedule.type == ReportScheduleType.ALERT: if self._report_schedule.type == ReportScheduleType.ALERT:
if self.is_in_grace_period(): if self.is_in_grace_period():
self.set_state_and_log( self.set_state_and_log(
@@ -541,11 +539,23 @@ class ReportSuccessState(BaseReportState):
error_message=str(ReportScheduleAlertGracePeriodError()), error_message=str(ReportScheduleAlertGracePeriodError()),
) )
return return
self.set_state_and_log( self.set_state_and_log(ReportState.WORKING)
ReportState.NOOP, try:
error_message=str(ReportScheduleAlertEndGracePeriodError()), if not AlertCommand(self._report_schedule).run():
) self.set_state_and_log(ReportState.NOOP)
return return
except CommandException as ex:
self.send_error(
f"Error occurred for {self._report_schedule.type}:"
f" {self._report_schedule.name}",
str(ex),
)
self.set_state_and_log(
ReportState.ERROR,
error_message=REPORT_SCHEDULE_ERROR_NOTIFICATION_MARKER,
)
raise ex
try: try:
self.send() self.send()
self.set_state_and_log(ReportState.SUCCESS) self.set_state_and_log(ReportState.SUCCESS)

View File

@@ -19,7 +19,7 @@ import logging
from celery.exceptions import SoftTimeLimitExceeded from celery.exceptions import SoftTimeLimitExceeded
from dateutil import parser from dateutil import parser
from superset import app, is_feature_enabled from superset import app
from superset.commands.exceptions import CommandException from superset.commands.exceptions import CommandException
from superset.extensions import celery_app from superset.extensions import celery_app
from superset.reports.commands.exceptions import ReportScheduleUnexpectedError from superset.reports.commands.exceptions import ReportScheduleUnexpectedError
@@ -37,8 +37,6 @@ def scheduler() -> None:
""" """
Celery beat main scheduler for reports Celery beat main scheduler for reports
""" """
if not is_feature_enabled("ALERT_REPORTS"):
return
with session_scope(nullpool=True) as session: with session_scope(nullpool=True) as session:
active_schedules = ReportScheduleDAO.find_active(session) active_schedules = ReportScheduleDAO.find_active(session)
for active_schedule in active_schedules: for active_schedule in active_schedules:

View File

@@ -365,30 +365,47 @@ def create_alert_slack_chart_success():
cleanup_report_schedule(report_schedule) cleanup_report_schedule(report_schedule)
@pytest.fixture() @pytest.fixture(
def create_alert_slack_chart_grace(): params=["alert1",]
)
def create_alert_slack_chart_grace(request):
param_config = {
"alert1": {
"sql": "SELECT count(*) from test_table",
"validator_type": ReportScheduleValidatorType.OPERATOR,
"validator_config_json": '{"op": "<", "threshold": 10}',
},
}
with app.app_context(): with app.app_context():
chart = db.session.query(Slice).first() chart = db.session.query(Slice).first()
report_schedule = create_report_notification( example_database = get_example_database()
slack_channel="slack_channel", with create_test_table_context(example_database):
chart=chart, report_schedule = create_report_notification(
report_type=ReportScheduleType.ALERT, slack_channel="slack_channel",
) chart=chart,
report_schedule.last_state = ReportState.GRACE report_type=ReportScheduleType.ALERT,
report_schedule.last_eval_dttm = datetime(2020, 1, 1, 0, 0) database=example_database,
sql=param_config[request.param]["sql"],
validator_type=param_config[request.param]["validator_type"],
validator_config_json=param_config[request.param][
"validator_config_json"
],
)
report_schedule.last_state = ReportState.GRACE
report_schedule.last_eval_dttm = datetime(2020, 1, 1, 0, 0)
log = ReportExecutionLog( log = ReportExecutionLog(
report_schedule=report_schedule, report_schedule=report_schedule,
state=ReportState.SUCCESS, state=ReportState.SUCCESS,
start_dttm=report_schedule.last_eval_dttm, start_dttm=report_schedule.last_eval_dttm,
end_dttm=report_schedule.last_eval_dttm, end_dttm=report_schedule.last_eval_dttm,
scheduled_dttm=report_schedule.last_eval_dttm, scheduled_dttm=report_schedule.last_eval_dttm,
) )
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
yield report_schedule yield report_schedule
cleanup_report_schedule(report_schedule) cleanup_report_schedule(report_schedule)
@pytest.fixture( @pytest.fixture(
@@ -1051,11 +1068,18 @@ def test_report_schedule_success_grace(create_alert_slack_chart_success):
@pytest.mark.usefixtures("create_alert_slack_chart_grace") @pytest.mark.usefixtures("create_alert_slack_chart_grace")
def test_report_schedule_success_grace_end(create_alert_slack_chart_grace): @patch("superset.reports.notifications.slack.WebClient.files_upload")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
def test_report_schedule_success_grace_end(
screenshot_mock, file_upload_mock, create_alert_slack_chart_grace
):
""" """
ExecuteReport Command: Test report schedule on grace to noop ExecuteReport Command: Test report schedule on grace to noop
""" """
# set current time to within the grace period
screenshot_mock.return_value = SCREENSHOT_FILE
# set current time to after the grace period
current_time = create_alert_slack_chart_grace.last_eval_dttm + timedelta( current_time = create_alert_slack_chart_grace.last_eval_dttm + timedelta(
seconds=create_alert_slack_chart_grace.grace_period + 1 seconds=create_alert_slack_chart_grace.grace_period + 1
) )
@@ -1066,7 +1090,7 @@ def test_report_schedule_success_grace_end(create_alert_slack_chart_grace):
).run() ).run()
db.session.commit() db.session.commit()
assert create_alert_slack_chart_grace.last_state == ReportState.NOOP assert create_alert_slack_chart_grace.last_state == ReportState.SUCCESS
@pytest.mark.usefixtures("create_alert_email_chart") @pytest.mark.usefixtures("create_alert_email_chart")

View File

@@ -115,25 +115,3 @@ def test_scheduler_celery_no_timeout_utc(execute_mock):
db.session.delete(report_schedule) db.session.delete(report_schedule)
db.session.commit() db.session.commit()
app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True
@patch("superset.tasks.scheduler.is_feature_enabled")
@patch("superset.tasks.scheduler.execute.apply_async")
def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled):
"""
Reports scheduler: Test scheduler with feature flag off
"""
with app.app_context():
is_feature_enabled.return_value = False
report_schedule = insert_report_schedule(
type=ReportScheduleType.ALERT,
name="report",
crontab="0 9 * * *",
timezone="UTC",
)
with freeze_time("2020-01-01T09:00:00Z"):
scheduler()
execute_mock.assert_not_called()
db.session.delete(report_schedule)
db.session.commit()