Skip to content

Commit 4041150

Browse files
authored
feat: Add getDataMask function to embedded SDK (#32997)
1 parent bcb4332 commit 4041150

File tree

5 files changed

+232
-61
lines changed

5 files changed

+232
-61
lines changed

superset-embedded-sdk/src/index.ts

Lines changed: 90 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import {
2121
DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY,
22-
IFRAME_COMMS_MESSAGE_TYPE
22+
IFRAME_COMMS_MESSAGE_TYPE,
2323
} from './const';
2424

2525
// We can swap this out for the actual switchboard package once it gets published
@@ -34,51 +34,61 @@ import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
3434
export type GuestTokenFetchFn = () => Promise<string>;
3535

3636
export type UiConfigType = {
37-
hideTitle?: boolean
38-
hideTab?: boolean
39-
hideChartControls?: boolean
40-
emitDataMasks?: boolean
37+
hideTitle?: boolean;
38+
hideTab?: boolean;
39+
hideChartControls?: boolean;
40+
emitDataMasks?: boolean;
4141
filters?: {
42-
[key: string]: boolean | undefined
43-
visible?: boolean
44-
expanded?: boolean
45-
}
42+
[key: string]: boolean | undefined;
43+
visible?: boolean;
44+
expanded?: boolean;
45+
};
4646
urlParams?: {
47-
[key: string]: any
48-
}
49-
}
47+
[key: string]: any;
48+
};
49+
};
5050

5151
export type EmbedDashboardParams = {
5252
/** The id provided by the embed configuration UI in Superset */
53-
id: string
53+
id: string;
5454
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
55-
supersetDomain: string
55+
supersetDomain: string;
5656
/** The html element within which to mount the iframe */
57-
mountPoint: HTMLElement
57+
mountPoint: HTMLElement;
5858
/** A function to fetch a guest token from the Host App's backend server */
59-
fetchGuestToken: GuestTokenFetchFn
59+
fetchGuestToken: GuestTokenFetchFn;
6060
/** The dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded **/
61-
dashboardUiConfig?: UiConfigType
61+
dashboardUiConfig?: UiConfigType;
6262
/** Are we in debug mode? */
63-
debug?: boolean
63+
debug?: boolean;
6464
/** The iframe title attribute */
65-
iframeTitle?: string
65+
iframeTitle?: string;
6666
/** additional iframe sandbox attributes ex (allow-top-navigation, allow-popups-to-escape-sandbox) **/
67-
iframeSandboxExtras?: string[]
67+
iframeSandboxExtras?: string[];
6868
/** force a specific refererPolicy to be used in the iframe request **/
69-
referrerPolicy?: ReferrerPolicy
70-
}
69+
referrerPolicy?: ReferrerPolicy;
70+
};
7171

7272
export type Size = {
73-
width: number, height: number
74-
}
73+
width: number;
74+
height: number;
75+
};
7576

77+
export type ObserveDataMaskCallbackFn = (
78+
dataMask: Record<string, any> & {
79+
crossFiltersChanged: boolean;
80+
nativeFiltersChanged: boolean;
81+
},
82+
) => void;
7683
export type EmbeddedDashboard = {
7784
getScrollSize: () => Promise<Size>;
7885
unmount: () => void;
7986
getDashboardPermalink: (anchor: string) => Promise<string>;
8087
getActiveTabs: () => Promise<string[]>;
81-
getDataMasks: (callbackFn: (dataMasks: any[]) => void) => void;
88+
observeDataMask: (
89+
callbackFn: ObserveDataMaskCallbackFn,
90+
) => void;
91+
getDataMask: () => Record<string, any>;
8292
};
8393

