This commit is contained in:
parent
ee1952e488
commit
6338ea5d42
|
|
@ -56,7 +56,6 @@ These features are **finished** but currently being tested. They are usable, but
|
||||||
- DASHBOARD_FILTERS_EXPERIMENTAL
|
- DASHBOARD_FILTERS_EXPERIMENTAL
|
||||||
- DASHBOARD_NATIVE_FILTERS
|
- DASHBOARD_NATIVE_FILTERS
|
||||||
- DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
- DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/installation/running-on-kubernetes)
|
||||||
- ENABLE_FILTER_BOX_MIGRATION
|
|
||||||
- ENABLE_JAVASCRIPT_CONTROLS
|
- ENABLE_JAVASCRIPT_CONTROLS
|
||||||
- GENERIC_CHART_AXES
|
- GENERIC_CHART_AXES
|
||||||
- GLOBAL_ASYNC_QUERIES [(docs)](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries)
|
- GLOBAL_ASYNC_QUERIES [(docs)](https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ export enum FeatureFlag {
|
||||||
ENABLE_ADVANCED_DATA_TYPES = 'ENABLE_ADVANCED_DATA_TYPES',
|
ENABLE_ADVANCED_DATA_TYPES = 'ENABLE_ADVANCED_DATA_TYPES',
|
||||||
ENABLE_DND_WITH_CLICK_UX = 'ENABLE_DND_WITH_CLICK_UX',
|
ENABLE_DND_WITH_CLICK_UX = 'ENABLE_DND_WITH_CLICK_UX',
|
||||||
ENABLE_EXPLORE_DRAG_AND_DROP = 'ENABLE_EXPLORE_DRAG_AND_DROP',
|
ENABLE_EXPLORE_DRAG_AND_DROP = 'ENABLE_EXPLORE_DRAG_AND_DROP',
|
||||||
ENABLE_FILTER_BOX_MIGRATION = 'ENABLE_FILTER_BOX_MIGRATION',
|
|
||||||
ENABLE_JAVASCRIPT_CONTROLS = 'ENABLE_JAVASCRIPT_CONTROLS',
|
ENABLE_JAVASCRIPT_CONTROLS = 'ENABLE_JAVASCRIPT_CONTROLS',
|
||||||
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',
|
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',
|
||||||
ENABLE_TEMPLATE_REMOVE_FILTERS = 'ENABLE_TEMPLATE_REMOVE_FILTERS',
|
ENABLE_TEMPLATE_REMOVE_FILTERS = 'ENABLE_TEMPLATE_REMOVE_FILTERS',
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ const propTypes = {
|
||||||
triggerRender: PropTypes.bool,
|
triggerRender: PropTypes.bool,
|
||||||
force: PropTypes.bool,
|
force: PropTypes.bool,
|
||||||
isFiltersInitialized: PropTypes.bool,
|
isFiltersInitialized: PropTypes.bool,
|
||||||
isDeactivatedViz: PropTypes.bool,
|
|
||||||
// state
|
// state
|
||||||
chartAlert: PropTypes.string,
|
chartAlert: PropTypes.string,
|
||||||
chartStatus: PropTypes.string,
|
chartStatus: PropTypes.string,
|
||||||
|
|
@ -94,7 +93,6 @@ const defaultProps = {
|
||||||
triggerRender: false,
|
triggerRender: false,
|
||||||
dashboardId: null,
|
dashboardId: null,
|
||||||
chartStackTrace: null,
|
chartStackTrace: null,
|
||||||
isDeactivatedViz: false,
|
|
||||||
force: false,
|
force: false,
|
||||||
isInView: true,
|
isInView: true,
|
||||||
};
|
};
|
||||||
|
|
@ -140,25 +138,13 @@ class Chart extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// during migration, hold chart queries before user choose review or cancel
|
if (this.props.triggerQuery) {
|
||||||
if (
|
|
||||||
this.props.triggerQuery &&
|
|
||||||
this.props.filterboxMigrationState !== 'UNDECIDED'
|
|
||||||
) {
|
|
||||||
this.runQuery();
|
this.runQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
// during migration, hold chart queries before user choose review or cancel
|
if (this.props.triggerQuery) {
|
||||||
if (
|
|
||||||
this.props.triggerQuery &&
|
|
||||||
this.props.filterboxMigrationState !== 'UNDECIDED'
|
|
||||||
) {
|
|
||||||
// if the chart is deactivated (filter_box), only load once
|
|
||||||
if (this.props.isDeactivatedViz && this.props.queriesResponse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.runQuery();
|
this.runQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +247,6 @@ class Chart extends React.PureComponent {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
chartIsStale,
|
chartIsStale,
|
||||||
queriesResponse = [],
|
queriesResponse = [],
|
||||||
isDeactivatedViz = false,
|
|
||||||
width,
|
width,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
@ -332,7 +317,7 @@ class Chart extends React.PureComponent {
|
||||||
<Loading />
|
<Loading />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isLoading && !isDeactivatedViz && <Loading />}
|
{isLoading && <Loading />}
|
||||||
</Styles>
|
</Styles>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,6 @@ export const BOOL_TRUE_DISPLAY = 'True';
|
||||||
export const BOOL_FALSE_DISPLAY = 'False';
|
export const BOOL_FALSE_DISPLAY = 'False';
|
||||||
|
|
||||||
export const URL_PARAMS = {
|
export const URL_PARAMS = {
|
||||||
migrationState: {
|
|
||||||
name: 'migration_state',
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
standalone: {
|
standalone: {
|
||||||
name: 'standalone',
|
name: 'standalone',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,9 @@ import newComponentFactory from 'src/dashboard/util/newComponentFactory';
|
||||||
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
|
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
||||||
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
|
||||||
import extractUrlParams from '../util/extractUrlParams';
|
import extractUrlParams from '../util/extractUrlParams';
|
||||||
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
|
|
||||||
import { updateColorSchema } from './dashboardInfo';
|
import { updateColorSchema } from './dashboardInfo';
|
||||||
import { getChartIdsInFilterScope } from '../util/getChartIdsInFilterScope';
|
import { getChartIdsInFilterScope } from '../util/getChartIdsInFilterScope';
|
||||||
import updateComponentParentsList from '../util/updateComponentParentsList';
|
import updateComponentParentsList from '../util/updateComponentParentsList';
|
||||||
|
|
@ -63,14 +61,7 @@ import { FilterBarOrientation } from '../types';
|
||||||
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||||
|
|
||||||
export const hydrateDashboard =
|
export const hydrateDashboard =
|
||||||
({
|
({ history, dashboard, charts, dataMask, activeTabs }) =>
|
||||||
history,
|
|
||||||
dashboard,
|
|
||||||
charts,
|
|
||||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
dataMask,
|
|
||||||
activeTabs,
|
|
||||||
}) =>
|
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
const { user, common, dashboardState } = getState();
|
const { user, common, dashboardState } = getState();
|
||||||
const { metadata, position_data: positionData } = dashboard;
|
const { metadata, position_data: positionData } = dashboard;
|
||||||
|
|
@ -232,25 +223,18 @@ export const hydrateDashboard =
|
||||||
const componentId = chartIdToLayoutId[key];
|
const componentId = chartIdToLayoutId[key];
|
||||||
const directPathToFilter = (layout[componentId].parents || []).slice();
|
const directPathToFilter = (layout[componentId].parents || []).slice();
|
||||||
directPathToFilter.push(componentId);
|
directPathToFilter.push(componentId);
|
||||||
if (
|
dashboardFilters[key] = {
|
||||||
[
|
...dashboardFilter,
|
||||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
chartId: key,
|
||||||
FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
componentId,
|
||||||
].includes(filterboxMigrationState)
|
datasourceId: slice.form_data.datasource,
|
||||||
) {
|
filterName: slice.slice_name,
|
||||||
dashboardFilters[key] = {
|
directPathToFilter,
|
||||||
...dashboardFilter,
|
columns,
|
||||||
chartId: key,
|
labels,
|
||||||
componentId,
|
scopes: scopesByChartId,
|
||||||
datasourceId: slice.form_data.datasource,
|
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
|
||||||
filterName: slice.slice_name,
|
};
|
||||||
directPathToFilter,
|
|
||||||
columns,
|
|
||||||
labels,
|
|
||||||
scopes: scopesByChartId,
|
|
||||||
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync layout names with current slice names in case a slice was edited
|
// sync layout names with current slice names in case a slice was edited
|
||||||
|
|
@ -319,28 +303,12 @@ export const hydrateDashboard =
|
||||||
directPathToChild.push(directLinkComponentId);
|
directPathToChild.push(directLinkComponentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// should convert filter_box to filter component?
|
|
||||||
let filterConfig = metadata?.native_filter_configuration || [];
|
|
||||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
|
||||||
filterConfig = getNativeFilterConfig(
|
|
||||||
charts,
|
|
||||||
filterScopes,
|
|
||||||
preselectFilters,
|
|
||||||
);
|
|
||||||
metadata.native_filter_configuration = filterConfig;
|
|
||||||
metadata.show_native_filters = true;
|
|
||||||
}
|
|
||||||
const nativeFilters = getInitialNativeFilterState({
|
const nativeFilters = getInitialNativeFilterState({
|
||||||
filterConfig,
|
filterConfig: metadata?.native_filter_configuration || [],
|
||||||
});
|
});
|
||||||
metadata.show_native_filters =
|
metadata.show_native_filters = isFeatureEnabled(
|
||||||
dashboard?.metadata?.show_native_filters ??
|
FeatureFlag.DASHBOARD_NATIVE_FILTERS,
|
||||||
(isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
|
);
|
||||||
[
|
|
||||||
FILTER_BOX_MIGRATION_STATES.CONVERTED,
|
|
||||||
FILTER_BOX_MIGRATION_STATES.REVIEWING,
|
|
||||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
].includes(filterboxMigrationState));
|
|
||||||
|
|
||||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
|
if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
|
||||||
// If user just added cross filter to dashboard it's not saving it scope on server,
|
// If user just added cross filter to dashboard it's not saving it scope on server,
|
||||||
|
|
@ -465,7 +433,6 @@ export const hydrateDashboard =
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
isFiltersRefreshing: false,
|
isFiltersRefreshing: false,
|
||||||
activeTabs: activeTabs || dashboardState?.activeTabs || [],
|
activeTabs: activeTabs || dashboardState?.activeTabs || [],
|
||||||
filterboxMigrationState,
|
|
||||||
datasetsStatus: ResourceStatus.LOADING,
|
datasetsStatus: ResourceStatus.LOADING,
|
||||||
},
|
},
|
||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ export function fetchAllSlicesFailed(error) {
|
||||||
|
|
||||||
export function fetchSlices(
|
export function fetchSlices(
|
||||||
userId,
|
userId,
|
||||||
excludeFilterBox,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
filter_value,
|
filter_value,
|
||||||
sortColumn = 'changed_on',
|
sortColumn = 'changed_on',
|
||||||
|
|
@ -84,11 +83,7 @@ export function fetchSlices(
|
||||||
})}`,
|
})}`,
|
||||||
})
|
})
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
let { result } = json;
|
const { result } = json;
|
||||||
// disable add filter_box viz to dashboard
|
|
||||||
if (excludeFilterBox) {
|
|
||||||
result = result.filter(slice => slice.viz_type !== 'filter_box');
|
|
||||||
}
|
|
||||||
result.forEach(slice => {
|
result.forEach(slice => {
|
||||||
let form_data = JSON.parse(slice.params);
|
let form_data = JSON.parse(slice.params);
|
||||||
form_data = {
|
form_data = {
|
||||||
|
|
@ -135,46 +130,31 @@ export function fetchSlices(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAllSlices(userId, excludeFilterBox = false) {
|
export function fetchAllSlices(userId) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { sliceEntities } = getState();
|
const { sliceEntities } = getState();
|
||||||
if (sliceEntities.lastUpdated === 0) {
|
if (sliceEntities.lastUpdated === 0) {
|
||||||
dispatch(fetchAllSlicesStarted());
|
dispatch(fetchAllSlicesStarted());
|
||||||
return fetchSlices(userId, excludeFilterBox, dispatch, undefined);
|
return fetchSlices(userId, dispatch, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(setAllSlices(sliceEntities.slices));
|
return dispatch(setAllSlices(sliceEntities.slices));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSortedSlices(
|
export function fetchSortedSlices(userId, order_column) {
|
||||||
userId,
|
|
||||||
excludeFilterBox = false,
|
|
||||||
order_column,
|
|
||||||
) {
|
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(fetchAllSlicesStarted());
|
dispatch(fetchAllSlicesStarted());
|
||||||
return fetchSlices(
|
return fetchSlices(userId, dispatch, undefined, order_column);
|
||||||
userId,
|
|
||||||
excludeFilterBox,
|
|
||||||
dispatch,
|
|
||||||
undefined,
|
|
||||||
order_column,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchFilteredSlices(
|
export function fetchFilteredSlices(userId, filter_value) {
|
||||||
userId,
|
|
||||||
excludeFilterBox = false,
|
|
||||||
filter_value,
|
|
||||||
) {
|
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchAllSlicesStarted());
|
dispatch(fetchAllSlicesStarted());
|
||||||
const { sliceEntities } = getState();
|
const { sliceEntities } = getState();
|
||||||
return fetchSlices(
|
return fetchSlices(
|
||||||
userId,
|
userId,
|
||||||
excludeFilterBox,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
filter_value,
|
filter_value,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ describe('slice entity actions', () => {
|
||||||
describe('fetchFilteredSlices', () => {
|
describe('fetchFilteredSlices', () => {
|
||||||
it('should dispatch an fetchAllSlicesStarted action', async () => {
|
it('should dispatch an fetchAllSlicesStarted action', async () => {
|
||||||
const { dispatch, getState } = setup();
|
const { dispatch, getState } = setup();
|
||||||
const thunk1 = fetchFilteredSlices('userId', false, 'filter_value');
|
const thunk1 = fetchFilteredSlices('userId', 'filter_value');
|
||||||
await thunk1(dispatch, getState);
|
await thunk1(dispatch, getState);
|
||||||
expect(dispatch.getCall(0).args[0]).toEqual({
|
expect(dispatch.getCall(0).args[0]).toEqual({
|
||||||
type: FETCH_ALL_SLICES_STARTED,
|
type: FETCH_ALL_SLICES_STARTED,
|
||||||
|
|
@ -82,7 +82,7 @@ describe('slice entity actions', () => {
|
||||||
sliceEntities: { slices: {}, lastUpdated: 1 },
|
sliceEntities: { slices: {}, lastUpdated: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const thunk1 = fetchAllSlices('userId', false, 'filter_value');
|
const thunk1 = fetchAllSlices('userId', 'filter_value');
|
||||||
await thunk1(dispatch, getState);
|
await thunk1(dispatch, getState);
|
||||||
|
|
||||||
expect(spy.get.callCount).toBe(0);
|
expect(spy.get.callCount).toBe(0);
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@
|
||||||
*/
|
*/
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||||
import { useCallback, useEffect, useState, useContext } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { RootState } from 'src/dashboard/types';
|
import { RootState } from 'src/dashboard/types';
|
||||||
import { MigrationContext } from 'src/dashboard/containers/DashboardPage';
|
|
||||||
import {
|
import {
|
||||||
useFilters,
|
useFilters,
|
||||||
useNativeFiltersDataMask,
|
useNativeFiltersDataMask,
|
||||||
|
|
@ -30,7 +29,6 @@ import {
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const useNativeFilters = () => {
|
export const useNativeFilters = () => {
|
||||||
const filterboxMigrationState = useContext(MigrationContext);
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
const showNativeFilters = useSelector<RootState, boolean>(
|
const showNativeFilters = useSelector<RootState, boolean>(
|
||||||
state =>
|
state =>
|
||||||
|
|
@ -78,15 +76,13 @@ export const useNativeFilters = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
expandFilters === false ||
|
expandFilters === false ||
|
||||||
(filterValues.length === 0 &&
|
(filterValues.length === 0 && nativeFiltersEnabled)
|
||||||
nativeFiltersEnabled &&
|
|
||||||
['CONVERTED', 'REVIEWING', 'NOOP'].includes(filterboxMigrationState))
|
|
||||||
) {
|
) {
|
||||||
toggleDashboardFiltersOpen(false);
|
toggleDashboardFiltersOpen(false);
|
||||||
} else {
|
} else {
|
||||||
toggleDashboardFiltersOpen(true);
|
toggleDashboardFiltersOpen(true);
|
||||||
}
|
}
|
||||||
}, [filterValues.length, filterboxMigrationState]);
|
}, [filterValues.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showDashboard) {
|
if (showDashboard) {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import downloadAsImage from 'src/utils/downloadAsImage';
|
||||||
import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
|
import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
|
||||||
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
|
@ -70,11 +69,6 @@ const propTypes = {
|
||||||
refreshLimit: PropTypes.number,
|
refreshLimit: PropTypes.number,
|
||||||
refreshWarning: PropTypes.string,
|
refreshWarning: PropTypes.string,
|
||||||
lastModifiedTime: PropTypes.number.isRequired,
|
lastModifiedTime: PropTypes.number.isRequired,
|
||||||
filterboxMigrationState: PropTypes.oneOf(
|
|
||||||
Object.keys(FILTER_BOX_MIGRATION_STATES).map(
|
|
||||||
key => FILTER_BOX_MIGRATION_STATES[key],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
|
@ -82,7 +76,6 @@ const defaultProps = {
|
||||||
colorScheme: undefined,
|
colorScheme: undefined,
|
||||||
refreshLimit: 0,
|
refreshLimit: 0,
|
||||||
refreshWarning: null,
|
refreshWarning: null,
|
||||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MENU_KEYS = {
|
const MENU_KEYS = {
|
||||||
|
|
@ -230,7 +223,6 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
lastModifiedTime,
|
lastModifiedTime,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
filterboxMigrationState,
|
|
||||||
setIsDropdownVisible,
|
setIsDropdownVisible,
|
||||||
isDropdownVisible,
|
isDropdownVisible,
|
||||||
...rest
|
...rest
|
||||||
|
|
@ -378,15 +370,14 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
</Menu>
|
</Menu>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{editMode &&
|
{editMode && (
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.CONVERTED && (
|
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
||||||
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
<FilterScopeModal
|
||||||
<FilterScopeModal
|
className="m-r-5"
|
||||||
className="m-r-5"
|
triggerNode={t('Set filter mapping')}
|
||||||
triggerNode={t('Set filter mapping')}
|
/>
|
||||||
/>
|
</Menu.Item>
|
||||||
</Menu.Item>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu.Item key={MENU_KEYS.AUTOREFRESH_MODAL}>
|
<Menu.Item key={MENU_KEYS.AUTOREFRESH_MODAL}>
|
||||||
<RefreshIntervalModal
|
<RefreshIntervalModal
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ import {
|
||||||
import setPeriodicRunner, {
|
import setPeriodicRunner, {
|
||||||
stopPeriodicRender,
|
stopPeriodicRender,
|
||||||
} from 'src/dashboard/util/setPeriodicRunner';
|
} from 'src/dashboard/util/setPeriodicRunner';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
|
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
|
||||||
import { DashboardEmbedModal } from '../DashboardEmbedControls';
|
import { DashboardEmbedModal } from '../DashboardEmbedControls';
|
||||||
import OverwriteConfirm from '../OverwriteConfirm';
|
import OverwriteConfirm from '../OverwriteConfirm';
|
||||||
|
|
@ -463,18 +462,13 @@ class Header extends React.PureComponent {
|
||||||
shouldPersistRefreshFrequency,
|
shouldPersistRefreshFrequency,
|
||||||
setRefreshFrequency,
|
setRefreshFrequency,
|
||||||
lastModifiedTime,
|
lastModifiedTime,
|
||||||
filterboxMigrationState,
|
|
||||||
logEvent,
|
logEvent,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const userCanEdit =
|
const userCanEdit =
|
||||||
dashboardInfo.dash_edit_perm &&
|
dashboardInfo.dash_edit_perm && !dashboardInfo.is_managed_externally;
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING &&
|
|
||||||
!dashboardInfo.is_managed_externally;
|
|
||||||
const userCanShare = dashboardInfo.dash_share_perm;
|
const userCanShare = dashboardInfo.dash_share_perm;
|
||||||
const userCanSaveAs =
|
const userCanSaveAs = dashboardInfo.dash_save_perm;
|
||||||
dashboardInfo.dash_save_perm &&
|
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING;
|
|
||||||
const userCanCurate =
|
const userCanCurate =
|
||||||
isFeatureEnabled(FeatureFlag.EMBEDDED_SUPERSET) &&
|
isFeatureEnabled(FeatureFlag.EMBEDDED_SUPERSET) &&
|
||||||
findPermission('can_set_embedded', 'Dashboard', user.roles);
|
findPermission('can_set_embedded', 'Dashboard', user.roles);
|
||||||
|
|
@ -680,7 +674,6 @@ class Header extends React.PureComponent {
|
||||||
refreshLimit={refreshLimit}
|
refreshLimit={refreshLimit}
|
||||||
refreshWarning={refreshWarning}
|
refreshWarning={refreshWarning}
|
||||||
lastModifiedTime={lastModifiedTime}
|
lastModifiedTime={lastModifiedTime}
|
||||||
filterboxMigrationState={filterboxMigrationState}
|
|
||||||
isDropdownVisible={this.state.isDropdownVisible}
|
isDropdownVisible={this.state.isDropdownVisible}
|
||||||
setIsDropdownVisible={this.setIsDropdownVisible}
|
setIsDropdownVisible={this.setIsDropdownVisible}
|
||||||
logEvent={logEvent}
|
logEvent={logEvent}
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,7 @@ import PropTypes from 'prop-types';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import { createFilter } from 'react-search-input';
|
import { createFilter } from 'react-search-input';
|
||||||
import {
|
import { t, styled, css } from '@superset-ui/core';
|
||||||
t,
|
|
||||||
styled,
|
|
||||||
isFeatureEnabled,
|
|
||||||
FeatureFlag,
|
|
||||||
css,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { Input } from 'src/components/Input';
|
import { Input } from 'src/components/Input';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
|
|
@ -43,7 +37,6 @@ import {
|
||||||
NEW_COMPONENTS_SOURCE_ID,
|
NEW_COMPONENTS_SOURCE_ID,
|
||||||
} from 'src/dashboard/util/constants';
|
} from 'src/dashboard/util/constants';
|
||||||
import { slicePropShape } from 'src/dashboard/util/propShapes';
|
import { slicePropShape } from 'src/dashboard/util/propShapes';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import AddSliceCard from './AddSliceCard';
|
import AddSliceCard from './AddSliceCard';
|
||||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||||
|
|
@ -58,7 +51,6 @@ const propTypes = {
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
||||||
editMode: PropTypes.bool,
|
editMode: PropTypes.bool,
|
||||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
|
||||||
dashboardId: PropTypes.number,
|
dashboardId: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,7 +58,6 @@ const defaultProps = {
|
||||||
selectedSliceIds: [],
|
selectedSliceIds: [],
|
||||||
editMode: false,
|
editMode: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
|
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
|
||||||
|
|
@ -150,12 +141,7 @@ class SliceAdder extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { userId, filterboxMigrationState } = this.props;
|
this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
|
||||||
this.slicesRequest = this.props.fetchAllSlices(
|
|
||||||
userId,
|
|
||||||
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
|
@ -198,13 +184,8 @@ class SliceAdder extends React.Component {
|
||||||
handleChange = _.debounce(value => {
|
handleChange = _.debounce(value => {
|
||||||
this.searchUpdated(value);
|
this.searchUpdated(value);
|
||||||
|
|
||||||
const { userId, filterboxMigrationState } = this.props;
|
const { userId } = this.props;
|
||||||
this.slicesRequest = this.props.fetchFilteredSlices(
|
this.slicesRequest = this.props.fetchFilteredSlices(userId, value);
|
||||||
userId,
|
|
||||||
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
searchUpdated(searchTerm) {
|
searchUpdated(searchTerm) {
|
||||||
|
|
@ -226,13 +207,8 @@ class SliceAdder extends React.Component {
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { userId, filterboxMigrationState } = this.props;
|
const { userId } = this.props;
|
||||||
this.slicesRequest = this.props.fetchSortedSlices(
|
this.slicesRequest = this.props.fetchSortedSlices(userId, sortBy);
|
||||||
userId,
|
|
||||||
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
|
||||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
|
||||||
sortBy,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rowRenderer({ key, index, style }) {
|
rowRenderer({ key, index, style }) {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import {
|
||||||
LOG_ACTIONS_FORCE_REFRESH_CHART,
|
LOG_ACTIONS_FORCE_REFRESH_CHART,
|
||||||
} from 'src/logger/LogUtils';
|
} from 'src/logger/LogUtils';
|
||||||
import { areObjectsEqual } from 'src/reduxUtils';
|
import { areObjectsEqual } from 'src/reduxUtils';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
|
|
||||||
|
|
@ -70,11 +69,6 @@ const propTypes = {
|
||||||
sliceName: PropTypes.string.isRequired,
|
sliceName: PropTypes.string.isRequired,
|
||||||
timeout: PropTypes.number.isRequired,
|
timeout: PropTypes.number.isRequired,
|
||||||
maxRows: PropTypes.number.isRequired,
|
maxRows: PropTypes.number.isRequired,
|
||||||
filterboxMigrationState: PropTypes.oneOf(
|
|
||||||
Object.keys(FILTER_BOX_MIGRATION_STATES).map(
|
|
||||||
key => FILTER_BOX_MIGRATION_STATES[key],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// all active filter fields in dashboard
|
// all active filter fields in dashboard
|
||||||
filters: PropTypes.object.isRequired,
|
filters: PropTypes.object.isRequired,
|
||||||
refreshChart: PropTypes.func.isRequired,
|
refreshChart: PropTypes.func.isRequired,
|
||||||
|
|
@ -129,11 +123,6 @@ const ChartOverlay = styled.div`
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
|
||||||
&.is-deactivated {
|
|
||||||
opacity: 0.5;
|
|
||||||
background-color: ${({ theme }) => theme.colors.grayscale.light1};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SliceContainer = styled.div`
|
const SliceContainer = styled.div`
|
||||||
|
|
@ -405,7 +394,6 @@ class Chart extends React.Component {
|
||||||
handleToggleFullSize,
|
handleToggleFullSize,
|
||||||
isFullSize,
|
isFullSize,
|
||||||
setControlValue,
|
setControlValue,
|
||||||
filterboxMigrationState,
|
|
||||||
postTransformProps,
|
postTransformProps,
|
||||||
datasetsStatus,
|
datasetsStatus,
|
||||||
isInView,
|
isInView,
|
||||||
|
|
@ -422,12 +410,6 @@ class Chart extends React.Component {
|
||||||
|
|
||||||
const { queriesResponse, chartUpdateEndTime, chartStatus } = chart;
|
const { queriesResponse, chartUpdateEndTime, chartStatus } = chart;
|
||||||
const isLoading = chartStatus === 'loading';
|
const isLoading = chartStatus === 'loading';
|
||||||
const isDeactivatedViz =
|
|
||||||
slice.viz_type === 'filter_box' &&
|
|
||||||
[
|
|
||||||
FILTER_BOX_MIGRATION_STATES.REVIEWING,
|
|
||||||
FILTER_BOX_MIGRATION_STATES.CONVERTED,
|
|
||||||
].includes(filterboxMigrationState);
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||||
const cachedDttm =
|
const cachedDttm =
|
||||||
|
|
@ -506,15 +488,15 @@ class Chart extends React.Component {
|
||||||
isOverflowable && 'dashboard-chart--overflowable',
|
isOverflowable && 'dashboard-chart--overflowable',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{(isLoading || isDeactivatedViz) && (
|
{isLoading && (
|
||||||
<ChartOverlay
|
<ChartOverlay
|
||||||
className={cx(isDeactivatedViz && 'is-deactivated')}
|
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
height: this.getChartHeight(),
|
height: this.getChartHeight(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
width={width}
|
width={width}
|
||||||
height={this.getChartHeight()}
|
height={this.getChartHeight()}
|
||||||
|
|
@ -538,8 +520,6 @@ class Chart extends React.Component {
|
||||||
triggerQuery={chart.triggerQuery}
|
triggerQuery={chart.triggerQuery}
|
||||||
vizType={slice.viz_type}
|
vizType={slice.viz_type}
|
||||||
setControlValue={setControlValue}
|
setControlValue={setControlValue}
|
||||||
isDeactivatedViz={isDeactivatedViz}
|
|
||||||
filterboxMigrationState={filterboxMigrationState}
|
|
||||||
postTransformProps={postTransformProps}
|
postTransformProps={postTransformProps}
|
||||||
datasetsStatus={datasetsStatus}
|
datasetsStatus={datasetsStatus}
|
||||||
isInView={isInView}
|
isInView={isInView}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { connect } from 'react-redux';
|
||||||
import { LineEditableTabs } from 'src/components/Tabs';
|
import { LineEditableTabs } from 'src/components/Tabs';
|
||||||
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
|
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
|
||||||
import { AntdModal } from 'src/components';
|
import { AntdModal } from 'src/components';
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import DragDroppable from '../dnd/DragDroppable';
|
import DragDroppable from '../dnd/DragDroppable';
|
||||||
import DragHandle from '../dnd/DragHandle';
|
import DragHandle from '../dnd/DragHandle';
|
||||||
import DashboardComponent from '../../containers/DashboardComponent';
|
import DashboardComponent from '../../containers/DashboardComponent';
|
||||||
|
|
@ -49,11 +48,6 @@ const propTypes = {
|
||||||
renderHoverMenu: PropTypes.bool,
|
renderHoverMenu: PropTypes.bool,
|
||||||
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
||||||
activeTabs: PropTypes.arrayOf(PropTypes.string),
|
activeTabs: PropTypes.arrayOf(PropTypes.string),
|
||||||
filterboxMigrationState: PropTypes.oneOf(
|
|
||||||
Object.keys(FILTER_BOX_MIGRATION_STATES).map(
|
|
||||||
key => FILTER_BOX_MIGRATION_STATES[key],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// actions (from DashboardComponent.jsx)
|
// actions (from DashboardComponent.jsx)
|
||||||
logEvent: PropTypes.func.isRequired,
|
logEvent: PropTypes.func.isRequired,
|
||||||
|
|
@ -81,7 +75,6 @@ const defaultProps = {
|
||||||
columnWidth: 0,
|
columnWidth: 0,
|
||||||
activeTabs: [],
|
activeTabs: [],
|
||||||
directPathToChild: [],
|
directPathToChild: [],
|
||||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
setActiveTabs() {},
|
setActiveTabs() {},
|
||||||
onResizeStart() {},
|
onResizeStart() {},
|
||||||
onResize() {},
|
onResize() {},
|
||||||
|
|
@ -136,10 +129,7 @@ export class Tabs extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (
|
if (prevState.activeKey !== this.state.activeKey) {
|
||||||
prevState.activeKey !== this.state.activeKey ||
|
|
||||||
prevProps.filterboxMigrationState !== this.props.filterboxMigrationState
|
|
||||||
) {
|
|
||||||
this.props.setActiveTabs(this.state.activeKey, prevState.activeKey);
|
this.props.setActiveTabs(this.state.activeKey, prevState.activeKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -446,7 +436,6 @@ function mapStateToProps(state) {
|
||||||
nativeFilters: state.nativeFilters,
|
nativeFilters: state.nativeFilters,
|
||||||
activeTabs: state.dashboardState.activeTabs,
|
activeTabs: state.dashboardState.activeTabs,
|
||||||
directPathToChild: state.dashboardState.directPathToChild,
|
directPathToChild: state.dashboardState.directPathToChild,
|
||||||
filterboxMigrationState: state.dashboardState.filterboxMigrationState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps)(Tabs);
|
export default connect(mapStateToProps)(Tabs);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { filter, keyBy } from 'lodash';
|
|
||||||
import {
|
import {
|
||||||
DataMaskState,
|
DataMaskState,
|
||||||
DataMaskStateWithId,
|
DataMaskStateWithId,
|
||||||
|
|
@ -27,10 +26,8 @@ import {
|
||||||
Filters,
|
Filters,
|
||||||
FilterSets as FilterSetsType,
|
FilterSets as FilterSetsType,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { ChartsState, RootState } from 'src/dashboard/types';
|
import { ChartsState, RootState } from 'src/dashboard/types';
|
||||||
import { MigrationContext } from 'src/dashboard/containers/DashboardPage';
|
|
||||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
|
||||||
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
||||||
|
|
||||||
export const useFilterSets = () =>
|
export const useFilterSets = () =>
|
||||||
|
|
@ -102,30 +99,14 @@ export const useFilterUpdates = (
|
||||||
export const useInitialization = () => {
|
export const useInitialization = () => {
|
||||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||||
const filters = useFilters();
|
const filters = useFilters();
|
||||||
const filterboxMigrationState = useContext(MigrationContext);
|
const charts = useSelector<RootState, ChartsState>(state => state.charts);
|
||||||
let charts = useSelector<RootState, ChartsState>(state => state.charts);
|
|
||||||
|
|
||||||
// We need to know how much charts now shown on dashboard to know how many of all charts should be loaded
|
// We need to know how much charts now shown on dashboard to know how many of all charts should be loaded
|
||||||
let numberOfLoadingCharts = 0;
|
let numberOfLoadingCharts = 0;
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
// do not load filter_box in reviewing
|
numberOfLoadingCharts = document.querySelectorAll(
|
||||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
'[data-ui-anchor="chart"]',
|
||||||
charts = keyBy(
|
).length;
|
||||||
filter(charts, chart => chart.form_data?.viz_type !== 'filter_box'),
|
|
||||||
'id',
|
|
||||||
);
|
|
||||||
const numberOfFilterbox = document.querySelectorAll(
|
|
||||||
'[data-test-viz-type="filter_box"]',
|
|
||||||
).length;
|
|
||||||
|
|
||||||
numberOfLoadingCharts =
|
|
||||||
document.querySelectorAll('[data-ui-anchor="chart"]').length -
|
|
||||||
numberOfFilterbox;
|
|
||||||
} else {
|
|
||||||
numberOfLoadingCharts = document.querySelectorAll(
|
|
||||||
'[data-ui-anchor="chart"]',
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,6 @@ function mapStateToProps(
|
||||||
filterState: dataMask[id]?.filterState,
|
filterState: dataMask[id]?.filterState,
|
||||||
maxRows: common.conf.SQL_MAX_ROW,
|
maxRows: common.conf.SQL_MAX_ROW,
|
||||||
setControlValue,
|
setControlValue,
|
||||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
|
||||||
datasetsStatus,
|
datasetsStatus,
|
||||||
emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
|
emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ function mapStateToProps({
|
||||||
slug: dashboardInfo.slug,
|
slug: dashboardInfo.slug,
|
||||||
metadata: dashboardInfo.metadata,
|
metadata: dashboardInfo.metadata,
|
||||||
reports,
|
reports,
|
||||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
CategoricalColorNamespace,
|
CategoricalColorNamespace,
|
||||||
|
|
@ -25,14 +25,11 @@ import {
|
||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
SharedLabelColorSource,
|
SharedLabelColorSource,
|
||||||
t,
|
t,
|
||||||
useTheme,
|
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Global } from '@emotion/react';
|
|
||||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import FilterBoxMigrationModal from 'src/dashboard/components/FilterBoxMigrationModal';
|
|
||||||
import {
|
import {
|
||||||
useDashboard,
|
useDashboard,
|
||||||
useDashboardCharts,
|
useDashboardCharts,
|
||||||
|
|
@ -42,37 +39,25 @@ import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
|
||||||
import { setDatasources } from 'src/dashboard/actions/datasources';
|
import { setDatasources } from 'src/dashboard/actions/datasources';
|
||||||
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
||||||
import setupPlugins from 'src/setup/setupPlugins';
|
import setupPlugins from 'src/setup/setupPlugins';
|
||||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
|
||||||
import { addWarningToast } from 'src/components/MessageToasts/actions';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getItem,
|
getItem,
|
||||||
LocalStorageKeys,
|
LocalStorageKeys,
|
||||||
setItem,
|
setItem,
|
||||||
} from 'src/utils/localStorageHelpers';
|
} from 'src/utils/localStorageHelpers';
|
||||||
import {
|
|
||||||
FILTER_BOX_MIGRATION_STATES,
|
|
||||||
FILTER_BOX_TRANSITION_SNOOZE_DURATION,
|
|
||||||
} from 'src/explore/constants';
|
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { canUserEditDashboard } from 'src/dashboard/util/permissionUtils';
|
|
||||||
import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
|
import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
|
||||||
import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
|
import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
|
||||||
import {
|
import {
|
||||||
getFilterValue,
|
getFilterValue,
|
||||||
getPermalinkValue,
|
getPermalinkValue,
|
||||||
} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
|
} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
|
||||||
import { filterCardPopoverStyle, headerStyles } from 'src/dashboard/styles';
|
|
||||||
import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
|
import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { RootState } from '../types';
|
import { RootState } from '../types';
|
||||||
import { getActiveFilters } from '../util/activeDashboardFilters';
|
import { getActiveFilters } from '../util/activeDashboardFilters';
|
||||||
|
|
||||||
export const MigrationContext = React.createContext(
|
|
||||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DashboardPageIdContext = React.createContext('');
|
export const DashboardPageIdContext = React.createContext('');
|
||||||
|
|
||||||
setupPlugins();
|
setupPlugins();
|
||||||
|
|
@ -156,11 +141,7 @@ const useSyncDashboardStateWithLocalStorage = () => {
|
||||||
|
|
||||||
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const theme = useTheme();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
|
||||||
state => state.user,
|
|
||||||
);
|
|
||||||
const dashboardPageId = useSyncDashboardStateWithLocalStorage();
|
const dashboardPageId = useSyncDashboardStateWithLocalStorage();
|
||||||
const { addDangerToast } = useToasts();
|
const { addDangerToast } = useToasts();
|
||||||
const { result: dashboard, error: dashboardApiError } =
|
const { result: dashboard, error: dashboardApiError } =
|
||||||
|
|
@ -176,16 +157,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
|
|
||||||
const error = dashboardApiError || chartsApiError;
|
const error = dashboardApiError || chartsApiError;
|
||||||
const readyToRender = Boolean(dashboard && charts);
|
const readyToRender = Boolean(dashboard && charts);
|
||||||
const migrationStateParam = getUrlParam(
|
|
||||||
URL_PARAMS.migrationState,
|
|
||||||
) as FILTER_BOX_MIGRATION_STATES;
|
|
||||||
const isMigrationEnabled = isFeatureEnabled(
|
|
||||||
FeatureFlag.ENABLE_FILTER_BOX_MIGRATION,
|
|
||||||
);
|
|
||||||
const { dashboard_title, css, metadata, id = 0 } = dashboard || {};
|
const { dashboard_title, css, metadata, id = 0 } = dashboard || {};
|
||||||
const [filterboxMigrationState, setFilterboxMigrationState] = useState(
|
|
||||||
migrationStateParam || FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// mark tab id as redundant when user closes browser tab - a new id will be
|
// mark tab id as redundant when user closes browser tab - a new id will be
|
||||||
|
|
@ -210,67 +182,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
dispatch(setDatasetsStatus(status));
|
dispatch(setDatasetsStatus(status));
|
||||||
}, [dispatch, status]);
|
}, [dispatch, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// should convert filter_box to filter component?
|
|
||||||
const hasFilterBox = charts?.some(
|
|
||||||
chart => chart.form_data?.viz_type === 'filter_box',
|
|
||||||
);
|
|
||||||
const canEdit = dashboard && canUserEditDashboard(dashboard, user);
|
|
||||||
|
|
||||||
if (canEdit) {
|
|
||||||
// can user edit dashboard?
|
|
||||||
if (metadata?.native_filter_configuration) {
|
|
||||||
setFilterboxMigrationState(
|
|
||||||
isMigrationEnabled
|
|
||||||
? FILTER_BOX_MIGRATION_STATES.CONVERTED
|
|
||||||
: FILTER_BOX_MIGRATION_STATES.NOOP,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set filterbox migration state if has filter_box in the dash:
|
|
||||||
if (hasFilterBox) {
|
|
||||||
if (isMigrationEnabled) {
|
|
||||||
// has url param?
|
|
||||||
if (
|
|
||||||
migrationStateParam &&
|
|
||||||
Object.values(FILTER_BOX_MIGRATION_STATES).includes(
|
|
||||||
migrationStateParam,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setFilterboxMigrationState(migrationStateParam);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// has cookie?
|
|
||||||
const snoozeDash = getItem(
|
|
||||||
LocalStorageKeys.filter_box_transition_snoozed_at,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
Date.now() - (snoozeDash[id] || 0) <
|
|
||||||
FILTER_BOX_TRANSITION_SNOOZE_DURATION
|
|
||||||
) {
|
|
||||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.SNOOZED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.UNDECIDED);
|
|
||||||
} else if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) {
|
|
||||||
dispatch(
|
|
||||||
addWarningToast(
|
|
||||||
t(
|
|
||||||
'filter_box will be deprecated ' +
|
|
||||||
'in a future version of Superset. ' +
|
|
||||||
'Please replace filter_box by dashboard filter components.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [readyToRender]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
async function getDataMaskApplied() {
|
async function getDataMaskApplied() {
|
||||||
|
|
@ -308,7 +219,6 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
dashboard,
|
dashboard,
|
||||||
charts,
|
charts,
|
||||||
activeTabs,
|
activeTabs,
|
||||||
filterboxMigrationState,
|
|
||||||
dataMask,
|
dataMask,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -317,7 +227,7 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
}
|
}
|
||||||
if (id) getDataMaskApplied();
|
if (id) getDataMaskApplied();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [readyToRender, filterboxMigrationState]);
|
}, [readyToRender]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dashboard_title) {
|
if (dashboard_title) {
|
||||||
|
|
@ -364,37 +274,9 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||||
if (!readyToRender) return <Loading />;
|
if (!readyToRender) return <Loading />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardPageIdContext.Provider value={dashboardPageId}>
|
||||||
<Global styles={[filterCardPopoverStyle(theme), headerStyles(theme)]} />
|
<DashboardContainer />
|
||||||
<FilterBoxMigrationModal
|
</DashboardPageIdContext.Provider>
|
||||||
show={filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.UNDECIDED}
|
|
||||||
hideFooter={!isMigrationEnabled}
|
|
||||||
onHide={() => {
|
|
||||||
// cancel button: only snooze this visit
|
|
||||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.SNOOZED);
|
|
||||||
}}
|
|
||||||
onClickReview={() => {
|
|
||||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.REVIEWING);
|
|
||||||
}}
|
|
||||||
onClickSnooze={() => {
|
|
||||||
const snoozedDash = getItem(
|
|
||||||
LocalStorageKeys.filter_box_transition_snoozed_at,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
setItem(LocalStorageKeys.filter_box_transition_snoozed_at, {
|
|
||||||
...snoozedDash,
|
|
||||||
[id]: Date.now(),
|
|
||||||
});
|
|
||||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.SNOOZED);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MigrationContext.Provider value={filterboxMigrationState}>
|
|
||||||
<DashboardPageIdContext.Provider value={dashboardPageId}>
|
|
||||||
<DashboardContainer />
|
|
||||||
</DashboardPageIdContext.Provider>
|
|
||||||
</MigrationContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ function mapStateToProps(
|
||||||
errorMessage: sliceEntities.errorMessage,
|
errorMessage: sliceEntities.errorMessage,
|
||||||
lastUpdated: sliceEntities.lastUpdated,
|
lastUpdated: sliceEntities.lastUpdated,
|
||||||
editMode: dashboardState.editMode,
|
editMode: dashboardState.editMode,
|
||||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import getNativeFilterConfig from './filterboxMigrationHelper';
|
|
||||||
|
|
||||||
const regionFilter = {
|
|
||||||
cache_timeout: null,
|
|
||||||
changed_on: '2021-10-07 11:57:48.355047',
|
|
||||||
description: null,
|
|
||||||
description_markeddown: '',
|
|
||||||
form_data: {
|
|
||||||
compare_lag: '10',
|
|
||||||
compare_suffix: 'o10Y',
|
|
||||||
country_fieldtype: 'cca3',
|
|
||||||
datasource: '1__table',
|
|
||||||
date_filter: false,
|
|
||||||
entity: 'country_code',
|
|
||||||
filter_configs: [
|
|
||||||
{
|
|
||||||
asc: false,
|
|
||||||
clearable: true,
|
|
||||||
column: 'region',
|
|
||||||
key: '2s98dfu',
|
|
||||||
metric: 'sum__SP_POP_TOTL',
|
|
||||||
multiple: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
asc: false,
|
|
||||||
clearable: true,
|
|
||||||
column: 'country_name',
|
|
||||||
key: 'li3j2lk',
|
|
||||||
metric: 'sum__SP_POP_TOTL',
|
|
||||||
multiple: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
granularity_sqla: 'year',
|
|
||||||
groupby: [],
|
|
||||||
limit: '25',
|
|
||||||
markup_type: 'markdown',
|
|
||||||
row_limit: 50000,
|
|
||||||
show_bubbles: true,
|
|
||||||
slice_id: 32,
|
|
||||||
time_range: '2014-01-01 : 2014-01-02',
|
|
||||||
viz_type: 'filter_box',
|
|
||||||
},
|
|
||||||
modified: '<bound method AuditMixinNullable.modified of Region Filter>',
|
|
||||||
slice_name: 'Region Filter',
|
|
||||||
slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%2032%7D',
|
|
||||||
slice_id: 32,
|
|
||||||
};
|
|
||||||
const chart1 = {
|
|
||||||
cache_timeout: null,
|
|
||||||
changed_on: '2021-09-07 18:05:18.896212',
|
|
||||||
description: null,
|
|
||||||
description_markeddown: '',
|
|
||||||
form_data: {
|
|
||||||
compare_lag: '10',
|
|
||||||
compare_suffix: 'over 10Y',
|
|
||||||
country_fieldtype: 'cca3',
|
|
||||||
datasource: '1__table',
|
|
||||||
entity: 'country_code',
|
|
||||||
granularity_sqla: 'year',
|
|
||||||
groupby: [],
|
|
||||||
limit: '25',
|
|
||||||
markup_type: 'markdown',
|
|
||||||
metric: 'sum__SP_POP_TOTL',
|
|
||||||
row_limit: 50000,
|
|
||||||
show_bubbles: true,
|
|
||||||
slice_id: 33,
|
|
||||||
time_range: '2000 : 2014-01-02',
|
|
||||||
viz_type: 'big_number',
|
|
||||||
},
|
|
||||||
modified: "<bound method AuditMixinNullable.modified of World's Population>",
|
|
||||||
slice_name: "World's Population",
|
|
||||||
slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%2033%7D',
|
|
||||||
slice_id: 33,
|
|
||||||
};
|
|
||||||
const chartData = [regionFilter, chart1];
|
|
||||||
const preselectedFilters = {
|
|
||||||
'32': {
|
|
||||||
region: ['East Asia & Pacific'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
test('should convert filter_box config to dashboard native filter config', () => {
|
|
||||||
const filterConfig = getNativeFilterConfig(chartData, {}, {});
|
|
||||||
// convert to 2 components
|
|
||||||
expect(filterConfig.length).toEqual(2);
|
|
||||||
|
|
||||||
expect(filterConfig[0].id).toBeDefined();
|
|
||||||
expect(filterConfig[0].filterType).toBe('filter_select');
|
|
||||||
expect(filterConfig[0].name).toBe('region');
|
|
||||||
expect(filterConfig[0].targets).toEqual([
|
|
||||||
{ column: { name: 'region' }, datasetId: 1 },
|
|
||||||
]);
|
|
||||||
expect(filterConfig[0].scope).toEqual({
|
|
||||||
excluded: [],
|
|
||||||
rootPath: ['ROOT_ID'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(filterConfig[1].id).toBeDefined();
|
|
||||||
expect(filterConfig[1].filterType).toBe('filter_select');
|
|
||||||
expect(filterConfig[1].name).toBe('country_name');
|
|
||||||
expect(filterConfig[1].targets).toEqual([
|
|
||||||
{ column: { name: 'country_name' }, datasetId: 1 },
|
|
||||||
]);
|
|
||||||
expect(filterConfig[1].scope).toEqual({
|
|
||||||
excluded: [],
|
|
||||||
rootPath: ['ROOT_ID'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should convert preselected filters', () => {
|
|
||||||
const filterConfig = getNativeFilterConfig(chartData, {}, preselectedFilters);
|
|
||||||
const { defaultDataMask } = filterConfig[0];
|
|
||||||
expect(defaultDataMask.filterState).toEqual({
|
|
||||||
value: ['East Asia & Pacific'],
|
|
||||||
});
|
|
||||||
expect(defaultDataMask.extraFormData?.filters).toEqual([
|
|
||||||
{
|
|
||||||
col: 'region',
|
|
||||||
op: 'IN',
|
|
||||||
val: ['East Asia & Pacific'],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import shortid from 'shortid';
|
|
||||||
import { find, isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FILTER_CONFIG_ATTRIBUTES,
|
|
||||||
TIME_FILTER_LABELS,
|
|
||||||
TIME_FILTER_MAP,
|
|
||||||
} from 'src/explore/constants';
|
|
||||||
import { DASHBOARD_FILTER_SCOPE_GLOBAL } from 'src/dashboard/reducers/dashboardFilters';
|
|
||||||
import { Filter, NativeFilterType, TimeGranularity } from '@superset-ui/core';
|
|
||||||
import { getChartIdsInFilterBoxScope } from './activeDashboardFilters';
|
|
||||||
import getFilterConfigsFromFormdata from './getFilterConfigsFromFormdata';
|
|
||||||
|
|
||||||
interface FilterConfig {
|
|
||||||
asc: boolean;
|
|
||||||
clearable: boolean;
|
|
||||||
column: string;
|
|
||||||
defaultValue?: any;
|
|
||||||
key: string;
|
|
||||||
label?: string;
|
|
||||||
metric: string;
|
|
||||||
multiple: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SliceData {
|
|
||||||
slice_id: number;
|
|
||||||
form_data: {
|
|
||||||
adhoc_filters?: [];
|
|
||||||
datasource: string;
|
|
||||||
date_filter?: boolean;
|
|
||||||
filter_configs?: FilterConfig[];
|
|
||||||
granularity?: string;
|
|
||||||
granularity_sqla?: string;
|
|
||||||
time_grain_sqla?: string;
|
|
||||||
time_range?: string;
|
|
||||||
show_sqla_time_column?: boolean;
|
|
||||||
show_sqla_time_granularity?: boolean;
|
|
||||||
viz_type: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterScopeType {
|
|
||||||
scope: string[];
|
|
||||||
immune: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterScopesMetadata {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: FilterScopeType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PreselectedFilterColumn {
|
|
||||||
[key: string]: boolean | string | number | string[] | number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PreselectedFiltersMetadata {
|
|
||||||
[key: string]: PreselectedFilterColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterBoxToFilterComponentMap {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterBoxDependencyMap {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: number[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FILTER_COMPONENT_FILTER_TYPES {
|
|
||||||
FILTER_TIME = 'filter_time',
|
|
||||||
FILTER_TIMEGRAIN = 'filter_timegrain',
|
|
||||||
FILTER_TIMECOLUMN = 'filter_timecolumn',
|
|
||||||
FILTER_SELECT = 'filter_select',
|
|
||||||
FILTER_RANGE = 'filter_range',
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPreselectedValuesFromDashboard =
|
|
||||||
(preselectedFilters: PreselectedFiltersMetadata) =>
|
|
||||||
(filterKey: string, column: string) => {
|
|
||||||
if (preselectedFilters[filterKey]?.[column]) {
|
|
||||||
// overwrite default values by dashboard default_filters
|
|
||||||
return preselectedFilters[filterKey][column];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilterBoxDefaultValues = (config: FilterConfig) => {
|
|
||||||
let defaultValues = config[FILTER_CONFIG_ATTRIBUTES.DEFAULT_VALUE];
|
|
||||||
|
|
||||||
// treat empty string as null (no default value)
|
|
||||||
if (defaultValues === '') {
|
|
||||||
defaultValues = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultValue could be ; separated values,
|
|
||||||
// could be null or ''
|
|
||||||
if (defaultValues && config[FILTER_CONFIG_ATTRIBUTES.MULTIPLE]) {
|
|
||||||
defaultValues = config.defaultValue.split(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setValuesInArray = (value1: any, value2: any) => {
|
|
||||||
if (!isEmpty(value1)) {
|
|
||||||
return [value1];
|
|
||||||
}
|
|
||||||
if (!isEmpty(value2)) {
|
|
||||||
return [value2];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilterboxDependencies = (filterScopes: FilterScopesMetadata) => {
|
|
||||||
const filterFieldsDependencies: FilterBoxDependencyMap = {};
|
|
||||||
const filterChartIds: number[] = Object.keys(filterScopes).map(key =>
|
|
||||||
parseInt(key, 10),
|
|
||||||
);
|
|
||||||
Object.entries(filterScopes).forEach(([key, filterFields]) => {
|
|
||||||
filterFieldsDependencies[key] = {};
|
|
||||||
Object.entries(filterFields).forEach(([filterField, filterScope]) => {
|
|
||||||
filterFieldsDependencies[key][filterField] = getChartIdsInFilterBoxScope({
|
|
||||||
filterScope,
|
|
||||||
}).filter(
|
|
||||||
chartId => filterChartIds.includes(chartId) && String(chartId) !== key,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return filterFieldsDependencies;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function getNativeFilterConfig(
|
|
||||||
chartData: SliceData[] = [],
|
|
||||||
filterScopes: FilterScopesMetadata = {},
|
|
||||||
preselectFilters: PreselectedFiltersMetadata = {},
|
|
||||||
): Filter[] {
|
|
||||||
const filterConfig: Filter[] = [];
|
|
||||||
const filterBoxToFilterComponentMap: FilterBoxToFilterComponentMap = {};
|
|
||||||
|
|
||||||
chartData.forEach(slice => {
|
|
||||||
const key = String(slice.slice_id);
|
|
||||||
|
|
||||||
if (slice.form_data.viz_type === 'filter_box') {
|
|
||||||
filterBoxToFilterComponentMap[key] = {};
|
|
||||||
const configs = getFilterConfigsFromFormdata(slice.form_data);
|
|
||||||
let { columns } = configs;
|
|
||||||
if (preselectFilters[key]) {
|
|
||||||
Object.keys(columns).forEach(col => {
|
|
||||||
if (preselectFilters[key][col]) {
|
|
||||||
columns = {
|
|
||||||
...columns,
|
|
||||||
[col]: preselectFilters[key][col],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopesByChartId = Object.keys(columns).reduce((map, column) => {
|
|
||||||
const scopeSettings = {
|
|
||||||
...filterScopes[key],
|
|
||||||
};
|
|
||||||
const { scope, immune }: FilterScopeType = {
|
|
||||||
...DASHBOARD_FILTER_SCOPE_GLOBAL,
|
|
||||||
...scopeSettings[column],
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...map,
|
|
||||||
[column]: {
|
|
||||||
scope,
|
|
||||||
immune,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const {
|
|
||||||
adhoc_filters = [],
|
|
||||||
datasource = '',
|
|
||||||
date_filter = false,
|
|
||||||
filter_configs = [],
|
|
||||||
granularity_sqla,
|
|
||||||
show_sqla_time_column = false,
|
|
||||||
show_sqla_time_granularity = false,
|
|
||||||
time_grain_sqla,
|
|
||||||
time_range,
|
|
||||||
} = slice.form_data;
|
|
||||||
|
|
||||||
const getDashboardDefaultValues =
|
|
||||||
getPreselectedValuesFromDashboard(preselectFilters);
|
|
||||||
|
|
||||||
if (date_filter) {
|
|
||||||
const { scope, immune }: FilterScopeType =
|
|
||||||
scopesByChartId[TIME_FILTER_MAP.time_range] ||
|
|
||||||
DASHBOARD_FILTER_SCOPE_GLOBAL;
|
|
||||||
const timeRangeFilter: Filter = {
|
|
||||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
|
||||||
description: 'time range filter',
|
|
||||||
controlValues: {},
|
|
||||||
name: TIME_FILTER_LABELS.time_range,
|
|
||||||
filterType: FILTER_COMPONENT_FILTER_TYPES.FILTER_TIME,
|
|
||||||
targets: [{}],
|
|
||||||
cascadeParentIds: [],
|
|
||||||
defaultDataMask: {},
|
|
||||||
type: NativeFilterType.NATIVE_FILTER,
|
|
||||||
scope: {
|
|
||||||
rootPath: scope,
|
|
||||||
excluded: immune,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
filterBoxToFilterComponentMap[key][TIME_FILTER_MAP.time_range] =
|
|
||||||
timeRangeFilter.id;
|
|
||||||
const dashboardDefaultValues =
|
|
||||||
getDashboardDefaultValues(key, TIME_FILTER_MAP.time_range) ||
|
|
||||||
time_range;
|
|
||||||
if (!isEmpty(dashboardDefaultValues)) {
|
|
||||||
timeRangeFilter.defaultDataMask = {
|
|
||||||
extraFormData: { time_range: dashboardDefaultValues as string },
|
|
||||||
filterState: { value: dashboardDefaultValues },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
filterConfig.push(timeRangeFilter);
|
|
||||||
|
|
||||||
if (show_sqla_time_granularity) {
|
|
||||||
const { scope, immune }: FilterScopeType =
|
|
||||||
scopesByChartId[TIME_FILTER_MAP.time_grain_sqla] ||
|
|
||||||
DASHBOARD_FILTER_SCOPE_GLOBAL;
|
|
||||||
const timeGrainFilter: Filter = {
|
|
||||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
|
||||||
controlValues: {},
|
|
||||||
description: 'time grain filter',
|
|
||||||
name: TIME_FILTER_LABELS.time_grain_sqla,
|
|
||||||
filterType: FILTER_COMPONENT_FILTER_TYPES.FILTER_TIMEGRAIN,
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
datasetId: parseInt(datasource.split('__')[0], 10),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
cascadeParentIds: [],
|
|
||||||
defaultDataMask: {},
|
|
||||||
type: NativeFilterType.NATIVE_FILTER,
|
|
||||||
scope: {
|
|
||||||
rootPath: scope,
|
|
||||||
excluded: immune,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
filterBoxToFilterComponentMap[key][TIME_FILTER_MAP.time_grain_sqla] =
|
|
||||||
timeGrainFilter.id;
|
|
||||||
const dashboardDefaultValues = getDashboardDefaultValues(
|
|
||||||
key,
|
|
||||||
TIME_FILTER_MAP.time_grain_sqla,
|
|
||||||
);
|
|
||||||
if (!isEmpty(dashboardDefaultValues)) {
|
|
||||||
timeGrainFilter.defaultDataMask = {
|
|
||||||
extraFormData: {
|
|
||||||
time_grain_sqla: (dashboardDefaultValues ||
|
|
||||||
time_grain_sqla) as TimeGranularity,
|
|
||||||
},
|
|
||||||
filterState: {
|
|
||||||
value: setValuesInArray(
|
|
||||||
dashboardDefaultValues,
|
|
||||||
time_grain_sqla,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
filterConfig.push(timeGrainFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show_sqla_time_column) {
|
|
||||||
const { scope, immune }: FilterScopeType =
|
|
||||||
scopesByChartId[TIME_FILTER_MAP.granularity_sqla] ||
|
|
||||||
DASHBOARD_FILTER_SCOPE_GLOBAL;
|
|
||||||
const timeColumnFilter: Filter = {
|
|
||||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
|
||||||
description: 'time column filter',
|
|
||||||
controlValues: {},
|
|
||||||
name: TIME_FILTER_LABELS.granularity_sqla,
|
|
||||||
filterType: FILTER_COMPONENT_FILTER_TYPES.FILTER_TIMECOLUMN,
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
datasetId: parseInt(datasource.split('__')[0], 10),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
cascadeParentIds: [],
|
|
||||||
defaultDataMask: {},
|
|
||||||
type: NativeFilterType.NATIVE_FILTER,
|
|
||||||
scope: {
|
|
||||||
rootPath: scope,
|
|
||||||
excluded: immune,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
filterBoxToFilterComponentMap[key][TIME_FILTER_MAP.granularity_sqla] =
|
|
||||||
timeColumnFilter.id;
|
|
||||||
const dashboardDefaultValues = getDashboardDefaultValues(
|
|
||||||
key,
|
|
||||||
TIME_FILTER_MAP.granularity_sqla,
|
|
||||||
);
|
|
||||||
if (!isEmpty(dashboardDefaultValues)) {
|
|
||||||
timeColumnFilter.defaultDataMask = {
|
|
||||||
extraFormData: {
|
|
||||||
granularity_sqla: (dashboardDefaultValues ||
|
|
||||||
granularity_sqla) as string,
|
|
||||||
},
|
|
||||||
filterState: {
|
|
||||||
value: setValuesInArray(
|
|
||||||
dashboardDefaultValues,
|
|
||||||
granularity_sqla,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
filterConfig.push(timeColumnFilter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filter_configs.forEach(config => {
|
|
||||||
const { scope, immune }: FilterScopeType =
|
|
||||||
scopesByChartId[config.column] || DASHBOARD_FILTER_SCOPE_GLOBAL;
|
|
||||||
const entry: Filter = {
|
|
||||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
|
||||||
description: '',
|
|
||||||
controlValues: {
|
|
||||||
enableEmptyFilter: !config[FILTER_CONFIG_ATTRIBUTES.CLEARABLE],
|
|
||||||
defaultToFirstItem: false,
|
|
||||||
inverseSelection: false,
|
|
||||||
multiSelect: config[FILTER_CONFIG_ATTRIBUTES.MULTIPLE],
|
|
||||||
sortAscending: config[FILTER_CONFIG_ATTRIBUTES.SORT_ASCENDING],
|
|
||||||
},
|
|
||||||
name: config.label || config.column,
|
|
||||||
filterType: FILTER_COMPONENT_FILTER_TYPES.FILTER_SELECT,
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
datasetId: parseInt(datasource.split('__')[0], 10),
|
|
||||||
column: {
|
|
||||||
name: config.column,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
cascadeParentIds: [],
|
|
||||||
defaultDataMask: {},
|
|
||||||
type: NativeFilterType.NATIVE_FILTER,
|
|
||||||
scope: {
|
|
||||||
rootPath: scope,
|
|
||||||
excluded: immune,
|
|
||||||
},
|
|
||||||
adhoc_filters,
|
|
||||||
sortMetric: config[FILTER_CONFIG_ATTRIBUTES.SORT_METRIC],
|
|
||||||
time_range,
|
|
||||||
};
|
|
||||||
filterBoxToFilterComponentMap[key][config.column] = entry.id;
|
|
||||||
const defaultValues =
|
|
||||||
getDashboardDefaultValues(key, config.column) ||
|
|
||||||
getFilterBoxDefaultValues(config);
|
|
||||||
if (!isEmpty(defaultValues)) {
|
|
||||||
entry.defaultDataMask = {
|
|
||||||
extraFormData: {
|
|
||||||
filters: [{ col: config.column, op: 'IN', val: defaultValues }],
|
|
||||||
},
|
|
||||||
filterState: { value: defaultValues },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
filterConfig.push(entry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const dependencies: FilterBoxDependencyMap =
|
|
||||||
getFilterboxDependencies(filterScopes);
|
|
||||||
Object.entries(dependencies).forEach(([key, filterFields]) => {
|
|
||||||
Object.entries(filterFields).forEach(([field, childrenChartIds]) => {
|
|
||||||
const parentComponentId = filterBoxToFilterComponentMap[key][field];
|
|
||||||
childrenChartIds.forEach(childrenChartId => {
|
|
||||||
const childComponentIds = Object.values(
|
|
||||||
filterBoxToFilterComponentMap[childrenChartId],
|
|
||||||
);
|
|
||||||
childComponentIds.forEach(childComponentId => {
|
|
||||||
const childComponent = find(
|
|
||||||
filterConfig,
|
|
||||||
({ id }) => id === childComponentId,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
childComponent &&
|
|
||||||
// time related filter components don't have parent
|
|
||||||
[
|
|
||||||
FILTER_COMPONENT_FILTER_TYPES.FILTER_SELECT,
|
|
||||||
FILTER_COMPONENT_FILTER_TYPES.FILTER_RANGE,
|
|
||||||
].includes(
|
|
||||||
childComponent.filterType as FILTER_COMPONENT_FILTER_TYPES,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
childComponent.cascadeParentIds =
|
|
||||||
childComponent.cascadeParentIds || [];
|
|
||||||
childComponent.cascadeParentIds.push(parentComponentId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return filterConfig;
|
|
||||||
}
|
|
||||||
|
|
@ -156,16 +156,6 @@ export const TIME_FILTER_MAP = {
|
||||||
granularity: '__granularity',
|
granularity: '__granularity',
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum FILTER_BOX_MIGRATION_STATES {
|
|
||||||
CONVERTED = 'CONVERTED',
|
|
||||||
NOOP = 'NOOP',
|
|
||||||
REVIEWING = 'REVIEWING',
|
|
||||||
SNOOZED = 'SNOOZED',
|
|
||||||
UNDECIDED = 'UNDECIDED',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FILTER_BOX_TRANSITION_SNOOZE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
|
||||||
|
|
||||||
export const POPOVER_INITIAL_HEIGHT = 240;
|
export const POPOVER_INITIAL_HEIGHT = 240;
|
||||||
export const POPOVER_INITIAL_WIDTH = 320;
|
export const POPOVER_INITIAL_WIDTH = 320;
|
||||||
export const UNRESIZABLE_POPOVER_WIDTH = 296;
|
export const UNRESIZABLE_POPOVER_WIDTH = 296;
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,6 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
||||||
"ALERT_REPORTS": False,
|
"ALERT_REPORTS": False,
|
||||||
"DASHBOARD_RBAC": False,
|
"DASHBOARD_RBAC": False,
|
||||||
"ENABLE_EXPLORE_DRAG_AND_DROP": True,
|
"ENABLE_EXPLORE_DRAG_AND_DROP": True,
|
||||||
"ENABLE_FILTER_BOX_MIGRATION": False,
|
|
||||||
"ENABLE_ADVANCED_DATA_TYPES": False,
|
"ENABLE_ADVANCED_DATA_TYPES": False,
|
||||||
"ENABLE_DND_WITH_CLICK_UX": True,
|
"ENABLE_DND_WITH_CLICK_UX": True,
|
||||||
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
|
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
|
||||||
|
|
|
||||||
|
|
@ -114,17 +114,9 @@ class Dashboard(BaseSupersetView):
|
||||||
@expose("/new/")
|
@expose("/new/")
|
||||||
def new(self) -> FlaskResponse: # pylint: disable=no-self-use
|
def new(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||||
"""Creates a new, blank dashboard and redirects to it in edit mode"""
|
"""Creates a new, blank dashboard and redirects to it in edit mode"""
|
||||||
metadata = {}
|
|
||||||
if is_feature_enabled("ENABLE_FILTER_BOX_MIGRATION"):
|
|
||||||
metadata = {
|
|
||||||
"native_filter_configuration": [],
|
|
||||||
"show_native_filters": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
new_dashboard = DashboardModel(
|
new_dashboard = DashboardModel(
|
||||||
dashboard_title="[ untitled dashboard ]",
|
dashboard_title="[ untitled dashboard ]",
|
||||||
owners=[g.user],
|
owners=[g.user],
|
||||||
json_metadata=json.dumps(metadata, sort_keys=True),
|
|
||||||
)
|
)
|
||||||
db.session.add(new_dashboard)
|
db.session.add(new_dashboard)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue