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:
parent
2c3f39f3f2
commit
cfd851aa13
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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]));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue