feat(native_filter_migration): add transition mode (#16992)
* feat: [Migrate filter_box to filter component] add transition mode * rebase and fix comments * rebase and fix commnent -- patch 2
This commit is contained in:
parent
6b1de57207
commit
7d22c9ce17
|
|
@ -52,6 +52,7 @@ const propTypes = {
|
|||
vizType: PropTypes.string.isRequired,
|
||||
triggerRender: PropTypes.bool,
|
||||
isFiltersInitialized: PropTypes.bool,
|
||||
isDeactivatedViz: PropTypes.bool,
|
||||
// state
|
||||
chartAlert: PropTypes.string,
|
||||
chartStatus: PropTypes.string,
|
||||
|
|
@ -82,6 +83,7 @@ const defaultProps = {
|
|||
triggerRender: false,
|
||||
dashboardId: null,
|
||||
chartStackTrace: null,
|
||||
isDeactivatedViz: false,
|
||||
};
|
||||
|
||||
const Styles = styled.div`
|
||||
|
|
@ -114,13 +116,25 @@ class Chart extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.triggerQuery) {
|
||||
// during migration, hold chart queries before user choose review or cancel
|
||||
if (
|
||||
this.props.triggerQuery &&
|
||||
this.props.filterboxMigrationState !== 'UNDECIDED'
|
||||
) {
|
||||
this.runQuery();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.triggerQuery) {
|
||||
// during migration, hold chart queries before user choose review or cancel
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -221,6 +235,8 @@ class Chart extends React.PureComponent {
|
|||
onQuery,
|
||||
refreshOverlayVisible,
|
||||
queriesResponse = [],
|
||||
isDeactivatedViz = false,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
const isLoading = chartStatus === 'loading';
|
||||
|
|
@ -250,6 +266,7 @@ class Chart extends React.PureComponent {
|
|||
className="chart-container"
|
||||
data-test="chart-container"
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
<div
|
||||
className={`slice_container ${isFaded ? ' faded' : ''}`}
|
||||
|
|
@ -266,7 +283,7 @@ class Chart extends React.PureComponent {
|
|||
</RefreshOverlayWrapper>
|
||||
)}
|
||||
|
||||
{isLoading && <Loading />}
|
||||
{isLoading && !isDeactivatedViz && <Loading />}
|
||||
</Styles>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -153,10 +153,18 @@ export function useTransformedResource<IN, OUT>(
|
|||
// While incomplete, there is no result - no need to transform.
|
||||
return resource;
|
||||
}
|
||||
return {
|
||||
...resource,
|
||||
result: transformFn(resource.result),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
...resource,
|
||||
result: transformFn(resource.result),
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
status: ResourceStatus.ERROR,
|
||||
result: null,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
}, [resource, transformFn]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export const useDashboard = (idOrSlug: string | number) =>
|
|||
useApiV1Resource<Dashboard>(`/api/v1/dashboard/${idOrSlug}`),
|
||||
dashboard => ({
|
||||
...dashboard,
|
||||
metadata: dashboard.json_metadata && JSON.parse(dashboard.json_metadata),
|
||||
metadata:
|
||||
(dashboard.json_metadata && JSON.parse(dashboard.json_metadata)) || {},
|
||||
position_data:
|
||||
dashboard.position_json && JSON.parse(dashboard.position_json),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export const BOOL_TRUE_DISPLAY = 'True';
|
|||
export const BOOL_FALSE_DISPLAY = 'False';
|
||||
|
||||
export const URL_PARAMS = {
|
||||
migrationState: {
|
||||
name: 'migration_state',
|
||||
type: 'string',
|
||||
},
|
||||
standalone: {
|
||||
name: 'standalone',
|
||||
type: 'number',
|
||||
|
|
|
|||
|
|
@ -55,17 +55,21 @@ import newComponentFactory from 'src/dashboard/util/newComponentFactory';
|
|||
import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
|
||||
import extractUrlParams from '../util/extractUrlParams';
|
||||
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
|
||||
|
||||
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||
|
||||
export const hydrateDashboard = (dashboardData, chartData) => (
|
||||
dispatch,
|
||||
getState,
|
||||
) => {
|
||||
export const hydrateDashboard = (
|
||||
dashboardData,
|
||||
chartData,
|
||||
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
) => (dispatch, getState) => {
|
||||
const { user, common } = getState();
|
||||
let { metadata } = dashboardData;
|
||||
|
||||
const { metadata } = dashboardData;
|
||||
const regularUrlParams = extractUrlParams('regular');
|
||||
const reservedUrlParams = extractUrlParams('reserved');
|
||||
const editMode = reservedUrlParams.edit === 'true';
|
||||
|
|
@ -227,19 +231,25 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
|||
const componentId = chartIdToLayoutId[key];
|
||||
const directPathToFilter = (layout[componentId].parents || []).slice();
|
||||
directPathToFilter.push(componentId);
|
||||
dashboardFilters[key] = {
|
||||
...dashboardFilter,
|
||||
chartId: key,
|
||||
componentId,
|
||||
datasourceId: slice.form_data.datasource,
|
||||
filterName: slice.slice_name,
|
||||
directPathToFilter,
|
||||
columns,
|
||||
labels,
|
||||
scopes: scopesByChartId,
|
||||
isInstantFilter: !!slice.form_data.instant_filtering,
|
||||
isDateFilter: Object.keys(columns).includes(TIME_RANGE),
|
||||
};
|
||||
if (
|
||||
[
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||
].includes(filterboxMigrationState)
|
||||
) {
|
||||
dashboardFilters[key] = {
|
||||
...dashboardFilter,
|
||||
chartId: key,
|
||||
componentId,
|
||||
datasourceId: slice.form_data.datasource,
|
||||
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
|
||||
|
|
@ -278,17 +288,28 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
|||
directPathToChild.push(directLinkComponentId);
|
||||
}
|
||||
|
||||
const nativeFilters = getInitialNativeFilterState({
|
||||
filterConfig: metadata?.native_filter_configuration || [],
|
||||
});
|
||||
|
||||
if (!metadata) {
|
||||
metadata = {};
|
||||
// should convert filter_box to filter component?
|
||||
let filterConfig = metadata?.native_filter_configuration || [];
|
||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||
filterConfig = getNativeFilterConfig(
|
||||
chartData,
|
||||
filterScopes,
|
||||
preselectFilters,
|
||||
);
|
||||
metadata.native_filter_configuration = filterConfig;
|
||||
metadata.show_native_filters = true;
|
||||
}
|
||||
|
||||
const nativeFilters = getInitialNativeFilterState({
|
||||
filterConfig,
|
||||
});
|
||||
metadata.show_native_filters =
|
||||
dashboardData?.metadata?.show_native_filters ??
|
||||
isFeatureEnabled(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 user just added cross filter to dashboard it's not saving it scope on server,
|
||||
|
|
@ -379,6 +400,7 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
|||
lastModifiedTime: dashboardData.changed_on,
|
||||
isRefreshing: false,
|
||||
activeTabs: [],
|
||||
filterboxMigrationState,
|
||||
},
|
||||
dashboardLayout,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function fetchAllSlicesFailed(error) {
|
|||
}
|
||||
|
||||
const FETCH_SLICES_PAGE_SIZE = 200;
|
||||
export function fetchAllSlices(userId) {
|
||||
export function fetchAllSlices(userId, excludeFilterBox = false) {
|
||||
return (dispatch, getState) => {
|
||||
const { sliceEntities } = getState();
|
||||
if (sliceEntities.lastUpdated === 0) {
|
||||
|
|
@ -71,7 +71,12 @@ export function fetchAllSlices(userId) {
|
|||
})
|
||||
.then(({ json }) => {
|
||||
const slices = {};
|
||||
json.result.forEach(slice => {
|
||||
let { result } = json;
|
||||
// disable add filter_box viz to dashboard
|
||||
if (excludeFilterBox) {
|
||||
result = result.filter(slice => slice.viz_type !== 'filter_box');
|
||||
}
|
||||
result.forEach(slice => {
|
||||
let form_data = JSON.parse(slice.params);
|
||||
form_data = {
|
||||
...form_data,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@
|
|||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState, useContext } from 'react';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
import { MigrationContext } from 'src/dashboard/containers/DashboardPage';
|
||||
import {
|
||||
useFilters,
|
||||
useNativeFiltersDataMask,
|
||||
|
|
@ -30,6 +31,7 @@ import { Filter } from '../nativeFilters/types';
|
|||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useNativeFilters = () => {
|
||||
const filterboxMigrationState = useContext(MigrationContext);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
|
||||
getUrlParam(URL_PARAMS.showFilters) ?? true,
|
||||
|
|
@ -74,12 +76,14 @@ export const useNativeFilters = () => {
|
|||
useEffect(() => {
|
||||
if (
|
||||
filterValues.length === 0 &&
|
||||
dashboardFiltersOpen &&
|
||||
nativeFiltersEnabled
|
||||
nativeFiltersEnabled &&
|
||||
['CONVERTED', 'REVIEWING', 'NOOP'].includes(filterboxMigrationState)
|
||||
) {
|
||||
toggleDashboardFiltersOpen(false);
|
||||
} else {
|
||||
toggleDashboardFiltersOpen(true);
|
||||
}
|
||||
}, [filterValues.length]);
|
||||
}, [filterValues.length, filterboxMigrationState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showDashboard) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
|
||||
import Modal from 'src/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
const StyledFilterBoxMigrationModal = styled(Modal)`
|
||||
.modal-content {
|
||||
height: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
interface FilterBoxMigrationModalProps {
|
||||
onHide: () => void;
|
||||
onClickReview: () => void;
|
||||
onClickSnooze: () => void;
|
||||
show: boolean;
|
||||
hideFooter: boolean;
|
||||
}
|
||||
|
||||
const FilterBoxMigrationModal: FunctionComponent<FilterBoxMigrationModalProps> = ({
|
||||
onClickReview,
|
||||
onClickSnooze,
|
||||
onHide,
|
||||
show,
|
||||
hideFooter = false,
|
||||
}) => (
|
||||
<StyledFilterBoxMigrationModal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
title={t('Ready to review filters in this dashboard?')}
|
||||
hideFooter={hideFooter}
|
||||
footer={
|
||||
<>
|
||||
<Button buttonSize="small" onClick={onClickSnooze}>
|
||||
{t('Remind me in 24 hours')}
|
||||
</Button>
|
||||
<Button buttonSize="small" onClick={onHide}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
onClick={onClickReview}
|
||||
>
|
||||
{t('Start Review')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
responsive
|
||||
>
|
||||
<div>
|
||||
{t(
|
||||
'filter_box will be deprecated ' +
|
||||
'in a future version of Superset. ' +
|
||||
'Please replace filter_box by dashboard filter components.',
|
||||
)}
|
||||
</div>
|
||||
</StyledFilterBoxMigrationModal>
|
||||
);
|
||||
|
||||
export default FilterBoxMigrationModal;
|
||||
|
|
@ -35,6 +35,7 @@ import downloadAsImage from 'src/utils/downloadAsImage';
|
|||
import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
|
||||
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
|
||||
const propTypes = {
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
|
|
@ -65,6 +66,7 @@ const propTypes = {
|
|||
refreshLimit: PropTypes.number,
|
||||
refreshWarning: PropTypes.string,
|
||||
lastModifiedTime: PropTypes.number.isRequired,
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
|
@ -72,6 +74,7 @@ const defaultProps = {
|
|||
colorScheme: undefined,
|
||||
refreshLimit: 0,
|
||||
refreshWarning: null,
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
};
|
||||
|
||||
const MENU_KEYS = {
|
||||
|
|
@ -209,6 +212,7 @@ class HeaderActionsDropdown extends React.PureComponent {
|
|||
lastModifiedTime,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
filterboxMigrationState,
|
||||
} = this.props;
|
||||
|
||||
const emailTitle = t('Superset dashboard');
|
||||
|
|
@ -283,14 +287,15 @@ class HeaderActionsDropdown extends React.PureComponent {
|
|||
/>
|
||||
</Menu.Item>
|
||||
|
||||
{editMode && (
|
||||
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
||||
<FilterScopeModal
|
||||
className="m-r-5"
|
||||
triggerNode={t('Set filter mapping')}
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{editMode &&
|
||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.CONVERTED && (
|
||||
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
||||
<FilterScopeModal
|
||||
className="m-r-5"
|
||||
triggerNode={t('Set filter mapping')}
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{editMode && (
|
||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import setPeriodicRunner, {
|
|||
stopPeriodicRender,
|
||||
} from 'src/dashboard/util/setPeriodicRunner';
|
||||
import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
|
||||
const propTypes = {
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
|
|
@ -474,10 +475,15 @@ class Header extends React.PureComponent {
|
|||
shouldPersistRefreshFrequency,
|
||||
setRefreshFrequency,
|
||||
lastModifiedTime,
|
||||
filterboxMigrationState,
|
||||
} = this.props;
|
||||
const userCanEdit = dashboardInfo.dash_edit_perm;
|
||||
const userCanEdit =
|
||||
dashboardInfo.dash_edit_perm &&
|
||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING;
|
||||
const userCanShare = dashboardInfo.dash_share_perm;
|
||||
const userCanSaveAs = dashboardInfo.dash_save_perm;
|
||||
const userCanSaveAs =
|
||||
dashboardInfo.dash_save_perm &&
|
||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING;
|
||||
const shouldShowReport = !editMode && this.canAddReports();
|
||||
const refreshLimit =
|
||||
dashboardInfo.common.conf.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
|
||||
|
|
@ -669,6 +675,7 @@ class Header extends React.PureComponent {
|
|||
refreshLimit={refreshLimit}
|
||||
refreshWarning={refreshWarning}
|
||||
lastModifiedTime={lastModifiedTime}
|
||||
filterboxMigrationState={filterboxMigrationState}
|
||||
/>
|
||||
</div>
|
||||
</StyledDashboardHeader>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { List } from 'react-virtualized';
|
||||
import { createFilter } from 'react-search-input';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { t, styled, isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
|
||||
import { Input } from 'src/common/components';
|
||||
import { Select } from 'src/components';
|
||||
import Loading from 'src/components/Loading';
|
||||
|
|
@ -34,6 +34,7 @@ import {
|
|||
NEW_COMPONENTS_SOURCE_ID,
|
||||
} from 'src/dashboard/util/constants';
|
||||
import { slicePropShape } from 'src/dashboard/util/propShapes';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
import AddSliceCard from './AddSliceCard';
|
||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||
import DragDroppable from './dnd/DragDroppable';
|
||||
|
|
@ -48,6 +49,7 @@ const propTypes = {
|
|||
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
||||
editMode: PropTypes.bool,
|
||||
height: PropTypes.number,
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
|
@ -55,6 +57,7 @@ const defaultProps = {
|
|||
editMode: false,
|
||||
errorMessage: '',
|
||||
height: window.innerHeight,
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
};
|
||||
|
||||
const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
|
||||
|
|
@ -114,7 +117,12 @@ class SliceAdder extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
|
||||
const { userId, filterboxMigrationState } = this.props;
|
||||
this.slicesRequest = this.props.fetchAllSlices(
|
||||
userId,
|
||||
isFeatureEnabled(FeatureFlag.ENABLE_FILTER_BOX_MIGRATION) &&
|
||||
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||
);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
LOG_ACTIONS_FORCE_REFRESH_CHART,
|
||||
} from 'src/logger/LogUtils';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
|
||||
import SliceHeader from '../SliceHeader';
|
||||
import MissingChart from '../MissingChart';
|
||||
|
|
@ -61,6 +62,7 @@ const propTypes = {
|
|||
sliceName: PropTypes.string.isRequired,
|
||||
timeout: PropTypes.number.isRequired,
|
||||
maxRows: PropTypes.number.isRequired,
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||
// all active filter fields in dashboard
|
||||
filters: PropTypes.object.isRequired,
|
||||
refreshChart: PropTypes.func.isRequired,
|
||||
|
|
@ -102,6 +104,11 @@ const ChartOverlay = styled.div`
|
|||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
|
||||
&.is-deactivated {
|
||||
opacity: 0.5;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
}
|
||||
`;
|
||||
|
||||
export default class Chart extends React.Component {
|
||||
|
|
@ -293,6 +300,7 @@ export default class Chart extends React.Component {
|
|||
filterState,
|
||||
handleToggleFullSize,
|
||||
isFullSize,
|
||||
filterboxMigrationState,
|
||||
} = this.props;
|
||||
|
||||
const { width } = this.state;
|
||||
|
|
@ -304,6 +312,12 @@ export default class Chart extends React.Component {
|
|||
|
||||
const { queriesResponse, chartUpdateEndTime, chartStatus } = chart;
|
||||
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
|
||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||
const cachedDttm =
|
||||
|
|
@ -378,15 +392,15 @@ export default class Chart extends React.Component {
|
|||
isOverflowable && 'dashboard-chart--overflowable',
|
||||
)}
|
||||
>
|
||||
{isLoading && (
|
||||
{(isLoading || isDeactivatedViz) && (
|
||||
<ChartOverlay
|
||||
className={cx(isDeactivatedViz && 'is-deactivated')}
|
||||
style={{
|
||||
width,
|
||||
height: this.getChartHeight(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChartContainer
|
||||
width={width}
|
||||
height={this.getChartHeight()}
|
||||
|
|
@ -408,6 +422,8 @@ export default class Chart extends React.Component {
|
|||
timeout={timeout}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
vizType={slice.viz_type}
|
||||
isDeactivatedViz={isDeactivatedViz}
|
||||
filterboxMigrationState={filterboxMigrationState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { connect } from 'react-redux';
|
|||
import { LineEditableTabs } from 'src/components/Tabs';
|
||||
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
|
||||
import { Modal } from 'src/common/components';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
import DragDroppable from '../dnd/DragDroppable';
|
||||
import DragHandle from '../dnd/DragHandle';
|
||||
import DashboardComponent from '../../containers/DashboardComponent';
|
||||
|
|
@ -47,6 +48,7 @@ const propTypes = {
|
|||
editMode: PropTypes.bool.isRequired,
|
||||
renderHoverMenu: PropTypes.bool,
|
||||
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||
|
||||
// actions (from DashboardComponent.jsx)
|
||||
logEvent: PropTypes.func.isRequired,
|
||||
|
|
@ -73,6 +75,7 @@ const defaultProps = {
|
|||
availableColumnCount: 0,
|
||||
columnWidth: 0,
|
||||
directPathToChild: [],
|
||||
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
setActiveTabs() {},
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
|
|
@ -135,7 +138,10 @@ export class Tabs extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.activeKey !== this.state.activeKey) {
|
||||
if (
|
||||
prevState.activeKey !== this.state.activeKey ||
|
||||
prevProps.filterboxMigrationState !== this.props.filterboxMigrationState
|
||||
) {
|
||||
this.props.setActiveTabs(this.state.activeKey, prevState.activeKey);
|
||||
}
|
||||
}
|
||||
|
|
@ -405,6 +411,7 @@ function mapStateToProps(state) {
|
|||
return {
|
||||
nativeFilters: state.nativeFilters,
|
||||
directPathToChild: state.dashboardState.directPathToChild,
|
||||
filterboxMigrationState: state.dashboardState.filterboxMigrationState,
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps)(Tabs);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useSelector } from 'react-redux';
|
||||
import { filter, keyBy } from 'lodash';
|
||||
import {
|
||||
Filters,
|
||||
FilterSets as FilterSetsType,
|
||||
|
|
@ -27,10 +28,12 @@ import {
|
|||
DataMaskStateWithId,
|
||||
DataMaskWithId,
|
||||
} from 'src/dataMask/types';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { ChartsState, RootState } from 'src/dashboard/types';
|
||||
import { MigrationContext } from 'src/dashboard/containers/DashboardPage';
|
||||
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||
import { Filter } from 'src/dashboard/components/nativeFilters/types';
|
||||
import { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
||||
import { Filter } from '../types';
|
||||
|
||||
export const useFilterSets = () =>
|
||||
useSelector<any, FilterSetsType>(
|
||||
|
|
@ -102,14 +105,30 @@ export const useFilterUpdates = (
|
|||
export const useInitialization = () => {
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||
const filters = useFilters();
|
||||
const charts = useSelector<RootState, ChartsState>(state => state.charts);
|
||||
const filterboxMigrationState = useContext(MigrationContext);
|
||||
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
|
||||
let numberOfLoadingCharts = 0;
|
||||
if (!isInitialized) {
|
||||
numberOfLoadingCharts = document.querySelectorAll(
|
||||
'[data-ui-anchor="chart"]',
|
||||
).length;
|
||||
// do not load filter_box in reviewing
|
||||
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||
charts = keyBy(
|
||||
filter(charts, chart => chart.formData?.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(() => {
|
||||
if (isInitialized) {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ export interface Filter {
|
|||
sortMetric?: string | null;
|
||||
adhoc_filters?: AdhocFilter[];
|
||||
granularity_sqla?: string;
|
||||
granularity?: string;
|
||||
druid_time_origin?: string;
|
||||
time_grain_sqla?: string;
|
||||
time_range?: string;
|
||||
requiredFirst?: boolean;
|
||||
tabsInScope?: string[];
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ function mapStateToProps(
|
|||
ownState: dataMask[id]?.ownState,
|
||||
filterState: dataMask[id]?.filterState,
|
||||
maxRows: common.conf.SQL_MAX_ROW,
|
||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ function mapStateToProps({
|
|||
slug: dashboardInfo.slug,
|
||||
metadata: dashboardInfo.metadata,
|
||||
reports,
|
||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FC, useRef, useEffect } from 'react';
|
||||
import React, { FC, useRef, useEffect, useState } from 'react';
|
||||
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import Loading from 'src/components/Loading';
|
||||
import FilterBoxMigrationModal from 'src/dashboard/components/FilterBoxMigrationModal';
|
||||
import {
|
||||
useDashboard,
|
||||
useDashboardCharts,
|
||||
|
|
@ -31,8 +32,27 @@ import { hydrateDashboard } from 'src/dashboard/actions/hydrate';
|
|||
import { setDatasources } from 'src/dashboard/actions/datasources';
|
||||
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
|
||||
import setupPlugins from 'src/setup/setupPlugins';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { addWarningToast } from 'src/components/MessageToasts/actions';
|
||||
|
||||
import {
|
||||
getFromLocalStorage,
|
||||
setInLocalStorage,
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import {
|
||||
FILTER_BOX_MIGRATION_STATES,
|
||||
FILTER_BOX_TRANSITION_SNOOZE_DURATION,
|
||||
FILTER_BOX_TRANSITION_SNOOZED_AT,
|
||||
} from 'src/explore/constants';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { canUserEditDashboard } from 'src/dashboard/util/findPermission';
|
||||
import { getFilterSets } from '../actions/nativeFilters';
|
||||
|
||||
export const MigrationContext = React.createContext(
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
);
|
||||
|
||||
setupPlugins();
|
||||
const DashboardContainer = React.lazy(
|
||||
() =>
|
||||
|
|
@ -47,6 +67,9 @@ const originalDocumentTitle = document.title;
|
|||
|
||||
const DashboardPage: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||
state => state.user,
|
||||
);
|
||||
const { addDangerToast } = useToasts();
|
||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
||||
const { result: dashboard, error: dashboardApiError } = useDashboard(
|
||||
|
|
@ -62,15 +85,89 @@ const DashboardPage: FC = () => {
|
|||
|
||||
const error = dashboardApiError || chartsApiError;
|
||||
const readyToRender = Boolean(dashboard && charts);
|
||||
const { dashboard_title, css } = dashboard || {};
|
||||
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 [filterboxMigrationState, setFilterboxMigrationState] = useState(
|
||||
migrationStateParam || FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
);
|
||||
|
||||
if (readyToRender && !isDashboardHydrated.current) {
|
||||
isDashboardHydrated.current = true;
|
||||
dispatch(hydrateDashboard(dashboard, charts));
|
||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) {
|
||||
dispatch(getFilterSets());
|
||||
useEffect(() => {
|
||||
// should convert filter_box to filter component?
|
||||
const hasFilterBox =
|
||||
charts &&
|
||||
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 =
|
||||
getFromLocalStorage(FILTER_BOX_TRANSITION_SNOOZED_AT, 0) || {};
|
||||
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 {
|
||||
dispatch(
|
||||
addWarningToast(
|
||||
t(
|
||||
'filter_box will be deprecated ' +
|
||||
'in a future version of Superset. ' +
|
||||
'Please replace filter_box by dashboard filter components.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [readyToRender]);
|
||||
|
||||
useEffect(() => {
|
||||
if (readyToRender) {
|
||||
if (!isDashboardHydrated.current) {
|
||||
isDashboardHydrated.current = true;
|
||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) {
|
||||
// only initialize filterset once
|
||||
dispatch(getFilterSets());
|
||||
}
|
||||
}
|
||||
dispatch(hydrateDashboard(dashboard, charts, filterboxMigrationState));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [readyToRender, filterboxMigrationState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboard_title) {
|
||||
|
|
@ -103,7 +200,34 @@ const DashboardPage: FC = () => {
|
|||
if (error) throw error; // caught in error boundary
|
||||
if (!readyToRender) return <Loading />;
|
||||
|
||||
return <DashboardContainer />;
|
||||
return (
|
||||
<>
|
||||
<FilterBoxMigrationModal
|
||||
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 =
|
||||
getFromLocalStorage(FILTER_BOX_TRANSITION_SNOOZED_AT, 0) || {};
|
||||
setInLocalStorage(FILTER_BOX_TRANSITION_SNOOZED_AT, {
|
||||
...snoozedDash,
|
||||
[id]: Date.now(),
|
||||
});
|
||||
setFilterboxMigrationState(FILTER_BOX_MIGRATION_STATES.SNOOZED);
|
||||
}}
|
||||
/>
|
||||
|
||||
<MigrationContext.Provider value={filterboxMigrationState}>
|
||||
<DashboardContainer />
|
||||
</MigrationContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,12 @@ import { connect } from 'react-redux';
|
|||
import { fetchAllSlices } from '../actions/sliceEntities';
|
||||
import SliceAdder from '../components/SliceAdder';
|
||||
|
||||
function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
|
||||
function mapStateToProps(
|
||||
{ sliceEntities, dashboardInfo, dashboardState },
|
||||
ownProps,
|
||||
) {
|
||||
return {
|
||||
height: ownProps.height,
|
||||
userId: dashboardInfo.userId,
|
||||
selectedSliceIds: dashboardState.sliceIds,
|
||||
slices: sliceEntities.slices,
|
||||
|
|
@ -31,6 +35,7 @@ function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
|
|||
errorMessage: sliceEntities.errorMessage,
|
||||
lastUpdated: sliceEntities.lastUpdated,
|
||||
editMode: dashboardState.editMode,
|
||||
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { DatasourceMeta } from '@superset-ui/chart-controls';
|
|||
import { chart } from 'src/chart/chartReducer';
|
||||
import componentTypes from 'src/dashboard/util/componentTypes';
|
||||
|
||||
import { User } from 'src/types/bootstrapTypes';
|
||||
import { DataMaskStateWithId } from '../dataMask/types';
|
||||
import { NativeFiltersState } from './reducers/types';
|
||||
import { ChartState } from '../explore/types';
|
||||
|
|
@ -64,7 +65,6 @@ export type DashboardState = {
|
|||
isRefreshing: boolean;
|
||||
hasUnsavedChanges: boolean;
|
||||
};
|
||||
|
||||
export type DashboardInfo = {
|
||||
id: number;
|
||||
common: {
|
||||
|
|
@ -104,6 +104,7 @@ export type RootState = {
|
|||
dataMask: DataMaskStateWithId;
|
||||
impressionId: string;
|
||||
nativeFilters: NativeFiltersState;
|
||||
user: User;
|
||||
};
|
||||
|
||||
/** State of dashboardLayout in redux */
|
||||
|
|
|
|||
|
|
@ -62,9 +62,7 @@ export function getAppliedFilterValues(chartId) {
|
|||
return appliedFilterValuesByChart[chartId];
|
||||
}
|
||||
|
||||
export function getChartIdsInFilterScope({
|
||||
filterScope = DASHBOARD_FILTER_SCOPE_GLOBAL,
|
||||
}) {
|
||||
export function getChartIdsInFilterScope({ filterScope }) {
|
||||
function traverse(chartIds = [], component = {}, immuneChartIds = []) {
|
||||
if (!component) {
|
||||
return;
|
||||
|
|
@ -85,7 +83,8 @@ export function getChartIdsInFilterScope({
|
|||
}
|
||||
|
||||
const chartIds = [];
|
||||
const { scope: scopeComponentIds, immune: immuneChartIds } = filterScope;
|
||||
const { scope: scopeComponentIds, immune: immuneChartIds } =
|
||||
filterScope || DASHBOARD_FILTER_SCOPE_GLOBAL;
|
||||
scopeComponentIds.forEach(componentId =>
|
||||
traverse(chartIds, allComponents[componentId], immuneChartIds),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* 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',
|
||||
time_range_endpoints: ['inclusive', 'exclusive'],
|
||||
viz_type: 'filter_box',
|
||||
},
|
||||
modified: '<bound method AuditMixinNullable.modified of Region Filter>',
|
||||
slice_name: 'Region Filter',
|
||||
slice_url: '/superset/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',
|
||||
time_range_endpoints: ['inclusive', 'exclusive'],
|
||||
viz_type: 'big_number',
|
||||
},
|
||||
modified: "<bound method AuditMixinNullable.modified of World's Population>",
|
||||
slice_name: "World's Population",
|
||||
slice_url: '/superset/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'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,525 @@
|
|||
/**
|
||||
* 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,
|
||||
NativeFilterType,
|
||||
} from 'src/dashboard/components/nativeFilters/types';
|
||||
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 { TimeGranularity } from '@superset-ui/core';
|
||||
import { getChartIdsInFilterScope } 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;
|
||||
druid_time_origin?: string;
|
||||
show_druid_time_granularity?: boolean;
|
||||
show_druid_time_origin?: boolean;
|
||||
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 PreselectedFiltersMeatadata {
|
||||
[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: PreselectedFiltersMeatadata,
|
||||
) => (filterKey: string, column: string) => {
|
||||
if (preselectedFilters[filterKey] && 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] = getChartIdsInFilterScope({
|
||||
filterScope,
|
||||
}).filter(
|
||||
chartId => filterChartIds.includes(chartId) && String(chartId) !== key,
|
||||
);
|
||||
});
|
||||
});
|
||||
return filterFieldsDependencies;
|
||||
};
|
||||
|
||||
export default function getNativeFilterConfig(
|
||||
chartData: SliceData[] = [],
|
||||
filterScopes: FilterScopesMetadata = {},
|
||||
preselectFilters: PreselectedFiltersMeatadata = {},
|
||||
): 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,
|
||||
druid_time_origin,
|
||||
filter_configs = [],
|
||||
granularity,
|
||||
granularity_sqla,
|
||||
show_druid_time_granularity = false,
|
||||
show_druid_time_origin = false,
|
||||
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);
|
||||
}
|
||||
|
||||
if (show_druid_time_granularity) {
|
||||
const { scope, immune }: FilterScopeType =
|
||||
scopesByChartId[TIME_FILTER_MAP.granularity] ||
|
||||
DASHBOARD_FILTER_SCOPE_GLOBAL;
|
||||
const druidGranularityFilter: Filter = {
|
||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
||||
description: 'time grain filter',
|
||||
controlValues: {},
|
||||
name: TIME_FILTER_LABELS.granularity,
|
||||
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.granularity] =
|
||||
druidGranularityFilter.id;
|
||||
const dashboardDefaultValues = getDashboardDefaultValues(
|
||||
key,
|
||||
TIME_FILTER_MAP.granularity,
|
||||
);
|
||||
if (!isEmpty(dashboardDefaultValues)) {
|
||||
druidGranularityFilter.defaultDataMask = {
|
||||
extraFormData: {
|
||||
granularity_sqla: (dashboardDefaultValues ||
|
||||
granularity) as string,
|
||||
},
|
||||
filterState: {
|
||||
value: setValuesInArray(dashboardDefaultValues, granularity),
|
||||
},
|
||||
};
|
||||
}
|
||||
filterConfig.push(druidGranularityFilter);
|
||||
}
|
||||
|
||||
if (show_druid_time_origin) {
|
||||
const { scope, immune }: FilterScopeType =
|
||||
scopesByChartId[TIME_FILTER_MAP.druid_time_origin] ||
|
||||
DASHBOARD_FILTER_SCOPE_GLOBAL;
|
||||
const druidOriginFilter: Filter = {
|
||||
id: `NATIVE_FILTER-${shortid.generate()}`,
|
||||
description: 'time column filter',
|
||||
controlValues: {},
|
||||
name: TIME_FILTER_LABELS.druid_time_origin,
|
||||
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.druid_time_origin
|
||||
] = druidOriginFilter.id;
|
||||
const dashboardDefaultValues = getDashboardDefaultValues(
|
||||
key,
|
||||
TIME_FILTER_MAP.druid_time_origin,
|
||||
);
|
||||
if (!isEmpty(dashboardDefaultValues)) {
|
||||
druidOriginFilter.defaultDataMask = {
|
||||
extraFormData: {
|
||||
granularity_sqla: (dashboardDefaultValues ||
|
||||
druid_time_origin) as string,
|
||||
},
|
||||
filterState: {
|
||||
value: setValuesInArray(
|
||||
dashboardDefaultValues,
|
||||
druid_time_origin,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
filterConfig.push(druidOriginFilter);
|
||||
}
|
||||
}
|
||||
|
||||
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.push(parentComponentId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return filterConfig;
|
||||
}
|
||||
|
|
@ -115,10 +115,12 @@ export const TIME_FILTER_LABELS = {
|
|||
};
|
||||
|
||||
export const FILTER_CONFIG_ATTRIBUTES = {
|
||||
CLEARABLE: 'clearable',
|
||||
DEFAULT_VALUE: 'defaultValue',
|
||||
MULTIPLE: 'multiple',
|
||||
SEARCH_ALL_OPTIONS: 'searchAllOptions',
|
||||
CLEARABLE: 'clearable',
|
||||
SORT_ASCENDING: 'asc',
|
||||
SORT_METRIC: 'metric',
|
||||
};
|
||||
|
||||
export const FILTER_OPTIONS_LIMIT = 1000;
|
||||
|
|
@ -137,3 +139,15 @@ export const TIME_FILTER_MAP = {
|
|||
// TODO: make this configurable per Superset installation
|
||||
export const DEFAULT_TIME_RANGE = 'No filter';
|
||||
export const NO_TIME_RANGE = 'No filter';
|
||||
|
||||
export enum FILTER_BOX_MIGRATION_STATES {
|
||||
CONVERTED = 'CONVERTED',
|
||||
NOOP = 'NOOP',
|
||||
REVIEWING = 'REVIEWING',
|
||||
SNOOZED = 'SNOOZED',
|
||||
UNDECIDED = 'UNDECIDED',
|
||||
}
|
||||
|
||||
export const FILTER_BOX_TRANSITION_SNOOZED_AT =
|
||||
'filter_box_transition_snoozed_at';
|
||||
export const FILTER_BOX_TRANSITION_SNOOZE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export interface Chart {
|
|||
thumbnail_url?: string;
|
||||
owners?: Owner[];
|
||||
datasource_name_text?: string;
|
||||
form_data: {
|
||||
viz_type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Slice = {
|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
|||
"OMNIBAR": False,
|
||||
"DASHBOARD_RBAC": False,
|
||||
"ENABLE_EXPLORE_DRAG_AND_DROP": False,
|
||||
"ENABLE_FILTER_BOX_MIGRATION": False,
|
||||
"ENABLE_DND_WITH_CLICK_UX": False,
|
||||
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
|
||||
# with screenshot and link
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import json
|
||||
import re
|
||||
from typing import List, Union
|
||||
|
||||
|
|
@ -116,8 +117,17 @@ class Dashboard(BaseSupersetView):
|
|||
@expose("/new/")
|
||||
def new(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||
"""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(
|
||||
dashboard_title="[ untitled dashboard ]", owners=[g.user]
|
||||
dashboard_title="[ untitled dashboard ]",
|
||||
owners=[g.user],
|
||||
json_metadata=json.dumps(metadata, sort_keys=True),
|
||||
)
|
||||
db.session.add(new_dashboard)
|
||||
db.session.commit()
|
||||
|
|
|
|||
Loading…
Reference in New Issue