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:
Ville Brofeldt 2021-06-29 18:57:49 +03:00 committed by GitHub
parent ab7f31fd85
commit 4630abb5a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 199 additions and 41 deletions

View File

@ -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',
);
});
}); });

View File

@ -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}

View File

@ -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',

View File

@ -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(),

View File

@ -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,
}; };
}; };

View File

@ -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

View File

@ -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}

View File

@ -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')}

View File

@ -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}

View File

@ -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 &&

View File

@ -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>(

View File

@ -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];
}

View File

@ -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,
); );

View File

@ -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 };

View File

@ -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,

View File

@ -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}`;
} }

View File

@ -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;
} }