fix: replace datamask with key from new key value api (#17680)

* afirst stage to ccheck to get initial datamask

* clean up code and update typescript

* remove consoles

* fix ts and update copy dashboard url

* use key when one doesn't exists

* lint clean up

* fix errors

* add suggested changes

* remove line

* add tests and add changes for copydashboard

* fix lint

* fix lint

* fix lint

* Update superset-frontend/src/dashboard/components/Header/index.jsx

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>

* add timeout

* fix test

* fix test, add qs to cypress and add suggestions

* add suggestions

* fix lint

* more suggested changes for backwards comapat

* fix lint

* cleanup naming and add qs parse to tests

* Update superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>

* Update superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>

* more changes and fix lint

* remove nativefiler param

* fix path

* remove con

* simplify logic

Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
Phillip Kelley-Dotson 2021-12-21 09:08:48 -08:00 committed by GitHub
parent 2c3f39f3f2
commit cfd851aa13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 304 additions and 78 deletions

View File

@ -0,0 +1,54 @@
/**
* 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 qs from 'querystringify';
import {
WORLD_HEALTH_DASHBOARD,
WORLD_HEALTH_CHARTS,
waitForChartLoad,
} from './dashboard.helper';
interface QueryString {
native_filters_key: string;
}
describe('nativefiler url param key', () => {
// const urlParams = { param1: '123', param2: 'abc' };
before(() => {
cy.login();
cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
});
beforeEach(() => {
cy.login();
});
let initialFilterKey: string;
it('should have cachekey in nativefilter param', () => {
cy.location().then(loc => {
const queryParams = qs.parse(loc.search) as QueryString;
expect(typeof queryParams.native_filters_key).eq('string');
});
});
it('should have different key when page reloads', () => {
cy.location().then(loc => {
const queryParams = qs.parse(loc.search) as QueryString;
expect(queryParams.native_filters_key).not.equal(initialFilterKey);
});
});
});

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import qs from 'querystring';
import { dashboardView, nativeFilters } from 'cypress/support/directories'; import { dashboardView, nativeFilters } from 'cypress/support/directories';
import { testItems } from './dashboard.helper'; import { testItems } from './dashboard.helper';
import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper'; import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper';
@ -93,6 +94,15 @@ describe('Nativefilters Sanity test', () => {
cy.get(nativeFilters.modal.container).should('be.visible'); cy.get(nativeFilters.modal.container).should('be.visible');
}); });
it('User can add a new native filter', () => { it('User can add a new native filter', () => {
let filterKey: string;
const removeFirstChar = (search: string) =>
search.split('').slice(1, search.length).join('');
cy.wait(3000);
cy.location().then(loc => {
const queryParams = qs.parse(removeFirstChar(loc.search));
filterKey = queryParams.native_filters_key as string;
expect(typeof filterKey).eq('string');
});
cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true }); cy.get(nativeFilters.filterFromDashboardView.expand).click({ force: true });
cy.get(nativeFilters.createFilterButton).should('be.visible').click(); cy.get(nativeFilters.createFilterButton).should('be.visible').click();
cy.get(nativeFilters.modal.container) cy.get(nativeFilters.modal.container)
@ -115,7 +125,7 @@ describe('Nativefilters Sanity test', () => {
cy.wait(5000); cy.wait(5000);
cy.get(nativeFilters.filtersPanel.filterInfoInput) cy.get(nativeFilters.filtersPanel.filterInfoInput)
.last() .last()
.should('be.visible') .should('be.visible', { timeout: 30000 })
.click({ force: true }); .click({ force: true });
cy.get(nativeFilters.filtersPanel.filterInfoInput) cy.get(nativeFilters.filtersPanel.filterInfoInput)
.last() .last()
@ -128,6 +138,13 @@ describe('Nativefilters Sanity test', () => {
.contains('Save') .contains('Save')
.should('be.visible') .should('be.visible')
.click(); .click();
cy.wait(3000);
cy.location().then(loc => {
const queryParams = qs.parse(removeFirstChar(loc.search));
const newfilterKey = queryParams.native_filters_key;
expect(newfilterKey).not.eq(filterKey);
});
cy.wait(3000);
cy.get(nativeFilters.modal.container).should('not.exist'); cy.get(nativeFilters.modal.container).should('not.exist');
}); });
it('User can delete a native filter', () => { it('User can delete a native filter', () => {

View File

@ -11,11 +11,13 @@
"dependencies": { "dependencies": {
"@cypress/code-coverage": "^3.9.11", "@cypress/code-coverage": "^3.9.11",
"@superset-ui/core": "^0.18.8", "@superset-ui/core": "^0.18.8",
"querystringify": "^2.2.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"rison": "^0.1.1", "rison": "^0.1.1",
"shortid": "^2.2.15" "shortid": "^2.2.15"
}, },
"devDependencies": { "devDependencies": {
"@types/querystringify": "^2.0.0",
"cypress": "^7.0.0", "cypress": "^7.0.0",
"eslint-plugin-cypress": "^2.12.1" "eslint-plugin-cypress": "^2.12.1"
} }
@ -1413,6 +1415,12 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
}, },
"node_modules/@types/querystringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/querystringify/-/querystringify-2.0.0.tgz",
"integrity": "sha512-9WgEGTevECrXJC2LSWPqiPYWq8BRmeaOyZn47js/3V6UF0PWtcVfvvR43YjeO8BzBsthTz98jMczujOwTw+WYg==",
"dev": true
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "17.0.3", "version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz",
@ -6682,6 +6690,11 @@
"node": ">=0.4.x" "node": ">=0.4.x"
} }
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -9741,6 +9754,12 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
}, },
"@types/querystringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/querystringify/-/querystringify-2.0.0.tgz",
"integrity": "sha512-9WgEGTevECrXJC2LSWPqiPYWq8BRmeaOyZn47js/3V6UF0PWtcVfvvR43YjeO8BzBsthTz98jMczujOwTw+WYg==",
"dev": true
},
"@types/react": { "@types/react": {
"version": "17.0.3", "version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz",
@ -13992,6 +14011,11 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
}, },
"querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"queue-microtask": { "queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -12,11 +12,13 @@
"dependencies": { "dependencies": {
"@cypress/code-coverage": "^3.9.11", "@cypress/code-coverage": "^3.9.11",
"@superset-ui/core": "^0.18.8", "@superset-ui/core": "^0.18.8",
"querystringify": "^2.2.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"rison": "^0.1.1", "rison": "^0.1.1",
"shortid": "^2.2.15" "shortid": "^2.2.15"
}, },
"devDependencies": { "devDependencies": {
"@types/querystringify": "^2.0.0",
"cypress": "^7.0.0", "cypress": "^7.0.0",
"eslint-plugin-cypress": "^2.12.1" "eslint-plugin-cypress": "^2.12.1"
}, },

View File

@ -73,25 +73,21 @@ describe('getChartIdsFromLayout', () => {
); );
}); });
it('should encode native filters', () => { it('should process native filters key', () => {
const windowSpy = jest.spyOn(window, 'window', 'get');
windowSpy.mockImplementation(() => ({
location: {
origin: 'https://localhost',
search:
'?preselect_filters=%7B%7D&native_filters_key=024380498jdkjf-2094838',
},
}));
const urlWithNativeFilters = getDashboardUrl({ const urlWithNativeFilters = getDashboardUrl({
pathname: 'path', pathname: 'path',
dataMask: {
'NATIVE_FILTER-foo123': {
filterState: {
label: 'custom label',
value: ['a', 'b'],
},
},
'NATIVE_FILTER-bar456': {
filterState: {
value: undefined,
},
},
},
}); });
expect(urlWithNativeFilters).toBe( expect(urlWithNativeFilters).toBe(
'path?preselect_filters=%7B%7D&native_filters=%28NATIVE_FILTER-bar456%3A%28filterState%3A%28value%3A%21n%29%29%2CNATIVE_FILTER-foo123%3A%28filterState%3A%28label%3A%27custom+label%27%2Cvalue%3A%21%28a%2Cb%29%29%29%29', 'path?preselect_filters=%7B%7D&native_filters_key=024380498jdkjf-2094838',
); );
}); });
}); });

View File

@ -23,9 +23,9 @@ export function useUrlShortener(url: string): Function {
const [update, setUpdate] = useState(false); const [update, setUpdate] = useState(false);
const [shortUrl, setShortUrl] = useState(''); const [shortUrl, setShortUrl] = useState('');
async function getShortUrl() { async function getShortUrl(urlOverride?: string) {
if (update) { if (update) {
const newShortUrl = await getShortUrlUtil(url); const newShortUrl = await getShortUrlUtil(urlOverride || url);
setShortUrl(newShortUrl); setShortUrl(newShortUrl);
setUpdate(false); setUpdate(false);
return newShortUrl; return newShortUrl;

View File

@ -39,6 +39,10 @@ export const URL_PARAMS = {
name: 'native_filters', name: 'native_filters',
type: 'rison', type: 'rison',
}, },
nativeFiltersKey: {
name: 'native_filters_key',
type: 'string',
},
filterSet: { filterSet: {
name: 'filter_set', name: 'filter_set',
type: 'string', type: 'string',

View File

@ -67,6 +67,7 @@ export const hydrateDashboard =
dashboardData, dashboardData,
chartData, chartData,
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP, filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
dataMaskApplied,
) => ) =>
(dispatch, getState) => { (dispatch, getState) => {
const { user, common } = getState(); const { user, common } = getState();
@ -378,10 +379,11 @@ export const hydrateDashboard =
slice_can_edit: findPermission('can_slice', 'Superset', roles), slice_can_edit: findPermission('can_slice', 'Superset', roles),
common: { common: {
// legacy, please use state.common instead // legacy, please use state.common instead
flash_messages: common.flash_messages, flash_messages: common?.flash_messages,
conf: common.conf, conf: common?.conf,
}, },
}, },
dataMask: dataMaskApplied,
dashboardFilters, dashboardFilters,
nativeFilters, nativeFilters,
dashboardState: { dashboardState: {

View File

@ -235,7 +235,7 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
); );
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
const rootChildId = dashboardRoot.children[0]; const rootChildId = dashboardRoot?.children[0];
const topLevelTabs = const topLevelTabs =
rootChildId !== DASHBOARD_GRID_ID rootChildId !== DASHBOARD_GRID_ID
? dashboardLayout[rootChildId] ? dashboardLayout[rootChildId]

View File

@ -25,7 +25,7 @@ import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponen
export const getRootLevelTabsComponent = (dashboardLayout: DashboardLayout) => { export const getRootLevelTabsComponent = (dashboardLayout: DashboardLayout) => {
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
const rootChildId = dashboardRoot.children[0]; const rootChildId = dashboardRoot?.children[0];
return rootChildId === DASHBOARD_GRID_ID return rootChildId === DASHBOARD_GRID_ID
? dashboardLayout[DASHBOARD_ROOT_ID] ? dashboardLayout[DASHBOARD_ROOT_ID]
: dashboardLayout[rootChildId]; : dashboardLayout[rootChildId];

View File

@ -193,7 +193,6 @@ class HeaderActionsDropdown extends React.PureComponent {
dashboardTitle, dashboardTitle,
dashboardId, dashboardId,
dashboardInfo, dashboardInfo,
dataMask,
refreshFrequency, refreshFrequency,
shouldPersistRefreshFrequency, shouldPersistRefreshFrequency,
editMode, editMode,
@ -220,7 +219,6 @@ class HeaderActionsDropdown extends React.PureComponent {
const emailBody = t('Check out this dashboard: '); const emailBody = t('Check out this dashboard: ');
const url = getDashboardUrl({ const url = getDashboardUrl({
dataMask,
pathname: window.location.pathname, pathname: window.location.pathname,
filters: getActiveFilters(), filters: getActiveFilters(),
hash: window.location.hash, hash: window.location.hash,
@ -266,6 +264,7 @@ class HeaderActionsDropdown extends React.PureComponent {
emailBody={emailBody} emailBody={emailBody}
addSuccessToast={addSuccessToast} addSuccessToast={addSuccessToast}
addDangerToast={addDangerToast} addDangerToast={addDangerToast}
dashboardId={dashboardId}
/> />
)} )}
<Menu.Item <Menu.Item

View File

@ -173,13 +173,15 @@ class Header extends React.PureComponent {
this.startPeriodicRender(refreshFrequency * 1000); this.startPeriodicRender(refreshFrequency * 1000);
if (this.canAddReports()) { if (this.canAddReports()) {
// this is in case there is an anonymous user. // this is in case there is an anonymous user.
this.props.fetchUISpecificReport( if (Object.entries(dashboardInfo).length) {
user.userId, this.props.fetchUISpecificReport(
'dashboard_id', user.userId,
'dashboards', 'dashboard_id',
dashboardInfo.id, 'dashboards',
user.email, dashboardInfo.id,
); user.email,
);
}
} }
} }
@ -211,11 +213,11 @@ class Header extends React.PureComponent {
) { ) {
// this is in case there is an anonymous user. // this is in case there is an anonymous user.
this.props.fetchUISpecificReport( this.props.fetchUISpecificReport(
user.userId, user?.userId,
'dashboard_id', 'dashboard_id',
'dashboards', 'dashboards',
nextProps.dashboardInfo.id, nextProps?.dashboardInfo?.id,
user.email, user?.email,
); );
} }
} }
@ -488,10 +490,10 @@ class Header extends React.PureComponent {
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING; 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;
const refreshWarning = const refreshWarning =
dashboardInfo.common.conf dashboardInfo.common?.conf
.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE; ?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE;
const handleOnPropertiesChange = updates => { const handleOnPropertiesChange = updates => {
const { dashboardInfoChanged, dashboardTitleChanged } = this.props; const { dashboardInfoChanged, dashboardTitleChanged } = this.props;
@ -529,7 +531,7 @@ class Header extends React.PureComponent {
canEdit={userCanEdit} canEdit={userCanEdit}
canSave={userCanSaveAs} canSave={userCanSaveAs}
/> />
{user?.userId && ( {user?.userId && dashboardInfo?.id && (
<FaveStar <FaveStar
itemId={dashboardInfo.id} itemId={dashboardInfo.id}
fetchFaveStar={this.props.fetchFaveStar} fetchFaveStar={this.props.fetchFaveStar}

View File

@ -27,14 +27,16 @@ import ShareMenuItems from '.';
const spy = jest.spyOn(copyTextToClipboard, 'default'); const spy = jest.spyOn(copyTextToClipboard, 'default');
const DASHBOARD_ID = '26';
const createProps = () => ({ const createProps = () => ({
addDangerToast: jest.fn(), addDangerToast: jest.fn(),
addSuccessToast: jest.fn(), addSuccessToast: jest.fn(),
url: '/superset/dashboard/26/?preselect_filters=%7B%7D', url: `/superset/dashboard/${DASHBOARD_ID}/?preselect_filters=%7B%7D`,
copyMenuItemTitle: 'Copy dashboard URL', copyMenuItemTitle: 'Copy dashboard URL',
emailMenuItemTitle: 'Share dashboard by email', emailMenuItemTitle: 'Share dashboard by email',
emailSubject: 'Superset dashboard COVID Vaccine Dashboard', emailSubject: 'Superset dashboard COVID Vaccine Dashboard',
emailBody: 'Check out this dashboard: ', emailBody: 'Check out this dashboard: ',
dashboardId: DASHBOARD_ID,
}); });
const { location } = window; const { location } = window;

View File

@ -21,6 +21,12 @@ import { useUrlShortener } from 'src/common/hooks/useUrlShortener';
import copyTextToClipboard from 'src/utils/copy'; import copyTextToClipboard from 'src/utils/copy';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { Menu } from 'src/common/components'; import { Menu } from 'src/common/components';
import { getUrlParam } from 'src/utils/urlUtils';
import { URL_PARAMS } from 'src/constants';
import {
createFilterKey,
getFilterValue,
} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
interface ShareMenuItemProps { interface ShareMenuItemProps {
url: string; url: string;
@ -30,6 +36,7 @@ interface ShareMenuItemProps {
emailBody: string; emailBody: string;
addDangerToast: Function; addDangerToast: Function;
addSuccessToast: Function; addSuccessToast: Function;
dashboardId?: string;
} }
const ShareMenuItems = (props: ShareMenuItemProps) => { const ShareMenuItems = (props: ShareMenuItemProps) => {
@ -41,14 +48,32 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
emailBody, emailBody,
addDangerToast, addDangerToast,
addSuccessToast, addSuccessToast,
dashboardId,
...rest ...rest
} = props; } = props;
const getShortUrl = useUrlShortener(url); const getShortUrl = useUrlShortener(url);
async function getCopyUrl() {
const risonObj = getUrlParam(URL_PARAMS.nativeFilters);
if (typeof risonObj === 'object' || !dashboardId) return null;
const prevData = await getFilterValue(
dashboardId,
getUrlParam(URL_PARAMS.nativeFiltersKey),
);
const newDataMaskKey = await createFilterKey(
dashboardId,
JSON.stringify(prevData),
);
const newUrl = new URL(`${window.location.origin}${url}`);
newUrl.searchParams.set(URL_PARAMS.nativeFilters.name, newDataMaskKey);
return `${newUrl.pathname}${newUrl.search}`;
}
async function onCopyLink() { async function onCopyLink() {
try { try {
const shortUrl = await getShortUrl(); const copyUrl = await getCopyUrl();
const shortUrl = await getShortUrl(copyUrl);
await copyTextToClipboard(shortUrl); await copyTextToClipboard(shortUrl);
addSuccessToast(t('Copied to clipboard!')); addSuccessToast(t('Copied to clipboard!'));
} catch (error) { } catch (error) {
@ -58,7 +83,8 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
async function onShareByEmail() { async function onShareByEmail() {
try { try {
const shortUrl = await getShortUrl(); const copyUrl = await getCopyUrl();
const shortUrl = await getShortUrl(copyUrl);
const bodyWithLink = `${emailBody}${shortUrl}`; const bodyWithLink = `${emailBody}${shortUrl}`;
window.location.href = `mailto:?Subject=${emailSubject}%20&Body=${bodyWithLink}`; window.location.href = `mailto:?Subject=${emailSubject}%20&Body=${bodyWithLink}`;
} catch (error) { } catch (error) {

View File

@ -20,13 +20,12 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core'; import { DataMask, HandlerFunction, styled, t } from '@superset-ui/core';
import React, { useEffect, useState, useCallback, useMemo } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import cx from 'classnames'; import cx from 'classnames';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { Tabs } from 'src/common/components'; import { Tabs } from 'src/common/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { usePrevious } from 'src/common/hooks/usePrevious'; import { usePrevious } from 'src/common/hooks/usePrevious';
import rison from 'rison';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { updateDataMask, clearDataMask } from 'src/dataMask/actions'; import { updateDataMask, clearDataMask } from 'src/dataMask/actions';
import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types'; import { DataMaskStateWithId, DataMaskWithId } from 'src/dataMask/types';
@ -40,7 +39,7 @@ import {
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 { URL_PARAMS } from 'src/constants'; import { URL_PARAMS } from 'src/constants';
import replaceUndefinedByNull from 'src/dashboard/util/replaceUndefinedByNull'; import { getUrlParam } from 'src/utils/urlUtils';
import { checkIsApplyDisabled, TabIds } from './utils'; import { checkIsApplyDisabled, TabIds } from './utils';
import FilterSets from './FilterSets'; import FilterSets from './FilterSets';
import { import {
@ -50,6 +49,7 @@ import {
useFilterUpdates, useFilterUpdates,
useInitialization, useInitialization,
} from './state'; } from './state';
import { createFilterKey, updateFilterKey } from './keyValue';
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';
@ -154,12 +154,16 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const [dataMaskSelected, setDataMaskSelected] = const [dataMaskSelected, setDataMaskSelected] =
useImmer<DataMaskStateWithId>(dataMaskApplied); useImmer<DataMaskStateWithId>(dataMaskApplied);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [updateKey, setUpdateKey] = useState(0);
const filterSets = useFilterSets(); const filterSets = useFilterSets();
const filterSetFilterValues = Object.values(filterSets); const filterSetFilterValues = Object.values(filterSets);
const [tab, setTab] = useState(TabIds.AllFilters); const [tab, setTab] = useState(TabIds.AllFilters);
const filters = useFilters(); const filters = useFilters();
const previousFilters = usePrevious(filters); const previousFilters = usePrevious(filters);
const filterValues = Object.values<Filter>(filters); const filterValues = Object.values<Filter>(filters);
const dashboardId = useSelector<any, string>(
({ dashboardInfo }) => dashboardInfo?.id,
);
const handleFilterSelectionChange = useCallback( const handleFilterSelectionChange = useCallback(
( (
@ -187,28 +191,36 @@ const FilterBar: React.FC<FiltersBarProps> = ({
); );
const publishDataMask = useCallback( const publishDataMask = useCallback(
(dataMaskSelected: DataMaskStateWithId) => { async (dataMaskSelected: DataMaskStateWithId) => {
const { location } = history; const { location } = history;
const { search } = location; const { search } = location;
const previousParams = new URLSearchParams(search); const previousParams = new URLSearchParams(search);
const newParams = new URLSearchParams(); const newParams = new URLSearchParams();
let dataMaskKey = '';
previousParams.forEach((value, key) => { previousParams.forEach((value, key) => {
if (key !== URL_PARAMS.nativeFilters.name) { if (key !== URL_PARAMS.nativeFilters.name) {
newParams.append(key, value); newParams.append(key, value);
} }
}); });
newParams.set( const nativeFiltersCacheKey = getUrlParam(URL_PARAMS.nativeFiltersKey);
URL_PARAMS.nativeFilters.name, const dataMask = JSON.stringify(dataMaskSelected);
rison.encode(replaceUndefinedByNull(dataMaskSelected)), if (
); updateKey &&
nativeFiltersCacheKey &&
(await updateFilterKey(dashboardId, dataMask, nativeFiltersCacheKey))
) {
dataMaskKey = nativeFiltersCacheKey;
} else {
dataMaskKey = await createFilterKey(dashboardId, dataMask);
}
newParams.set(URL_PARAMS.nativeFiltersKey.name, dataMaskKey);
history.replace({ history.replace({
search: newParams.toString(), search: newParams.toString(),
}); });
}, },
[history], [history, updateKey],
); );
useEffect(() => { useEffect(() => {
@ -250,6 +262,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
const filterIds = Object.keys(dataMaskSelected); const filterIds = Object.keys(dataMaskSelected);
setUpdateKey(1);
filterIds.forEach(filterId => { filterIds.forEach(filterId => {
if (dataMaskSelected[filterId]) { if (dataMaskSelected[filterId]) {
dispatch(updateDataMask(filterId, dataMaskSelected[filterId])); dispatch(updateDataMask(filterId, dataMaskSelected[filterId]));

View File

@ -0,0 +1,54 @@
/**
* 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 { SupersetClient, logging } from '@superset-ui/core';
export const updateFilterKey = (dashId: string, value: string, key: string) =>
SupersetClient.put({
endpoint: `api/v1/dashboard/${dashId}/filter_state/${key}/`,
jsonPayload: { value },
})
.then(r => r.json.message)
.catch(err => {
logging.error(err);
return null;
});
export const createFilterKey = (dashId: string | number, value: string) =>
SupersetClient.post({
endpoint: `api/v1/dashboard/${dashId}/filter_state`,
jsonPayload: { value },
})
.then(r => r.json.key)
.catch(err => {
logging.error(err);
return null;
});
export const getFilterValue = (
dashId: string | number | undefined,
key: string,
) =>
SupersetClient.get({
endpoint: `api/v1/dashboard/${dashId}/filter_state/${key}/`,
})
.then(({ json }) => JSON.parse(json.value))
.catch(err => {
logging.error(err);
return null;
});

View File

@ -88,7 +88,6 @@ export const useFilterUpdates = (
) => { ) => {
const filters = useFilters(); const filters = useFilters();
const dataMaskApplied = useNativeFiltersDataMask(); const dataMaskApplied = useNativeFiltersDataMask();
useEffect(() => { useEffect(() => {
// Remove deleted filters from local state // Remove deleted filters from local state
Object.keys(dataMaskSelected).forEach(selectedId => { Object.keys(dataMaskSelected).forEach(selectedId => {

View File

@ -69,7 +69,6 @@ export const checkIsApplyDisabled = (
) => { ) => {
const dataSelectedValues = Object.values(dataMaskSelected); const dataSelectedValues = Object.values(dataMaskSelected);
const dataAppliedValues = Object.values(dataMaskApplied); const dataAppliedValues = Object.values(dataMaskApplied);
return ( return (
areObjectsEqual( areObjectsEqual(
getOnlyExtraFormData(dataMaskSelected), getOnlyExtraFormData(dataMaskSelected),

View File

@ -49,8 +49,8 @@ function mapStateToProps(state: RootState) {
} = state; } = state;
return { return {
initMessages: dashboardInfo.common.flash_messages, initMessages: dashboardInfo.common?.flash_messages,
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, timeout: dashboardInfo.common?.conf?.SUPERSET_WEBSERVER_TIMEOUT,
userId: dashboardInfo.userId, userId: dashboardInfo.userId,
dashboardInfo, dashboardInfo,
dashboardState, dashboardState,

View File

@ -48,6 +48,7 @@ import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils'; import { getUrlParam } from 'src/utils/urlUtils';
import { canUserEditDashboard } from 'src/dashboard/util/findPermission'; import { canUserEditDashboard } from 'src/dashboard/util/findPermission';
import { getFilterSets } from '../actions/nativeFilters'; import { getFilterSets } from '../actions/nativeFilters';
import { getFilterValue } from '../components/nativeFilters/FilterBar/keyValue';
export const MigrationContext = React.createContext( export const MigrationContext = React.createContext(
FILTER_BOX_MIGRATION_STATES.NOOP, FILTER_BOX_MIGRATION_STATES.NOOP,
@ -155,16 +156,40 @@ const DashboardPage: FC = () => {
}, [readyToRender]); }, [readyToRender]);
useEffect(() => { useEffect(() => {
if (readyToRender) { // eslint-disable-next-line consistent-return
if (!isDashboardHydrated.current) { async function getDataMaskApplied() {
isDashboardHydrated.current = true; const nativeFilterKeyValue = getUrlParam(URL_PARAMS.nativeFiltersKey);
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) { let dataMaskFromUrl = nativeFilterKeyValue || {};
// only initialize filterset once
dispatch(getFilterSets(id)); const isOldRison = getUrlParam(URL_PARAMS.nativeFilters);
} // check if key from key_value api and get datamask
if (nativeFilterKeyValue) {
dataMaskFromUrl = await getFilterValue(id, nativeFilterKeyValue);
} }
dispatch(hydrateDashboard(dashboard, charts, filterboxMigrationState)); if (isOldRison) {
dataMaskFromUrl = isOldRison;
}
if (readyToRender) {
if (!isDashboardHydrated.current) {
isDashboardHydrated.current = true;
if (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET)) {
// only initialize filterset once
dispatch(getFilterSets(id));
}
}
dispatch(
hydrateDashboard(
dashboard,
charts,
filterboxMigrationState,
dataMaskFromUrl,
),
);
}
return null;
} }
if (id) getDataMaskApplied();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [readyToRender, filterboxMigrationState]); }, [readyToRender, filterboxMigrationState]);

View File

@ -16,25 +16,21 @@
* 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 { JsonObject } from '@superset-ui/core';
import { URL_PARAMS } from 'src/constants'; import { URL_PARAMS } from 'src/constants';
import replaceUndefinedByNull from './replaceUndefinedByNull'; import { getUrlParam } from 'src/utils/urlUtils';
import serializeActiveFilterValues from './serializeActiveFilterValues'; import serializeActiveFilterValues from './serializeActiveFilterValues';
import { DataMaskState } from '../../dataMask/types';
export default function getDashboardUrl({ export default function getDashboardUrl({
pathname, pathname,
filters = {}, filters = {},
hash = '', hash = '',
standalone, standalone,
dataMask,
}: { }: {
pathname: string; pathname: string;
filters: JsonObject; filters: JsonObject;
hash: string; hash: string;
standalone?: number | null; standalone?: number | null;
dataMask?: DataMaskState;
}) { }) {
const newSearchParams = new URLSearchParams(); const newSearchParams = new URLSearchParams();
@ -48,11 +44,11 @@ export default function getDashboardUrl({
if (standalone) { if (standalone) {
newSearchParams.set(URL_PARAMS.standalone.name, standalone.toString()); newSearchParams.set(URL_PARAMS.standalone.name, standalone.toString());
} }
const dataMaskKey = getUrlParam(URL_PARAMS.nativeFiltersKey);
if (dataMask) { if (dataMaskKey) {
newSearchParams.set( newSearchParams.set(
URL_PARAMS.nativeFilters.name, URL_PARAMS.nativeFiltersKey.name,
rison.encode(replaceUndefinedByNull(dataMask)), dataMaskKey as string,
); );
} }

View File

@ -34,6 +34,12 @@ export interface UpdateDataMask {
dataMask: DataMask; dataMask: DataMask;
} }
export const INIT_DATAMASK = 'INIT_DATAMASK';
export interface INITDATAMASK {
type: typeof INIT_DATAMASK;
dataMask: DataMask;
}
export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE = export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE =
'SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE'; 'SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE';

View File

@ -24,8 +24,6 @@ import { DataMask, FeatureFlag } from '@superset-ui/core';
import { NATIVE_FILTER_PREFIX } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils'; import { NATIVE_FILTER_PREFIX } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate'; import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
import { isFeatureEnabled } from 'src/featureFlags'; import { isFeatureEnabled } from 'src/featureFlags';
import { getUrlParam } from 'src/utils/urlUtils';
import { URL_PARAMS } from 'src/constants';
import { DataMaskStateWithId, DataMaskWithId } from './types'; import { DataMaskStateWithId, DataMaskWithId } from './types';
import { import {
AnyDataMaskAction, AnyDataMaskAction,
@ -63,18 +61,19 @@ export function getInitialDataMask(
} as DataMaskWithId; } as DataMaskWithId;
} }
function fillNativeFilters( async function fillNativeFilters(
filterConfig: FilterConfiguration, filterConfig: FilterConfiguration,
mergedDataMask: DataMaskStateWithId, mergedDataMask: DataMaskStateWithId,
draftDataMask: DataMaskStateWithId, draftDataMask: DataMaskStateWithId,
initialDataMask?: DataMaskStateWithId,
currentFilters?: Filters, currentFilters?: Filters,
) { ) {
const dataMaskFromUrl = getUrlParam(URL_PARAMS.nativeFilters) || {};
filterConfig.forEach((filter: Filter) => { filterConfig.forEach((filter: Filter) => {
const dataMask = initialDataMask || {};
mergedDataMask[filter.id] = { mergedDataMask[filter.id] = {
...getInitialDataMask(filter.id), // take initial data ...getInitialDataMask(filter.id), // take initial data
...filter.defaultDataMask, // if something new came from BE - take it ...filter.defaultDataMask, // if something new came from BE - take it
...dataMaskFromUrl[filter.id], ...dataMask[filter.id],
}; };
if ( if (
currentFilters && currentFilters &&
@ -131,6 +130,8 @@ const dataMaskReducer = produce(
[], [],
cleanState, cleanState,
draft, draft,
// @ts-ignore
action.data.dataMask,
); );
return cleanState; return cleanState;
case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE: case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE:
@ -138,6 +139,8 @@ const dataMaskReducer = produce(
action.filterConfig ?? [], action.filterConfig ?? [],
cleanState, cleanState,
draft, draft,
// @ts-ignore
action.data.dataMask,
action.filters, action.filters,
); );
return cleanState; return cleanState;

View File

@ -28,6 +28,9 @@ 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(param: UrlParam & { type: 'rison' }): object;
export function getUrlParam(
param: UrlParam & { type: 'rison | string' },
): string | 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) {
@ -62,7 +65,7 @@ export function getUrlParam({ name, type }: UrlParam): unknown {
try { try {
return rison.decode(urlParam); return rison.decode(urlParam);
} catch { } catch {
return null; return urlParam;
} }
default: default:
return urlParam; return urlParam;