8494
/**
@@ -91,7 +101,7 @@ export async function embedDashboard({
91101
fetchGuestToken,
92102
dashboardUiConfig,
93103
debug = false,
94-
iframeTitle = "Embedded Dashboard",
104+
iframeTitle = 'Embedded Dashboard',
95105
iframeSandboxExtras = [],
96106
referrerPolicy,
97107
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
@@ -103,55 +113,67 @@ export async function embedDashboard({
103113

104114
log('embedding');
105115

106-
if (supersetDomain.endsWith("/")) {
116+
if (supersetDomain.endsWith('/')) {
107117
supersetDomain = supersetDomain.slice(0, -1);
108118
}
109119

110120
function calculateConfig() {
111-
let configNumber = 0
112-
if(dashboardUiConfig) {
113-
if(dashboardUiConfig.hideTitle) {
114-
configNumber += 1
121+
let configNumber = 0;
122+
if (dashboardUiConfig) {
123+
if (dashboardUiConfig.hideTitle) {
124+
configNumber += 1;
115125
}
116-
if(dashboardUiConfig.hideTab) {
117-
configNumber += 2
126+
if (dashboardUiConfig.hideTab) {
127+
configNumber += 2;
118128
}
119-
if(dashboardUiConfig.hideChartControls) {
120-
configNumber += 8
129+
if (dashboardUiConfig.hideChartControls) {
130+
configNumber += 8;
121131
}
122132
if (dashboardUiConfig.emitDataMasks) {
123-
configNumber += 16
133+
configNumber += 16;
124134
}
125135
}
126-
return configNumber
136+
return configNumber;
127137
}
128138

129139
async function mountIframe(): Promise<Switchboard> {
130140
return new Promise(resolve => {
131141
const iframe = document.createElement('iframe');
132-
const dashboardConfigUrlParams = dashboardUiConfig ? {uiConfig: `${calculateConfig()}`} : undefined;
133-
const filterConfig = dashboardUiConfig?.filters || {}
134-
const filterConfigKeys = Object.keys(filterConfig)
135-
const filterConfigUrlParams = Object.fromEntries(filterConfigKeys.map(
136-
key => [DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY[key], filterConfig[key]]))
142+
const dashboardConfigUrlParams = dashboardUiConfig
143+
? { uiConfig: `${calculateConfig()}` }
144+
: undefined;
145+
const filterConfig = dashboardUiConfig?.filters || {};
146+
const filterConfigKeys = Object.keys(filterConfig);
147+
const filterConfigUrlParams = Object.fromEntries(
148+
filterConfigKeys.map(key => [
149+
DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY[key],
150+
filterConfig[key],
151+
]),
152+
);
137153

138154
// Allow url query parameters from dashboardUiConfig.urlParams to override the ones from filterConfig
139-
const urlParams = {...dashboardConfigUrlParams, ...filterConfigUrlParams, ...dashboardUiConfig?.urlParams}
140-
const urlParamsString = Object.keys(urlParams).length ? '?' + new URLSearchParams(urlParams).toString() : ''
155+
const urlParams = {
156+
...dashboardConfigUrlParams,
157+
...filterConfigUrlParams,
158+
...dashboardUiConfig?.urlParams,
159+
};
160+
const urlParamsString = Object.keys(urlParams).length
161+
? '?' + new URLSearchParams(urlParams).toString()
162+
: '';
141163

142164
// set up the iframe's sandbox configuration
143-
iframe.sandbox.add("allow-same-origin"); // needed for postMessage to work
144-
iframe.sandbox.add("allow-scripts"); // obviously the iframe needs scripts
145-
iframe.sandbox.add("allow-presentation"); // for fullscreen charts
146-
iframe.sandbox.add("allow-downloads"); // for downloading charts as image
147-
iframe.sandbox.add("allow-forms"); // for forms to submit
148-
iframe.sandbox.add("allow-popups"); // for exporting charts as csv
165+
iframe.sandbox.add('allow-same-origin'); // needed for postMessage to work
166+
iframe.sandbox.add('allow-scripts'); // obviously the iframe needs scripts
167+
iframe.sandbox.add('allow-presentation'); // for fullscreen charts
168+
iframe.sandbox.add('allow-downloads'); // for downloading charts as image
169+
iframe.sandbox.add('allow-forms'); // for forms to submit
170+
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
149171
// additional sandbox props
150172
iframeSandboxExtras.forEach((key: string) => {
151173
iframe.sandbox.add(key);
152174
});
153175
// force a specific refererPolicy to be used in the iframe request
154-
if(referrerPolicy) {
176+
if (referrerPolicy) {
155177
iframe.referrerPolicy = referrerPolicy;
156178
}
157179

@@ -167,20 +189,26 @@ export async function embedDashboard({
167189
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
168190
// we know the content window isn't null because we are in the load event handler.
169191
iframe.contentWindow!.postMessage(
170-
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: "port transfer" },
192+
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: 'port transfer' },
171193
supersetDomain,
172194
[theirPort],
173-
)
195+
);
174196
log('sent message channel to the iframe');
175197

176198
// return our port from the promise
177-
resolve(new Switchboard({ port: ourPort, name: 'superset-embedded-sdk', debug }));
199+
resolve(
200+
new Switchboard({
201+
port: ourPort,
202+
name: 'superset-embedded-sdk',
203+
debug,
204+
}),
205+
);
178206
});
179207
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
180208
iframe.title = iframeTitle;
181209
//@ts-ignore
182210
mountPoint.replaceChildren(iframe);
183-
log('placed the iframe')
211+
log('placed the iframe');
184212
});
185213
}
186214

@@ -210,17 +238,20 @@ export async function embedDashboard({
210238
const getDashboardPermalink = (anchor: string) =>
211239
ourPort.get<string>('getDashboardPermalink', { anchor });
212240
const getActiveTabs = () => ourPort.get<string[]>('getActiveTabs');
213-
const getDataMasks = (callbackFn: (dataMasks: any[]) => void) => {
241+
const getDataMask = () => ourPort.get<Record<string, any>>('getDataMask');
242+
const observeDataMask = (
243+
callbackFn: ObserveDataMaskCallbackFn,
244+
) => {
214245
ourPort.start();
215-
ourPort.defineMethod("getDataMasks", callbackFn);
246+
ourPort.defineMethod('observeDataMask', callbackFn);
216247
};
217248

218-
219249
return {
220250
getScrollSize,
221251
unmount,
222252
getDashboardPermalink,
223253
getActiveTabs,
224-
getDataMasks,
254+
observeDataMask,
255+
getDataMask,
225256
};
226257
}

superset-frontend/src/embedded/api.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19+
import { DataMaskStateWithId } from '@superset-ui/core';
1920
import getBootstrapData from 'src/utils/getBootstrapData';
2021
import { store } from '../views/store';
2122
import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils';
@@ -31,6 +32,7 @@ type EmbeddedSupersetApi = {
3132
getScrollSize: () => Size;
3233
getDashboardPermalink: ({ anchor }: { anchor: string }) => Promise<string>;
3334
getActiveTabs: () => string[];
35+
getDataMask: () => DataMaskStateWithId;
3436
};
3537

3638
const getScrollSize = (): Size => ({
@@ -61,8 +63,11 @@ const getDashboardPermalink = async ({
6163

6264
const getActiveTabs = () => store?.getState()?.dashboardState?.activeTabs || [];
6365

66+
const getDataMask = () => store?.getState()?.dataMask || {};
67+
6468
export const embeddedApi: EmbeddedSupersetApi = {
6569
getScrollSize,
6670
getDashboardPermalink,
6771
getActiveTabs,
72+
getDataMask,
6873
};

superset-frontend/src/embedded/index.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
3333
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
3434
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
3535
import { embeddedApi } from './api';
36+
import { getDataMaskChangeTrigger } from './utils';
3637

3738
setupPlugins();
3839

@@ -59,9 +60,20 @@ const EmbededLazyDashboardPage = () => {
5960
if (uiConfig?.emitDataMasks) {
6061
log('setting up Switchboard event emitter');
6162

63+
let previousDataMask = store.getState().dataMask;
64+
6265
store.subscribe(() => {
63-
const state = store.getState();
64-
Switchboard.emit('getDataMasks', state.dataMask);
66+
const currentState = store.getState();
67+
const currentDataMask = currentState.dataMask;
68+
69+
// Only emit if the dataMask has changed
70+
if (previousDataMask !== currentDataMask) {
71+
Switchboard.emit('observeDataMask', {
72+
...currentDataMask,
73+
...getDataMaskChangeTrigger(currentDataMask, previousDataMask),
74+
});
75+
previousDataMask = currentDataMask;
76+
}
6577
});
6678
}
6779

@@ -226,6 +238,7 @@ window.addEventListener('message', function embeddedPageInitializer(event) {
226238
embeddedApi.getDashboardPermalink,
227239
);
228240
Switchboard.defineMethod('getActiveTabs', embeddedApi.getActiveTabs);
241+
Switchboard.defineMethod('getDataMask', embeddedApi.getDataMask);
229242
Switchboard.start();
230243
}
231244
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { DataMaskStateWithId } from '@superset-ui/core';
20+
import { cloneDeep } from 'lodash';
21+
import { getDataMaskChangeTrigger } from './utils';
22+
23+
const dataMask: DataMaskStateWithId = {
24+
'1': {
25+
id: '1',
26+
extraFormData: {},
27+
filterState: {},
28+
ownState: {},
29+
},
30+
'2': {
31+
id: '2',
32+
extraFormData: {},
33+
filterState: {},
34+
ownState: {},
35+
},
36+
'NATIVE_FILTER-1': {
37+
id: 'NATIVE_FILTER-1',
38+
extraFormData: {},
39+
filterState: {
40+
value: null,
41+
},
42+
ownState: {},
43+
},
44+
'NATIVE_FILTER-2': {
45+
id: 'NATIVE_FILTER-2',
46+
extraFormData: {},
47+
filterState: {},
48+
ownState: {},
49+
},
50+
};
51+
52+
it('datamask didnt change - both triggers set to false', () => {
53+
const previousDataMask = cloneDeep(dataMask);
54+
expect(getDataMaskChangeTrigger(dataMask, previousDataMask)).toEqual({
55+
crossFiltersChanged: false,
56+
nativeFiltersChanged: false,
57+
});
58+
});
59+
60+
it('a native filter changed - nativeFiltersChanged set to true', () => {
61+
const previousDataMask = cloneDeep(dataMask);
62+
previousDataMask['NATIVE_FILTER-1'].filterState!.value = 'test';
63+
expect(getDataMaskChangeTrigger(dataMask, previousDataMask)).toEqual({
64+
crossFiltersChanged: false,
65+
nativeFiltersChanged: true,
66+
});
67+
});
68+
69+
it('a cross filter changed - crossFiltersChanged set to true', () => {
70+
const previousDataMask = cloneDeep(dataMask);
71+
previousDataMask['1'].filterState!.value = 'test';
72+
expect(getDataMaskChangeTrigger(dataMask, previousDataMask)).toEqual({
73+
crossFiltersChanged: true,
74+
nativeFiltersChanged: false,
75+
});
76+
});

0 commit comments

Comments
 (0)