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,
|
vizType: PropTypes.string.isRequired,
|
||||||
triggerRender: PropTypes.bool,
|
triggerRender: 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,
|
||||||
|
|
@ -82,6 +83,7 @@ const defaultProps = {
|
||||||
triggerRender: false,
|
triggerRender: false,
|
||||||
dashboardId: null,
|
dashboardId: null,
|
||||||
chartStackTrace: null,
|
chartStackTrace: null,
|
||||||
|
isDeactivatedViz: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Styles = styled.div`
|
const Styles = styled.div`
|
||||||
|
|
@ -114,13 +116,25 @@ class Chart extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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();
|
this.runQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
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();
|
this.runQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +235,8 @@ class Chart extends React.PureComponent {
|
||||||
onQuery,
|
onQuery,
|
||||||
refreshOverlayVisible,
|
refreshOverlayVisible,
|
||||||
queriesResponse = [],
|
queriesResponse = [],
|
||||||
|
isDeactivatedViz = false,
|
||||||
|
width,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isLoading = chartStatus === 'loading';
|
const isLoading = chartStatus === 'loading';
|
||||||
|
|
@ -250,6 +266,7 @@ class Chart extends React.PureComponent {
|
||||||
className="chart-container"
|
className="chart-container"
|
||||||
data-test="chart-container"
|
data-test="chart-container"
|
||||||
height={height}
|
height={height}
|
||||||
|
width={width}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`slice_container ${isFaded ? ' faded' : ''}`}
|
className={`slice_container ${isFaded ? ' faded' : ''}`}
|
||||||
|
|
@ -266,7 +283,7 @@ class Chart extends React.PureComponent {
|
||||||
</RefreshOverlayWrapper>
|
</RefreshOverlayWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && <Loading />}
|
{isLoading && !isDeactivatedViz && <Loading />}
|
||||||
</Styles>
|
</Styles>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,18 @@ export function useTransformedResource<IN, OUT>(
|
||||||
// While incomplete, there is no result - no need to transform.
|
// While incomplete, there is no result - no need to transform.
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
return {
|
try {
|
||||||
...resource,
|
return {
|
||||||
result: transformFn(resource.result),
|
...resource,
|
||||||
};
|
result: transformFn(resource.result),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: ResourceStatus.ERROR,
|
||||||
|
result: null,
|
||||||
|
error: e,
|
||||||
|
};
|
||||||
|
}
|
||||||
}, [resource, transformFn]);
|
}, [resource, transformFn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ export const useDashboard = (idOrSlug: string | number) =>
|
||||||
useApiV1Resource<Dashboard>(`/api/v1/dashboard/${idOrSlug}`),
|
useApiV1Resource<Dashboard>(`/api/v1/dashboard/${idOrSlug}`),
|
||||||
dashboard => ({
|
dashboard => ({
|
||||||
...dashboard,
|
...dashboard,
|
||||||
metadata: dashboard.json_metadata && JSON.parse(dashboard.json_metadata),
|
metadata:
|
||||||
|
(dashboard.json_metadata && JSON.parse(dashboard.json_metadata)) || {},
|
||||||
position_data:
|
position_data:
|
||||||
dashboard.position_json && JSON.parse(dashboard.position_json),
|
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 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',
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,21 @@ 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 { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
|
||||||
import extractUrlParams from '../util/extractUrlParams';
|
import extractUrlParams from '../util/extractUrlParams';
|
||||||
|
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
|
||||||
|
|
||||||
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
|
||||||
|
|
||||||
export const hydrateDashboard = (dashboardData, chartData) => (
|
export const hydrateDashboard = (
|
||||||
dispatch,
|
dashboardData,
|
||||||
getState,
|
chartData,
|
||||||
) => {
|
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||||
|
) => (dispatch, getState) => {
|
||||||
const { user, common } = getState();
|
const { user, common } = getState();
|
||||||
let { metadata } = dashboardData;
|
|
||||||
|
const { metadata } = dashboardData;
|
||||||
const regularUrlParams = extractUrlParams('regular');
|
const regularUrlParams = extractUrlParams('regular');
|
||||||
const reservedUrlParams = extractUrlParams('reserved');
|
const reservedUrlParams = extractUrlParams('reserved');
|
||||||
const editMode = reservedUrlParams.edit === 'true';
|
const editMode = reservedUrlParams.edit === 'true';
|
||||||
|
|
@ -227,19 +231,25 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
||||||
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);
|
||||||
dashboardFilters[key] = {
|
if (
|
||||||
...dashboardFilter,
|
[
|
||||||
chartId: key,
|
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||||
componentId,
|
FILTER_BOX_MIGRATION_STATES.SNOOZED,
|
||||||
datasourceId: slice.form_data.datasource,
|
].includes(filterboxMigrationState)
|
||||||
filterName: slice.slice_name,
|
) {
|
||||||
directPathToFilter,
|
dashboardFilters[key] = {
|
||||||
columns,
|
...dashboardFilter,
|
||||||
labels,
|
chartId: key,
|
||||||
scopes: scopesByChartId,
|
componentId,
|
||||||
isInstantFilter: !!slice.form_data.instant_filtering,
|
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
|
||||||
|
|
@ -278,17 +288,28 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
||||||
directPathToChild.push(directLinkComponentId);
|
directPathToChild.push(directLinkComponentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeFilters = getInitialNativeFilterState({
|
// should convert filter_box to filter component?
|
||||||
filterConfig: metadata?.native_filter_configuration || [],
|
let filterConfig = metadata?.native_filter_configuration || [];
|
||||||
});
|
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||||
|
filterConfig = getNativeFilterConfig(
|
||||||
if (!metadata) {
|
chartData,
|
||||||
metadata = {};
|
filterScopes,
|
||||||
|
preselectFilters,
|
||||||
|
);
|
||||||
|
metadata.native_filter_configuration = filterConfig;
|
||||||
|
metadata.show_native_filters = true;
|
||||||
}
|
}
|
||||||
|
const nativeFilters = getInitialNativeFilterState({
|
||||||
|
filterConfig,
|
||||||
|
});
|
||||||
metadata.show_native_filters =
|
metadata.show_native_filters =
|
||||||
dashboardData?.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 (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,
|
||||||
|
|
@ -379,6 +400,7 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
||||||
lastModifiedTime: dashboardData.changed_on,
|
lastModifiedTime: dashboardData.changed_on,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
activeTabs: [],
|
activeTabs: [],
|
||||||
|
filterboxMigrationState,
|
||||||
},
|
},
|
||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function fetchAllSlicesFailed(error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FETCH_SLICES_PAGE_SIZE = 200;
|
const FETCH_SLICES_PAGE_SIZE = 200;
|
||||||
export function fetchAllSlices(userId) {
|
export function fetchAllSlices(userId, excludeFilterBox = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { sliceEntities } = getState();
|
const { sliceEntities } = getState();
|
||||||
if (sliceEntities.lastUpdated === 0) {
|
if (sliceEntities.lastUpdated === 0) {
|
||||||
|
|
@ -71,7 +71,12 @@ export function fetchAllSlices(userId) {
|
||||||
})
|
})
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const slices = {};
|
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);
|
let form_data = JSON.parse(slice.params);
|
||||||
form_data = {
|
form_data = {
|
||||||
...form_data,
|
...form_data,
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
*/
|
*/
|
||||||
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 } from 'react';
|
import { useCallback, useEffect, useState, useContext } 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,6 +31,7 @@ import { Filter } from '../nativeFilters/types';
|
||||||
|
|
||||||
// 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 [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
|
const [dashboardFiltersOpen, setDashboardFiltersOpen] = useState(
|
||||||
getUrlParam(URL_PARAMS.showFilters) ?? true,
|
getUrlParam(URL_PARAMS.showFilters) ?? true,
|
||||||
|
|
@ -74,12 +76,14 @@ export const useNativeFilters = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
filterValues.length === 0 &&
|
filterValues.length === 0 &&
|
||||||
dashboardFiltersOpen &&
|
nativeFiltersEnabled &&
|
||||||
nativeFiltersEnabled
|
['CONVERTED', 'REVIEWING', 'NOOP'].includes(filterboxMigrationState)
|
||||||
) {
|
) {
|
||||||
toggleDashboardFiltersOpen(false);
|
toggleDashboardFiltersOpen(false);
|
||||||
|
} else {
|
||||||
|
toggleDashboardFiltersOpen(true);
|
||||||
}
|
}
|
||||||
}, [filterValues.length]);
|
}, [filterValues.length, filterboxMigrationState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showDashboard) {
|
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 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';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
addSuccessToast: PropTypes.func.isRequired,
|
||||||
|
|
@ -65,6 +66,7 @@ const propTypes = {
|
||||||
refreshLimit: PropTypes.number,
|
refreshLimit: PropTypes.number,
|
||||||
refreshWarning: PropTypes.string,
|
refreshWarning: PropTypes.string,
|
||||||
lastModifiedTime: PropTypes.number.isRequired,
|
lastModifiedTime: PropTypes.number.isRequired,
|
||||||
|
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
|
@ -72,6 +74,7 @@ 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 = {
|
||||||
|
|
@ -209,6 +212,7 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
lastModifiedTime,
|
lastModifiedTime,
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
|
filterboxMigrationState,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const emailTitle = t('Superset dashboard');
|
const emailTitle = t('Superset dashboard');
|
||||||
|
|
@ -283,14 +287,15 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
{editMode && (
|
{editMode &&
|
||||||
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.CONVERTED && (
|
||||||
<FilterScopeModal
|
<Menu.Item key={MENU_KEYS.SET_FILTER_MAPPING}>
|
||||||
className="m-r-5"
|
<FilterScopeModal
|
||||||
triggerNode={t('Set filter mapping')}
|
className="m-r-5"
|
||||||
/>
|
triggerNode={t('Set filter mapping')}
|
||||||
</Menu.Item>
|
/>
|
||||||
)}
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
<Menu.Item key={MENU_KEYS.EDIT_PROPERTIES}>
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import setPeriodicRunner, {
|
||||||
stopPeriodicRender,
|
stopPeriodicRender,
|
||||||
} from 'src/dashboard/util/setPeriodicRunner';
|
} from 'src/dashboard/util/setPeriodicRunner';
|
||||||
import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal';
|
import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal';
|
||||||
|
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
addSuccessToast: PropTypes.func.isRequired,
|
addSuccessToast: PropTypes.func.isRequired,
|
||||||
|
|
@ -474,10 +475,15 @@ class Header extends React.PureComponent {
|
||||||
shouldPersistRefreshFrequency,
|
shouldPersistRefreshFrequency,
|
||||||
setRefreshFrequency,
|
setRefreshFrequency,
|
||||||
lastModifiedTime,
|
lastModifiedTime,
|
||||||
|
filterboxMigrationState,
|
||||||
} = this.props;
|
} = 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 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 shouldShowReport = !editMode && this.canAddReports();
|
||||||
const refreshLimit =
|
const refreshLimit =
|
||||||
dashboardInfo.common.conf.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
|
dashboardInfo.common.conf.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
|
||||||
|
|
@ -669,6 +675,7 @@ class Header extends React.PureComponent {
|
||||||
refreshLimit={refreshLimit}
|
refreshLimit={refreshLimit}
|
||||||
refreshWarning={refreshWarning}
|
refreshWarning={refreshWarning}
|
||||||
lastModifiedTime={lastModifiedTime}
|
lastModifiedTime={lastModifiedTime}
|
||||||
|
filterboxMigrationState={filterboxMigrationState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StyledDashboardHeader>
|
</StyledDashboardHeader>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { List } from 'react-virtualized';
|
import { List } from 'react-virtualized';
|
||||||
import { createFilter } from 'react-search-input';
|
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 { Input } from 'src/common/components';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
|
|
@ -34,6 +34,7 @@ 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 AddSliceCard from './AddSliceCard';
|
import AddSliceCard from './AddSliceCard';
|
||||||
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
|
||||||
import DragDroppable from './dnd/DragDroppable';
|
import DragDroppable from './dnd/DragDroppable';
|
||||||
|
|
@ -48,6 +49,7 @@ const propTypes = {
|
||||||
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
selectedSliceIds: PropTypes.arrayOf(PropTypes.number),
|
||||||
editMode: PropTypes.bool,
|
editMode: PropTypes.bool,
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
|
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
|
@ -55,6 +57,7 @@ const defaultProps = {
|
||||||
editMode: false,
|
editMode: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
|
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'];
|
||||||
|
|
@ -114,7 +117,12 @@ class SliceAdder extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ 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 SliceHeader from '../SliceHeader';
|
import SliceHeader from '../SliceHeader';
|
||||||
import MissingChart from '../MissingChart';
|
import MissingChart from '../MissingChart';
|
||||||
|
|
@ -61,6 +62,7 @@ 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: FILTER_BOX_MIGRATION_STATES,
|
||||||
// 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,
|
||||||
|
|
@ -102,6 +104,11 @@ 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};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default class Chart extends React.Component {
|
export default class Chart extends React.Component {
|
||||||
|
|
@ -293,6 +300,7 @@ export default class Chart extends React.Component {
|
||||||
filterState,
|
filterState,
|
||||||
handleToggleFullSize,
|
handleToggleFullSize,
|
||||||
isFullSize,
|
isFullSize,
|
||||||
|
filterboxMigrationState,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { width } = this.state;
|
const { width } = this.state;
|
||||||
|
|
@ -304,6 +312,12 @@ export default 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 =
|
||||||
|
|
@ -378,15 +392,15 @@ export default class Chart extends React.Component {
|
||||||
isOverflowable && 'dashboard-chart--overflowable',
|
isOverflowable && 'dashboard-chart--overflowable',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{(isLoading || isDeactivatedViz) && (
|
||||||
<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()}
|
||||||
|
|
@ -408,6 +422,8 @@ export default class Chart extends React.Component {
|
||||||
timeout={timeout}
|
timeout={timeout}
|
||||||
triggerQuery={chart.triggerQuery}
|
triggerQuery={chart.triggerQuery}
|
||||||
vizType={slice.viz_type}
|
vizType={slice.viz_type}
|
||||||
|
isDeactivatedViz={isDeactivatedViz}
|
||||||
|
filterboxMigrationState={filterboxMigrationState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ 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 { Modal } from 'src/common/components';
|
import { Modal } from 'src/common/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';
|
||||||
|
|
@ -47,6 +48,7 @@ const propTypes = {
|
||||||
editMode: PropTypes.bool.isRequired,
|
editMode: PropTypes.bool.isRequired,
|
||||||
renderHoverMenu: PropTypes.bool,
|
renderHoverMenu: PropTypes.bool,
|
||||||
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
directPathToChild: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES,
|
||||||
|
|
||||||
// actions (from DashboardComponent.jsx)
|
// actions (from DashboardComponent.jsx)
|
||||||
logEvent: PropTypes.func.isRequired,
|
logEvent: PropTypes.func.isRequired,
|
||||||
|
|
@ -73,6 +75,7 @@ const defaultProps = {
|
||||||
availableColumnCount: 0,
|
availableColumnCount: 0,
|
||||||
columnWidth: 0,
|
columnWidth: 0,
|
||||||
directPathToChild: [],
|
directPathToChild: [],
|
||||||
|
filterboxMigrationState: FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||||
setActiveTabs() {},
|
setActiveTabs() {},
|
||||||
onResizeStart() {},
|
onResizeStart() {},
|
||||||
onResize() {},
|
onResize() {},
|
||||||
|
|
@ -135,7 +138,10 @@ export class Tabs extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
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);
|
this.props.setActiveTabs(this.state.activeKey, prevState.activeKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,6 +411,7 @@ function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
nativeFilters: state.nativeFilters,
|
nativeFilters: state.nativeFilters,
|
||||||
directPathToChild: state.dashboardState.directPathToChild,
|
directPathToChild: state.dashboardState.directPathToChild,
|
||||||
|
filterboxMigrationState: state.dashboardState.filterboxMigrationState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps)(Tabs);
|
export default connect(mapStateToProps)(Tabs);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
/* 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 {
|
||||||
Filters,
|
Filters,
|
||||||
FilterSets as FilterSetsType,
|
FilterSets as FilterSetsType,
|
||||||
|
|
@ -27,10 +28,12 @@ import {
|
||||||
DataMaskStateWithId,
|
DataMaskStateWithId,
|
||||||
DataMaskWithId,
|
DataMaskWithId,
|
||||||
} from 'src/dataMask/types';
|
} 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 { 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 { NATIVE_FILTER_PREFIX } from '../FiltersConfigModal/utils';
|
||||||
import { Filter } from '../types';
|
|
||||||
|
|
||||||
export const useFilterSets = () =>
|
export const useFilterSets = () =>
|
||||||
useSelector<any, FilterSetsType>(
|
useSelector<any, FilterSetsType>(
|
||||||
|
|
@ -102,14 +105,30 @@ 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 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
|
// 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) {
|
||||||
numberOfLoadingCharts = document.querySelectorAll(
|
// do not load filter_box in reviewing
|
||||||
'[data-ui-anchor="chart"]',
|
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
|
||||||
).length;
|
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(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ export interface Filter {
|
||||||
sortMetric?: string | null;
|
sortMetric?: string | null;
|
||||||
adhoc_filters?: AdhocFilter[];
|
adhoc_filters?: AdhocFilter[];
|
||||||
granularity_sqla?: string;
|
granularity_sqla?: string;
|
||||||
|
granularity?: string;
|
||||||
|
druid_time_origin?: string;
|
||||||
|
time_grain_sqla?: string;
|
||||||
time_range?: string;
|
time_range?: string;
|
||||||
requiredFirst?: boolean;
|
requiredFirst?: boolean;
|
||||||
tabsInScope?: string[];
|
tabsInScope?: string[];
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ function mapStateToProps(
|
||||||
ownState: dataMask[id]?.ownState,
|
ownState: dataMask[id]?.ownState,
|
||||||
filterState: dataMask[id]?.filterState,
|
filterState: dataMask[id]?.filterState,
|
||||||
maxRows: common.conf.SQL_MAX_ROW,
|
maxRows: common.conf.SQL_MAX_ROW,
|
||||||
|
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ function mapStateToProps({
|
||||||
slug: dashboardInfo.slug,
|
slug: dashboardInfo.slug,
|
||||||
metadata: dashboardInfo.metadata,
|
metadata: dashboardInfo.metadata,
|
||||||
reports,
|
reports,
|
||||||
|
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 { 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 { useParams } from 'react-router-dom';
|
||||||
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,
|
||||||
|
|
@ -31,8 +32,27 @@ 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 {
|
||||||
|
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';
|
import { getFilterSets } from '../actions/nativeFilters';
|
||||||
|
|
||||||
|
export const MigrationContext = React.createContext(
|
||||||
|
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||||
|
);
|
||||||
|
|
||||||
setupPlugins();
|
setupPlugins();
|
||||||
const DashboardContainer = React.lazy(
|
const DashboardContainer = React.lazy(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -47,6 +67,9 @@ const originalDocumentTitle = document.title;
|
||||||
|
|
||||||
const DashboardPage: FC = () => {
|
const DashboardPage: FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||||
|
state => state.user,
|
||||||
|
);
|
||||||
const { addDangerToast } = useToasts();
|
const { addDangerToast } = useToasts();
|
||||||
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
const { idOrSlug } = useParams<{ idOrSlug: string }>();
|
||||||
const { result: dashboard, error: dashboardApiError } = useDashboard(
|
const { result: dashboard, error: dashboardApiError } = useDashboard(
|
||||||
|
|
@ -62,15 +85,89 @@ const DashboardPage: FC = () => {
|
||||||
|
|
||||||
const error = dashboardApiError || chartsApiError;
|
const error = dashboardApiError || chartsApiError;
|
||||||
const readyToRender = Boolean(dashboard && charts);
|
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) {
|
useEffect(() => {
|
||||||
isDashboardHydrated.current = true;
|
// should convert filter_box to filter component?
|
||||||
dispatch(hydrateDashboard(dashboard, charts));
|
const hasFilterBox =
|
||||||
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) {
|
charts &&
|
||||||
dispatch(getFilterSets());
|
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(() => {
|
useEffect(() => {
|
||||||
if (dashboard_title) {
|
if (dashboard_title) {
|
||||||
|
|
@ -103,7 +200,34 @@ const DashboardPage: FC = () => {
|
||||||
if (error) throw error; // caught in error boundary
|
if (error) throw error; // caught in error boundary
|
||||||
if (!readyToRender) return <Loading />;
|
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;
|
export default DashboardPage;
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,12 @@ import { connect } from 'react-redux';
|
||||||
import { fetchAllSlices } from '../actions/sliceEntities';
|
import { fetchAllSlices } from '../actions/sliceEntities';
|
||||||
import SliceAdder from '../components/SliceAdder';
|
import SliceAdder from '../components/SliceAdder';
|
||||||
|
|
||||||
function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
|
function mapStateToProps(
|
||||||
|
{ sliceEntities, dashboardInfo, dashboardState },
|
||||||
|
ownProps,
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
|
height: ownProps.height,
|
||||||
userId: dashboardInfo.userId,
|
userId: dashboardInfo.userId,
|
||||||
selectedSliceIds: dashboardState.sliceIds,
|
selectedSliceIds: dashboardState.sliceIds,
|
||||||
slices: sliceEntities.slices,
|
slices: sliceEntities.slices,
|
||||||
|
|
@ -31,6 +35,7 @@ function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
|
||||||
errorMessage: sliceEntities.errorMessage,
|
errorMessage: sliceEntities.errorMessage,
|
||||||
lastUpdated: sliceEntities.lastUpdated,
|
lastUpdated: sliceEntities.lastUpdated,
|
||||||
editMode: dashboardState.editMode,
|
editMode: dashboardState.editMode,
|
||||||
|
filterboxMigrationState: dashboardState.filterboxMigrationState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { DatasourceMeta } from '@superset-ui/chart-controls';
|
||||||
import { chart } from 'src/chart/chartReducer';
|
import { chart } from 'src/chart/chartReducer';
|
||||||
import componentTypes from 'src/dashboard/util/componentTypes';
|
import componentTypes from 'src/dashboard/util/componentTypes';
|
||||||
|
|
||||||
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
import { DataMaskStateWithId } from '../dataMask/types';
|
import { DataMaskStateWithId } from '../dataMask/types';
|
||||||
import { NativeFiltersState } from './reducers/types';
|
import { NativeFiltersState } from './reducers/types';
|
||||||
import { ChartState } from '../explore/types';
|
import { ChartState } from '../explore/types';
|
||||||
|
|
@ -64,7 +65,6 @@ export type DashboardState = {
|
||||||
isRefreshing: boolean;
|
isRefreshing: boolean;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardInfo = {
|
export type DashboardInfo = {
|
||||||
id: number;
|
id: number;
|
||||||
common: {
|
common: {
|
||||||
|
|
@ -104,6 +104,7 @@ export type RootState = {
|
||||||
dataMask: DataMaskStateWithId;
|
dataMask: DataMaskStateWithId;
|
||||||
impressionId: string;
|
impressionId: string;
|
||||||
nativeFilters: NativeFiltersState;
|
nativeFilters: NativeFiltersState;
|
||||||
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** State of dashboardLayout in redux */
|
/** State of dashboardLayout in redux */
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,7 @@ export function getAppliedFilterValues(chartId) {
|
||||||
return appliedFilterValuesByChart[chartId];
|
return appliedFilterValuesByChart[chartId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChartIdsInFilterScope({
|
export function getChartIdsInFilterScope({ filterScope }) {
|
||||||
filterScope = DASHBOARD_FILTER_SCOPE_GLOBAL,
|
|
||||||
}) {
|
|
||||||
function traverse(chartIds = [], component = {}, immuneChartIds = []) {
|
function traverse(chartIds = [], component = {}, immuneChartIds = []) {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -85,7 +83,8 @@ export function getChartIdsInFilterScope({
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartIds = [];
|
const chartIds = [];
|
||||||
const { scope: scopeComponentIds, immune: immuneChartIds } = filterScope;
|
const { scope: scopeComponentIds, immune: immuneChartIds } =
|
||||||
|
filterScope || DASHBOARD_FILTER_SCOPE_GLOBAL;
|
||||||
scopeComponentIds.forEach(componentId =>
|
scopeComponentIds.forEach(componentId =>
|
||||||
traverse(chartIds, allComponents[componentId], immuneChartIds),
|
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 = {
|
export const FILTER_CONFIG_ATTRIBUTES = {
|
||||||
|
CLEARABLE: 'clearable',
|
||||||
DEFAULT_VALUE: 'defaultValue',
|
DEFAULT_VALUE: 'defaultValue',
|
||||||
MULTIPLE: 'multiple',
|
MULTIPLE: 'multiple',
|
||||||
SEARCH_ALL_OPTIONS: 'searchAllOptions',
|
SEARCH_ALL_OPTIONS: 'searchAllOptions',
|
||||||
CLEARABLE: 'clearable',
|
SORT_ASCENDING: 'asc',
|
||||||
|
SORT_METRIC: 'metric',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FILTER_OPTIONS_LIMIT = 1000;
|
export const FILTER_OPTIONS_LIMIT = 1000;
|
||||||
|
|
@ -137,3 +139,15 @@ export const TIME_FILTER_MAP = {
|
||||||
// TODO: make this configurable per Superset installation
|
// TODO: make this configurable per Superset installation
|
||||||
export const DEFAULT_TIME_RANGE = 'No filter';
|
export const DEFAULT_TIME_RANGE = 'No filter';
|
||||||
export const NO_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;
|
thumbnail_url?: string;
|
||||||
owners?: Owner[];
|
owners?: Owner[];
|
||||||
datasource_name_text?: string;
|
datasource_name_text?: string;
|
||||||
|
form_data: {
|
||||||
|
viz_type: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Slice = {
|
export type Slice = {
|
||||||
|
|
|
||||||
|
|
@ -392,6 +392,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
||||||
"OMNIBAR": False,
|
"OMNIBAR": False,
|
||||||
"DASHBOARD_RBAC": False,
|
"DASHBOARD_RBAC": False,
|
||||||
"ENABLE_EXPLORE_DRAG_AND_DROP": False,
|
"ENABLE_EXPLORE_DRAG_AND_DROP": False,
|
||||||
|
"ENABLE_FILTER_BOX_MIGRATION": False,
|
||||||
"ENABLE_DND_WITH_CLICK_UX": False,
|
"ENABLE_DND_WITH_CLICK_UX": False,
|
||||||
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
|
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
|
||||||
# with screenshot and link
|
# with screenshot and link
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
@ -116,8 +117,17 @@ 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 ]", 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.add(new_dashboard)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue