feat(native-filters): add support for preselect filters (#15427)
* feat(native-filters): add support for sharing preselected filters * abc * add serialization
This commit is contained in:
parent
ab7f31fd85
commit
4630abb5a8
|
|
@ -34,35 +34,64 @@ describe('getChartIdsFromLayout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode filters', () => {
|
it('should encode filters', () => {
|
||||||
const url = getDashboardUrl('path', filters);
|
const url = getDashboardUrl({ pathname: 'path', filters });
|
||||||
expect(url).toBe(
|
expect(url).toBe(
|
||||||
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
|
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode filters with hash', () => {
|
it('should encode filters with hash', () => {
|
||||||
const urlWithHash = getDashboardUrl('path', filters, 'iamhashtag');
|
const urlWithHash = getDashboardUrl({
|
||||||
|
pathname: 'path',
|
||||||
|
filters,
|
||||||
|
hash: 'iamhashtag',
|
||||||
|
});
|
||||||
expect(urlWithHash).toBe(
|
expect(urlWithHash).toBe(
|
||||||
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D#iamhashtag',
|
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D#iamhashtag',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode filters with standalone', () => {
|
it('should encode filters with standalone', () => {
|
||||||
const urlWithStandalone = getDashboardUrl(
|
const urlWithStandalone = getDashboardUrl({
|
||||||
'path',
|
pathname: 'path',
|
||||||
filters,
|
filters,
|
||||||
'',
|
standalone: DashboardStandaloneMode.HIDE_NAV,
|
||||||
DashboardStandaloneMode.HIDE_NAV,
|
});
|
||||||
);
|
|
||||||
expect(urlWithStandalone).toBe(
|
expect(urlWithStandalone).toBe(
|
||||||
`path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D&standalone=${DashboardStandaloneMode.HIDE_NAV}`,
|
`path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D&standalone=${DashboardStandaloneMode.HIDE_NAV}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode filters with missing standalone', () => {
|
it('should encode filters with missing standalone', () => {
|
||||||
const urlWithStandalone = getDashboardUrl('path', filters, '', null);
|
const urlWithStandalone = getDashboardUrl({
|
||||||
|
pathname: 'path',
|
||||||
|
filters,
|
||||||
|
standalone: null,
|
||||||
|
});
|
||||||
expect(urlWithStandalone).toBe(
|
expect(urlWithStandalone).toBe(
|
||||||
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
|
'path?preselect_filters=%7B%2235%22%3A%7B%22key%22%3A%5B%22value%22%5D%7D%7D',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should encode native filters', () => {
|
||||||
|
const urlWithNativeFilters = getDashboardUrl({
|
||||||
|
pathname: 'path',
|
||||||
|
dataMask: {
|
||||||
|
'NATIVE_FILTER-foo123': {
|
||||||
|
filterState: {
|
||||||
|
label: 'custom label',
|
||||||
|
value: ['a', 'b'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'NATIVE_FILTER-bar456': {
|
||||||
|
filterState: {
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(urlWithNativeFilters).toBe(
|
||||||
|
'path?preselect_filters=%7B%7D&native_filters=%28NATIVE_FILTER-bar456%3A%21n%2CNATIVE_FILTER-foo123%3A%21%28a%2Cb%29%29',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,11 @@ class AnchorLink extends React.PureComponent {
|
||||||
<span className="anchor-link-container" id={anchorLinkId}>
|
<span className="anchor-link-container" id={anchorLinkId}>
|
||||||
{showShortLinkButton && (
|
{showShortLinkButton && (
|
||||||
<URLShortLinkButton
|
<URLShortLinkButton
|
||||||
url={getDashboardUrl(
|
url={getDashboardUrl({
|
||||||
window.location.pathname,
|
pathname: window.location.pathname,
|
||||||
filters,
|
filters,
|
||||||
anchorLinkId,
|
hash: anchorLinkId,
|
||||||
)}
|
})}
|
||||||
emailSubject={t('Superset chart')}
|
emailSubject={t('Superset chart')}
|
||||||
emailContent={t('Check out this chart in dashboard:')}
|
emailContent={t('Check out this chart in dashboard:')}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ export const URL_PARAMS = {
|
||||||
name: 'preselect_filters',
|
name: 'preselect_filters',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
},
|
},
|
||||||
|
nativeFilters: {
|
||||||
|
name: 'native_filters',
|
||||||
|
type: 'rison',
|
||||||
|
},
|
||||||
|
filterSet: {
|
||||||
|
name: 'filter_set',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
showFilters: {
|
showFilters: {
|
||||||
name: 'show_filters',
|
name: 'show_filters',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,7 @@ export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => (
|
||||||
dashboardFilters,
|
dashboardFilters,
|
||||||
nativeFilters,
|
nativeFilters,
|
||||||
dashboardState: {
|
dashboardState: {
|
||||||
|
preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters),
|
||||||
sliceIds: Array.from(sliceIds),
|
sliceIds: Array.from(sliceIds),
|
||||||
directPathToChild,
|
directPathToChild,
|
||||||
directPathLastUpdated: Date.now(),
|
directPathLastUpdated: Date.now(),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { JsonObject } from '@superset-ui/core';
|
||||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
|
|
@ -37,6 +38,9 @@ export const useNativeFilters = () => {
|
||||||
const showNativeFilters = useSelector<RootState, boolean>(
|
const showNativeFilters = useSelector<RootState, boolean>(
|
||||||
state => state.dashboardInfo.metadata?.show_native_filters,
|
state => state.dashboardInfo.metadata?.show_native_filters,
|
||||||
);
|
);
|
||||||
|
const preselectNativeFilters = useSelector<RootState, JsonObject>(
|
||||||
|
state => state.dashboardState?.preselectNativeFilters || {},
|
||||||
|
);
|
||||||
const canEdit = useSelector<RootState, boolean>(
|
const canEdit = useSelector<RootState, boolean>(
|
||||||
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
||||||
);
|
);
|
||||||
|
|
@ -50,7 +54,7 @@ export const useNativeFilters = () => {
|
||||||
(canEdit || (!canEdit && filterValues.length !== 0));
|
(canEdit || (!canEdit && filterValues.length !== 0));
|
||||||
|
|
||||||
const requiredFirstFilter = filterValues.filter(
|
const requiredFirstFilter = filterValues.filter(
|
||||||
({ requiredFirst }) => requiredFirst,
|
filter => filter.requiredFirst || preselectNativeFilters[filter.id],
|
||||||
);
|
);
|
||||||
const dataMask = useNativeFiltersDataMask();
|
const dataMask = useNativeFiltersDataMask();
|
||||||
const showDashboard =
|
const showDashboard =
|
||||||
|
|
@ -89,5 +93,6 @@ export const useNativeFilters = () => {
|
||||||
dashboardFiltersOpen,
|
dashboardFiltersOpen,
|
||||||
toggleDashboardFiltersOpen,
|
toggleDashboardFiltersOpen,
|
||||||
nativeFiltersEnabled,
|
nativeFiltersEnabled,
|
||||||
|
preselectNativeFilters,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ const propTypes = {
|
||||||
dashboardInfo: PropTypes.object.isRequired,
|
dashboardInfo: PropTypes.object.isRequired,
|
||||||
dashboardId: PropTypes.number.isRequired,
|
dashboardId: PropTypes.number.isRequired,
|
||||||
dashboardTitle: PropTypes.string.isRequired,
|
dashboardTitle: PropTypes.string.isRequired,
|
||||||
|
dataMask: PropTypes.object.isRequired,
|
||||||
customCss: PropTypes.string.isRequired,
|
customCss: PropTypes.string.isRequired,
|
||||||
colorNamespace: PropTypes.string,
|
colorNamespace: PropTypes.string,
|
||||||
colorScheme: PropTypes.string,
|
colorScheme: PropTypes.string,
|
||||||
|
|
@ -164,12 +165,13 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MENU_KEYS.TOGGLE_FULLSCREEN: {
|
case MENU_KEYS.TOGGLE_FULLSCREEN: {
|
||||||
const url = getDashboardUrl(
|
const url = getDashboardUrl({
|
||||||
window.location.pathname,
|
dataMask: this.props.dataMask,
|
||||||
getActiveFilters(),
|
pathname: window.location.pathname,
|
||||||
window.location.hash,
|
filters: getActiveFilters(),
|
||||||
!getUrlParam(URL_PARAMS.standalone),
|
hash: window.location.hash,
|
||||||
);
|
standalone: !getUrlParam(URL_PARAMS.standalone),
|
||||||
|
});
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +185,7 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
dashboardTitle,
|
dashboardTitle,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
dashboardInfo,
|
dashboardInfo,
|
||||||
|
dataMask,
|
||||||
refreshFrequency,
|
refreshFrequency,
|
||||||
shouldPersistRefreshFrequency,
|
shouldPersistRefreshFrequency,
|
||||||
editMode,
|
editMode,
|
||||||
|
|
@ -206,11 +209,13 @@ class HeaderActionsDropdown extends React.PureComponent {
|
||||||
const emailTitle = t('Superset dashboard');
|
const emailTitle = t('Superset dashboard');
|
||||||
const emailSubject = `${emailTitle} ${dashboardTitle}`;
|
const emailSubject = `${emailTitle} ${dashboardTitle}`;
|
||||||
const emailBody = t('Check out this dashboard: ');
|
const emailBody = t('Check out this dashboard: ');
|
||||||
const url = getDashboardUrl(
|
|
||||||
window.location.pathname,
|
const url = getDashboardUrl({
|
||||||
getActiveFilters(),
|
dataMask,
|
||||||
window.location.hash,
|
pathname: window.location.pathname,
|
||||||
);
|
filters: getActiveFilters(),
|
||||||
|
hash: window.location.hash,
|
||||||
|
});
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu
|
<Menu
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ const propTypes = {
|
||||||
addWarningToast: PropTypes.func.isRequired,
|
addWarningToast: PropTypes.func.isRequired,
|
||||||
dashboardInfo: PropTypes.object.isRequired,
|
dashboardInfo: PropTypes.object.isRequired,
|
||||||
dashboardTitle: PropTypes.string.isRequired,
|
dashboardTitle: PropTypes.string.isRequired,
|
||||||
|
dataMask: PropTypes.object.isRequired,
|
||||||
charts: PropTypes.objectOf(chartPropShape).isRequired,
|
charts: PropTypes.objectOf(chartPropShape).isRequired,
|
||||||
layout: PropTypes.object.isRequired,
|
layout: PropTypes.object.isRequired,
|
||||||
expandedSlices: PropTypes.object.isRequired,
|
expandedSlices: PropTypes.object.isRequired,
|
||||||
|
|
@ -353,6 +354,7 @@ class Header extends React.PureComponent {
|
||||||
expandedSlices,
|
expandedSlices,
|
||||||
customCss,
|
customCss,
|
||||||
colorNamespace,
|
colorNamespace,
|
||||||
|
dataMask,
|
||||||
setColorSchemeAndUnsavedChanges,
|
setColorSchemeAndUnsavedChanges,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
onUndo,
|
onUndo,
|
||||||
|
|
@ -526,6 +528,7 @@ class Header extends React.PureComponent {
|
||||||
dashboardId={dashboardInfo.id}
|
dashboardId={dashboardInfo.id}
|
||||||
dashboardTitle={dashboardTitle}
|
dashboardTitle={dashboardTitle}
|
||||||
dashboardInfo={dashboardInfo}
|
dashboardInfo={dashboardInfo}
|
||||||
|
dataMask={dataMask}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
expandedSlices={expandedSlices}
|
expandedSlices={expandedSlices}
|
||||||
customCss={customCss}
|
customCss={customCss}
|
||||||
|
|
|
||||||
|
|
@ -293,11 +293,11 @@ class SliceHeaderControls extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
{supersetCanShare && (
|
{supersetCanShare && (
|
||||||
<ShareMenuItems
|
<ShareMenuItems
|
||||||
url={getDashboardUrl(
|
url={getDashboardUrl({
|
||||||
window.location.pathname,
|
pathname: window.location.pathname,
|
||||||
getActiveFilters(),
|
filters: getActiveFilters(),
|
||||||
componentId,
|
hash: componentId,
|
||||||
)}
|
})}
|
||||||
copyMenuItemTitle={t('Copy chart URL')}
|
copyMenuItemTitle={t('Copy chart URL')}
|
||||||
emailMenuItemTitle={t('Share chart by email')}
|
emailMenuItemTitle={t('Share chart by email')}
|
||||||
emailSubject={t('Superset chart')}
|
emailSubject={t('Superset chart')}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import { ClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
import { FilterProps } from './types';
|
import { FilterProps } from './types';
|
||||||
import { getFormData } from '../../utils';
|
import { getFormData } from '../../utils';
|
||||||
import { useCascadingFilters } from './state';
|
import { useCascadingFilters } from './state';
|
||||||
|
import { usePreselectNativeFilter } from '../../state';
|
||||||
|
|
||||||
const HEIGHT = 32;
|
const HEIGHT = 32;
|
||||||
|
|
||||||
|
|
@ -80,7 +81,8 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||||
const { name: groupby } = column;
|
const { name: groupby } = column;
|
||||||
const hasDataSource = !!datasetId;
|
const hasDataSource = !!datasetId;
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(hasDataSource);
|
const [isLoading, setIsLoading] = useState<boolean>(hasDataSource);
|
||||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(true);
|
const [isRefreshing, setIsRefreshing] = useState(true);
|
||||||
|
const preselection = usePreselectNativeFilter(filter.id);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -195,6 +197,10 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const filterState = { ...filter.dataMask?.filterState };
|
||||||
|
if (filterState.value === undefined && preselection) {
|
||||||
|
filterState.value = preselection;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv data-test="form-item-value">
|
<StyledDiv data-test="form-item-value">
|
||||||
|
|
@ -209,7 +215,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||||
queriesData={hasDataSource ? state : [{ data: [{}] }]}
|
queriesData={hasDataSource ? state : [{ data: [{}] }]}
|
||||||
chartType={filterType}
|
chartType={filterType}
|
||||||
behaviors={[Behavior.NATIVE_FILTER]}
|
behaviors={[Behavior.NATIVE_FILTER]}
|
||||||
filterState={{ ...filter.dataMask?.filterState }}
|
filterState={filterState}
|
||||||
ownState={filter.dataMask?.ownState}
|
ownState={filter.dataMask?.ownState}
|
||||||
enableNoResults={metadata?.enableNoResults}
|
enableNoResults={metadata?.enableNoResults}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { testWithId } from 'src/utils/testUtils';
|
||||||
import { Filter } from 'src/dashboard/components/nativeFilters/types';
|
import { Filter } from 'src/dashboard/components/nativeFilters/types';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import { getInitialDataMask } from 'src/dataMask/reducer';
|
import { getInitialDataMask } from 'src/dataMask/reducer';
|
||||||
|
import { areObjectsEqual } from 'src/reduxUtils';
|
||||||
import { checkIsApplyDisabled, TabIds } from './utils';
|
import { checkIsApplyDisabled, TabIds } from './utils';
|
||||||
import FilterSets from './FilterSets';
|
import FilterSets from './FilterSets';
|
||||||
import {
|
import {
|
||||||
|
|
@ -45,6 +46,7 @@ import {
|
||||||
import EditSection from './FilterSets/EditSection';
|
import EditSection from './FilterSets/EditSection';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import FilterControls from './FilterControls/FilterControls';
|
import FilterControls from './FilterControls/FilterControls';
|
||||||
|
import { usePreselectNativeFilters } from '../state';
|
||||||
|
|
||||||
export const FILTER_BAR_TEST_ID = 'filter-bar';
|
export const FILTER_BAR_TEST_ID = 'filter-bar';
|
||||||
export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);
|
export const getFilterBarTestId = testWithId(FILTER_BAR_TEST_ID);
|
||||||
|
|
@ -156,6 +158,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||||
const filterValues = Object.values<Filter>(filters);
|
const filterValues = Object.values<Filter>(filters);
|
||||||
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
|
const dataMaskApplied: DataMaskStateWithId = useNativeFiltersDataMask();
|
||||||
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
|
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
|
||||||
|
const preselectNativeFilters = usePreselectNativeFilters();
|
||||||
|
const [initializedFilters, setInitializedFilters] = useState<any[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDataMaskSelected(() => dataMaskApplied);
|
setDataMaskSelected(() => dataMaskApplied);
|
||||||
|
|
@ -185,8 +189,33 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||||
) => {
|
) => {
|
||||||
setIsFilterSetChanged(tab !== TabIds.AllFilters);
|
setIsFilterSetChanged(tab !== TabIds.AllFilters);
|
||||||
setDataMaskSelected(draft => {
|
setDataMaskSelected(draft => {
|
||||||
// force instant updating on initialization for filters with `requiredFirst` is true or instant filters
|
// check if a filter has preselect filters
|
||||||
if (
|
if (
|
||||||
|
preselectNativeFilters?.[filter.id] !== undefined &&
|
||||||
|
!initializedFilters.includes(filter.id)
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* since preselect filters don't have extraFormData, they need to iterate
|
||||||
|
* a few times to populate the full state necessary for proper filtering.
|
||||||
|
* Once both filterState and extraFormData are identical, we can coclude
|
||||||
|
* that the filter has been fully initialized.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
areObjectsEqual(
|
||||||
|
dataMask.filterState,
|
||||||
|
dataMaskSelected[filter.id]?.filterState,
|
||||||
|
) &&
|
||||||
|
areObjectsEqual(
|
||||||
|
dataMask.extraFormData,
|
||||||
|
dataMaskSelected[filter.id]?.extraFormData,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setInitializedFilters(prevState => [...prevState, filter.id]);
|
||||||
|
}
|
||||||
|
dispatch(updateDataMask(filter.id, dataMask));
|
||||||
|
}
|
||||||
|
// force instant updating on initialization for filters with `requiredFirst` is true or instant filters
|
||||||
|
else if (
|
||||||
(dataMaskSelected[filter.id] && filter.isInstant) ||
|
(dataMaskSelected[filter.id] && filter.isInstant) ||
|
||||||
// filterState.value === undefined - means that value not initialized
|
// filterState.value === undefined - means that value not initialized
|
||||||
(dataMask.filterState?.value !== undefined &&
|
(dataMask.filterState?.value !== undefined &&
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ChartsState, RootState } from 'src/dashboard/types';
|
import { ChartsState, RootState } from 'src/dashboard/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>(
|
||||||
|
|
@ -37,7 +38,20 @@ export const useFilterSets = () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useFilters = () =>
|
export const useFilters = () =>
|
||||||
useSelector<any, Filters>(state => state.nativeFilters.filters);
|
useSelector<any, Filters>(state => {
|
||||||
|
const preselectNativeFilters =
|
||||||
|
state.dashboardState?.preselectNativeFilters || {};
|
||||||
|
return Object.entries(state.nativeFilters.filters).reduce(
|
||||||
|
(acc, [filterId, filter]: [string, Filter]) => ({
|
||||||
|
...acc,
|
||||||
|
[filterId]: {
|
||||||
|
...filter,
|
||||||
|
preselect: preselectNativeFilters[filterId],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Filters,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const useNativeFiltersDataMask = () => {
|
export const useNativeFiltersDataMask = () => {
|
||||||
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
const dataMask = useSelector<RootState, DataMaskStateWithId>(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { JsonObject } from '@superset-ui/core';
|
||||||
import { Filter, FilterConfiguration } from './types';
|
import { Filter, FilterConfiguration } from './types';
|
||||||
import { ActiveTabs, DashboardLayout, RootState } from '../../types';
|
import { ActiveTabs, DashboardLayout, RootState } from '../../types';
|
||||||
import { TAB_TYPE } from '../../util/componentTypes';
|
import { TAB_TYPE } from '../../util/componentTypes';
|
||||||
|
|
@ -124,3 +125,13 @@ export function useSelectFiltersInScope(cascadeFilters: CascadeFilter[]) {
|
||||||
return [filtersInScope, filtersOutOfScope];
|
return [filtersInScope, filtersOutOfScope];
|
||||||
}, [cascadeFilters, dashboardHasTabs, isFilterInScope]);
|
}, [cascadeFilters, dashboardHasTabs, isFilterInScope]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePreselectNativeFilters(): JsonObject | undefined {
|
||||||
|
return useSelector<RootState, any>(
|
||||||
|
state => state.dashboardState?.preselectNativeFilters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreselectNativeFilter(id: string): JsonObject | undefined {
|
||||||
|
return usePreselectNativeFilters()?.[id];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { updateDataMask } from 'src/dataMask/actions';
|
||||||
import DashboardHeader from '../components/Header';
|
import DashboardHeader from '../components/Header';
|
||||||
import isDashboardLoading from '../util/isDashboardLoading';
|
import isDashboardLoading from '../util/isDashboardLoading';
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ function mapStateToProps({
|
||||||
dashboardState,
|
dashboardState,
|
||||||
dashboardInfo,
|
dashboardInfo,
|
||||||
charts,
|
charts,
|
||||||
|
dataMask,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
dashboardInfo,
|
dashboardInfo,
|
||||||
|
|
@ -77,6 +79,7 @@ function mapStateToProps({
|
||||||
colorNamespace: dashboardState.colorNamespace,
|
colorNamespace: dashboardState.colorNamespace,
|
||||||
colorScheme: dashboardState.colorScheme,
|
colorScheme: dashboardState.colorScheme,
|
||||||
charts,
|
charts,
|
||||||
|
dataMask,
|
||||||
userId: dashboardInfo.userId,
|
userId: dashboardInfo.userId,
|
||||||
isStarred: !!dashboardState.isStarred,
|
isStarred: !!dashboardState.isStarred,
|
||||||
isPublished: !!dashboardState.isPublished,
|
isPublished: !!dashboardState.isPublished,
|
||||||
|
|
@ -118,6 +121,7 @@ function mapDispatchToProps(dispatch) {
|
||||||
setRefreshFrequency,
|
setRefreshFrequency,
|
||||||
dashboardInfoChanged,
|
dashboardInfoChanged,
|
||||||
dashboardTitleChanged,
|
dashboardTitleChanged,
|
||||||
|
updateDataMask,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export type ActiveTabs = string[];
|
||||||
export type DashboardLayout = { [key: string]: LayoutItem };
|
export type DashboardLayout = { [key: string]: LayoutItem };
|
||||||
export type DashboardLayoutState = { present: DashboardLayout };
|
export type DashboardLayoutState = { present: DashboardLayout };
|
||||||
export type DashboardState = {
|
export type DashboardState = {
|
||||||
|
preselectNativeFilters?: JsonObject;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
directPathToChild: string[];
|
directPathToChild: string[];
|
||||||
activeTabs: ActiveTabs;
|
activeTabs: ActiveTabs;
|
||||||
|
|
@ -63,7 +64,10 @@ export type DashboardInfo = {
|
||||||
};
|
};
|
||||||
userId: string;
|
userId: string;
|
||||||
dash_edit_perm: boolean;
|
dash_edit_perm: boolean;
|
||||||
metadata: { show_native_filters: boolean; chart_configuration: JsonObject };
|
metadata: {
|
||||||
|
show_native_filters: boolean;
|
||||||
|
chart_configuration: JsonObject;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChartsState = { [key: string]: Chart };
|
export type ChartsState = { [key: string]: Chart };
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ let allComponents = {};
|
||||||
|
|
||||||
// output: { [id_column]: { values, scope } }
|
// output: { [id_column]: { values, scope } }
|
||||||
export function getActiveFilters() {
|
export function getActiveFilters() {
|
||||||
return activeFilters;
|
return {
|
||||||
|
...activeFilters,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently filter_box is a chart,
|
// currently filter_box is a chart,
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,25 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import rison from 'rison';
|
||||||
|
import { JsonObject } from '@superset-ui/core';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import serializeActiveFilterValues from './serializeActiveFilterValues';
|
import serializeActiveFilterValues from './serializeActiveFilterValues';
|
||||||
|
import { DataMaskState } from '../../dataMask/types';
|
||||||
|
|
||||||
export default function getDashboardUrl(
|
export default function getDashboardUrl({
|
||||||
pathname: string,
|
pathname,
|
||||||
filters = {},
|
filters = {},
|
||||||
hash = '',
|
hash = '',
|
||||||
standalone?: number | null,
|
standalone,
|
||||||
) {
|
dataMask,
|
||||||
|
}: {
|
||||||
|
pathname: string;
|
||||||
|
filters: JsonObject;
|
||||||
|
hash: string;
|
||||||
|
standalone?: number | null;
|
||||||
|
dataMask?: DataMaskState;
|
||||||
|
}) {
|
||||||
const newSearchParams = new URLSearchParams();
|
const newSearchParams = new URLSearchParams();
|
||||||
|
|
||||||
// convert flattened { [id_column]: values } object
|
// convert flattened { [id_column]: values } object
|
||||||
|
|
@ -38,7 +48,23 @@ export default function getDashboardUrl(
|
||||||
newSearchParams.set(URL_PARAMS.standalone.name, standalone.toString());
|
newSearchParams.set(URL_PARAMS.standalone.name, standalone.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashSection = hash ? `#${hash}` : '';
|
if (dataMask) {
|
||||||
|
const filterStates = Object.entries(dataMask).reduce(
|
||||||
|
(agg, [key, value]) => {
|
||||||
|
const filterState = value?.filterState?.value;
|
||||||
|
return {
|
||||||
|
...agg,
|
||||||
|
[key]: filterState || null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
newSearchParams.set(
|
||||||
|
URL_PARAMS.nativeFilters.name,
|
||||||
|
rison.encode(filterStates),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashSection = hash ? `#${hash}` : '';
|
||||||
return `${pathname}?${newSearchParams.toString()}${hashSection}`;
|
return `${pathname}?${newSearchParams.toString()}${hashSection}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,17 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
import rison from 'rison';
|
||||||
import { getClientErrorObject } from './getClientErrorObject';
|
import { getClientErrorObject } from './getClientErrorObject';
|
||||||
import { URL_PARAMS } from '../constants';
|
import { URL_PARAMS } from '../constants';
|
||||||
|
|
||||||
export type UrlParamType = 'string' | 'number' | 'boolean' | 'object';
|
export type UrlParamType = 'string' | 'number' | 'boolean' | 'object' | 'rison';
|
||||||
export type UrlParam = typeof URL_PARAMS[keyof typeof URL_PARAMS];
|
export type UrlParam = typeof URL_PARAMS[keyof typeof URL_PARAMS];
|
||||||
export function getUrlParam(param: UrlParam & { type: 'string' }): string;
|
export function getUrlParam(param: UrlParam & { type: 'string' }): string;
|
||||||
export function getUrlParam(param: UrlParam & { type: 'number' }): number;
|
export function getUrlParam(param: UrlParam & { type: 'number' }): number;
|
||||||
export function getUrlParam(param: UrlParam & { type: 'boolean' }): boolean;
|
export function getUrlParam(param: UrlParam & { type: 'boolean' }): boolean;
|
||||||
export function getUrlParam(param: UrlParam & { type: 'object' }): object;
|
export function getUrlParam(param: UrlParam & { type: 'object' }): object;
|
||||||
|
export function getUrlParam(param: UrlParam & { type: 'rison' }): object;
|
||||||
export function getUrlParam({ name, type }: UrlParam): unknown {
|
export function getUrlParam({ name, type }: UrlParam): unknown {
|
||||||
const urlParam = new URLSearchParams(window.location.search).get(name);
|
const urlParam = new URLSearchParams(window.location.search).get(name);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -53,6 +55,15 @@ export function getUrlParam({ name, type }: UrlParam): unknown {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return urlParam !== 'false' && urlParam !== '0';
|
return urlParam !== 'false' && urlParam !== '0';
|
||||||
|
case 'rison':
|
||||||
|
if (!urlParam) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return rison.decode(urlParam);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return urlParam;
|
return urlParam;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue