This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 5.0-pulse
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/5.0-pulse by this push:
new d66d2fc649 feat(embedded-sdk): Add resolvePermalinkUrl callback for
custom permalink URLs (#36924)
d66d2fc649 is described below
commit d66d2fc649dfcbd005de9ed3e44a2aadaa6f936c
Author: Michael S. Molina <[email protected]>
AuthorDate: Fri Jan 9 17:02:38 2026 -0300
feat(embedded-sdk): Add resolvePermalinkUrl callback for custom permalink
URLs (#36924)
(cherry picked from commit 53dddf4db26d32ec86673a6d42b009cc49fbd09d)
---
superset-embedded-sdk/README.md | 97 ++++++++---
superset-embedded-sdk/src/index.ts | 182 ++++++++++++++-------
.../components/URLShortLinkButton/index.tsx | 6 +-
.../components/menu/ShareMenuItems/index.tsx | 6 +-
.../src/dashboard/containers/DashboardPage.tsx | 16 +-
superset-frontend/src/embedded/api.tsx | 4 +-
.../src/explore/components/EmbedCodeContent.jsx | 8 +-
.../useExploreAdditionalActionsMenu/index.jsx | 17 +-
superset-frontend/src/utils/urlUtils.ts | 72 ++++++--
9 files changed, 299 insertions(+), 109 deletions(-)
diff --git a/superset-embedded-sdk/README.md b/superset-embedded-sdk/README.md
index 377720dd3b..3065e79026 100644
--- a/superset-embedded-sdk/README.md
+++ b/superset-embedded-sdk/README.md
@@ -29,8 +29,8 @@ Embedding is done by inserting an iframe, containing a
Superset page, into the h
## Prerequisites
-* Activate the feature flag `EMBEDDED_SUPERSET`
-* Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET`
(see configuration file config.py). Be aware that its default value must be
changed in production.
+- Activate the feature flag `EMBEDDED_SUPERSET`
+- Set a strong password in configuration variable `GUEST_TOKEN_JWT_SECRET`
(see configuration file config.py). Be aware that its default value must be
changed in production.
## Embedding a Dashboard
@@ -48,19 +48,27 @@ embedDashboard({
supersetDomain: "https://superset.example.com",
mountPoint: document.getElementById("my-superset-container"), // any html
element that can contain an iframe
fetchGuestToken: () => fetchGuestTokenFromBackend(),
- dashboardUiConfig: { // dashboard UI config: hideTitle, hideTab,
hideChartControls, filters.visible, filters.expanded (optional), urlParams
(optional)
- hideTitle: true,
- filters: {
- expanded: true,
- },
- urlParams: {
- foo: 'value1',
- bar: 'value2',
- // ...
- }
+ dashboardUiConfig: {
+ // dashboard UI config: hideTitle, hideTab, hideChartControls,
filters.visible, filters.expanded (optional), urlParams (optional)
+ hideTitle: true,
+ filters: {
+ expanded: true,
+ },
+ urlParams: {
+ foo: "value1",
+ bar: "value2",
+ // ...
+ },
},
- // optional additional iframe sandbox attributes
- iframeSandboxExtras: ['allow-top-navigation',
'allow-popups-to-escape-sandbox']
+ // optional additional iframe sandbox attributes
+ iframeSandboxExtras: [
+ "allow-top-navigation",
+ "allow-popups-to-escape-sandbox",
+ ],
+ // optional Permissions Policy features
+ iframeAllowExtras: ["clipboard-write", "fullscreen"],
+ // optional callback to customize permalink URLs
+ resolvePermalinkUrl: ({ key }) =>
`https://my-app.com/analytics/share/${key}`,
});
```
@@ -91,7 +99,7 @@ Guest tokens can have Row Level Security rules which filter
data for the user ca
The agent making the `POST` request must be authenticated with the
`can_grant_guest_token` permission.
-Within your app, using the Guest Token will then allow authentication to your
Superset instance via creating an Anonymous user object. This guest anonymous
user will default to the public role as per this setting `GUEST_ROLE_NAME =
"Public"`.
+Within your app, using the Guest Token will then allow authentication to your
Superset instance via creating an Anonymous user object. This guest anonymous
user will default to the public role as per this setting `GUEST_ROLE_NAME =
"Public"`.
The user parameters in the example below are optional and are provided as a
means of passing user attributes that may be accessed in jinja templates inside
your charts.
@@ -104,18 +112,19 @@ Example `POST /security/guest_token` payload:
"first_name": "Stan",
"last_name": "Lee"
},
- "resources": [{
- "type": "dashboard",
- "id": "abc123"
- }],
- "rls": [
- { "clause": "publisher = 'Nintendo'" }
- ]
+ "resources": [
+ {
+ "type": "dashboard",
+ "id": "abc123"
+ }
+ ],
+ "rls": [{ "clause": "publisher = 'Nintendo'" }]
}
```
Alternatively, a guest token can be created directly in your app with a json
like the following, and then signed
with the secret set in configuration variable `GUEST_TOKEN_JWT_SECRET` (see
configuration file config.py)
+
```
{
"user": {
@@ -142,7 +151,47 @@ with the secret set in configuration variable
`GUEST_TOKEN_JWT_SECRET` (see conf
The Embedded SDK creates an iframe with
[sandbox](https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe#sandbox)
mode by default
which applies certain restrictions to the iframe's content.
To pass additional sandbox attributes you can use `iframeSandboxExtras`:
+
```js
- // optional additional iframe sandbox attributes
- iframeSandboxExtras: ['allow-top-navigation',
'allow-popups-to-escape-sandbox']
+// optional additional iframe sandbox attributes
+iframeSandboxExtras: ["allow-top-navigation",
"allow-popups-to-escape-sandbox"];
+```
+
+### Customizing Permalink URLs
+
+When users click share buttons inside an embedded dashboard, Superset
generates permalinks using Superset's domain. If you want to use your own
domain and URL format for these permalinks, you can provide a
`resolvePermalinkUrl` callback:
+
+```js
+embedDashboard({
+ id: "abc123",
+ supersetDomain: "https://superset.example.com",
+ mountPoint: document.getElementById("my-superset-container"),
+ fetchGuestToken: () => fetchGuestTokenFromBackend(),
+
+ // Customize permalink URLs
+ resolvePermalinkUrl: ({ key }) => {
+ // key: the permalink key (e.g., "xyz789")
+ return `https://my-app.com/analytics/share/${key}`;
+ },
+});
+```
+
+To restore the dashboard state from a permalink in your app:
+
+```js
+// In your route handler for /analytics/share/:key
+const permalinkKey = routeParams.key;
+
+embedDashboard({
+ id: "abc123",
+ supersetDomain: "https://superset.example.com",
+ mountPoint: document.getElementById("my-superset-container"),
+ fetchGuestToken: () => fetchGuestTokenFromBackend(),
+ resolvePermalinkUrl: ({ key }) =>
`https://my-app.com/analytics/share/${key}`,
+ dashboardUiConfig: {
+ urlParams: {
+ permalink_key: permalinkKey, // Restores filters, tabs, chart states,
and scrolls to anchor
+ },
+ },
+});
```
diff --git a/superset-embedded-sdk/src/index.ts
b/superset-embedded-sdk/src/index.ts
index 063db77fb7..d00a403016 100644
--- a/superset-embedded-sdk/src/index.ts
+++ b/superset-embedded-sdk/src/index.ts
@@ -19,12 +19,12 @@
import {
DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY,
- IFRAME_COMMS_MESSAGE_TYPE
-} from './const';
+ IFRAME_COMMS_MESSAGE_TYPE,
+} from "./const";
// We can swap this out for the actual switchboard package once it gets
published
-import { Switchboard } from '@superset-ui/switchboard';
-import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
+import { Switchboard } from "@superset-ui/switchboard";
+import { getGuestTokenRefreshTiming } from "./guestTokenRefresh";
/**
* The function to fetch a guest token from your Host App's backend server.
@@ -34,48 +34,63 @@ import { getGuestTokenRefreshTiming } from
'./guestTokenRefresh';
export type GuestTokenFetchFn = () => Promise<string>;
export type UiConfigType = {
- hideTitle?: boolean
- hideTab?: boolean
- hideChartControls?: boolean
+ hideTitle?: boolean;
+ hideTab?: boolean;
+ hideChartControls?: boolean;
filters?: {
- [key: string]: boolean | undefined
- visible?: boolean
- expanded?: boolean
- }
+ [key: string]: boolean | undefined;
+ visible?: boolean;
+ expanded?: boolean;
+ };
urlParams?: {
- [key: string]: any
- }
-}
+ [key: string]: any;
+ };
+};
export type EmbedDashboardParams = {
/** The id provided by the embed configuration UI in Superset */
- id: string
+ id: string;
/** The domain where Superset can be located, with protocol, such as:
https://superset.example.com */
- supersetDomain: string
+ supersetDomain: string;
/** The html element within which to mount the iframe */
- mountPoint: HTMLElement
+ mountPoint: HTMLElement;
/** A function to fetch a guest token from the Host App's backend server */
- fetchGuestToken: GuestTokenFetchFn
+ fetchGuestToken: GuestTokenFetchFn;
/** The dashboard UI config: hideTitle, hideTab, hideChartControls,
filters.visible, filters.expanded **/
- dashboardUiConfig?: UiConfigType
+ dashboardUiConfig?: UiConfigType;
/** Are we in debug mode? */
- debug?: boolean
+ debug?: boolean;
/** The iframe title attribute */
- iframeTitle?: string
+ iframeTitle?: string;
/** additional iframe sandbox attributes ex (allow-top-navigation,
allow-popups-to-escape-sandbox) **/
- iframeSandboxExtras?: string[]
-}
+ iframeSandboxExtras?: string[];
+ /** iframe allow attribute for Permissions Policy (e.g., ['clipboard-write',
'fullscreen']) **/
+ iframeAllowExtras?: string[];
+ /** Callback to resolve permalink URLs. If provided, this will be called
when generating permalinks
+ * to allow the host app to customize the URL. If not provided, Superset's
default URL is used. */
+ resolvePermalinkUrl?: ResolvePermalinkUrlFn;
+};
export type Size = {
- width: number, height: number
-}
+ width: number;
+ height: number;
+};
+
+/**
+ * Callback to resolve permalink URLs.
+ * Receives the permalink key and returns the full URL to use for the
permalink.
+ */
+export type ResolvePermalinkUrlFn = (params: {
+ /** The permalink key (e.g., "xyz789") */
+ key: string;
+}) => string | Promise<string>;
export type EmbeddedDashboard = {
- getScrollSize: () => Promise<Size>
- unmount: () => void
- getDashboardPermalink: (anchor: string) => Promise<string>
- getActiveTabs: () => Promise<string[]>
-}
+ getScrollSize: () => Promise<Size>;
+ unmount: () => void;
+ getDashboardPermalink: (anchor: string) => Promise<string>;
+ getActiveTabs: () => Promise<string[]>;
+};
/**
* Embeds a Superset dashboard into the page using an iframe.
@@ -88,7 +103,9 @@ export async function embedDashboard({
dashboardUiConfig,
debug = false,
iframeTitle = "Embedded Dashboard",
- iframeSandboxExtras = []
+ iframeSandboxExtras = [],
+ iframeAllowExtras = [],
+ resolvePermalinkUrl,
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
function log(...info: unknown[]) {
if (debug) {
@@ -96,40 +113,52 @@ export async function embedDashboard({
}
}
- log('embedding');
+ log("embedding");
if (supersetDomain.endsWith("/")) {
supersetDomain = supersetDomain.slice(0, -1);
}
function calculateConfig() {
- let configNumber = 0
- if(dashboardUiConfig) {
- if(dashboardUiConfig.hideTitle) {
- configNumber += 1
+ let configNumber = 0;
+ if (dashboardUiConfig) {
+ if (dashboardUiConfig.hideTitle) {
+ configNumber += 1;
}
- if(dashboardUiConfig.hideTab) {
- configNumber += 2
+ if (dashboardUiConfig.hideTab) {
+ configNumber += 2;
}
- if(dashboardUiConfig.hideChartControls) {
- configNumber += 8
+ if (dashboardUiConfig.hideChartControls) {
+ configNumber += 8;
}
}
- return configNumber
+ return configNumber;
}
async function mountIframe(): Promise<Switchboard> {
- return new Promise(resolve => {
- const iframe = document.createElement('iframe');
- const dashboardConfigUrlParams = dashboardUiConfig ? {uiConfig:
`${calculateConfig()}`} : undefined;
- const filterConfig = dashboardUiConfig?.filters || {}
- const filterConfigKeys = Object.keys(filterConfig)
- const filterConfigUrlParams = Object.fromEntries(filterConfigKeys.map(
- key => [DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY[key],
filterConfig[key]]))
+ return new Promise((resolve) => {
+ const iframe = document.createElement("iframe");
+ const dashboardConfigUrlParams = dashboardUiConfig
+ ? { uiConfig: `${calculateConfig()}` }
+ : undefined;
+ const filterConfig = dashboardUiConfig?.filters || {};
+ const filterConfigKeys = Object.keys(filterConfig);
+ const filterConfigUrlParams = Object.fromEntries(
+ filterConfigKeys.map((key) => [
+ DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY[key],
+ filterConfig[key],
+ ])
+ );
// Allow url query parameters from dashboardUiConfig.urlParams to
override the ones from filterConfig
- const urlParams = {...dashboardConfigUrlParams,
...filterConfigUrlParams, ...dashboardUiConfig?.urlParams}
- const urlParamsString = Object.keys(urlParams).length ? '?' + new
URLSearchParams(urlParams).toString() : ''
+ const urlParams = {
+ ...dashboardConfigUrlParams,
+ ...filterConfigUrlParams,
+ ...dashboardUiConfig?.urlParams,
+ };
+ const urlParamsString = Object.keys(urlParams).length
+ ? "?" + new URLSearchParams(urlParams).toString()
+ : "";
// set up the iframe's sandbox configuration
iframe.sandbox.add("allow-same-origin"); // needed for postMessage to
work
@@ -144,7 +173,7 @@ export async function embedDashboard({
});
// add the event listener before setting src, to be 100% sure that we
capture the load event
- iframe.addEventListener('load', () => {
+ iframe.addEventListener("load", () => {
// MessageChannel allows us to send and receive messages smoothly
between our window and the iframe
// See
https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API
const commsChannel = new MessageChannel();
@@ -157,18 +186,27 @@ export async function embedDashboard({
iframe.contentWindow!.postMessage(
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: "port transfer" },
supersetDomain,
- [theirPort],
- )
- log('sent message channel to the iframe');
+ [theirPort]
+ );
+ log("sent message channel to the iframe");
// return our port from the promise
- resolve(new Switchboard({ port: ourPort, name:
'superset-embedded-sdk', debug }));
+ resolve(
+ new Switchboard({
+ port: ourPort,
+ name: "superset-embedded-sdk",
+ debug,
+ })
+ );
});
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
iframe.title = iframeTitle;
+ if (iframeAllowExtras.length > 0) {
+ iframe.setAttribute("allow", iframeAllowExtras.join("; "));
+ }
//@ts-ignore
mountPoint.replaceChildren(iframe);
- log('placed the iframe')
+ log("placed the iframe");
});
}
@@ -177,27 +215,45 @@ export async function embedDashboard({
mountIframe(),
]);
- ourPort.emit('guestToken', { guestToken });
- log('sent guest token');
+ ourPort.emit("guestToken", { guestToken });
+ log("sent guest token");
async function refreshGuestToken() {
const newGuestToken = await fetchGuestToken();
- ourPort.emit('guestToken', { guestToken: newGuestToken });
+ ourPort.emit("guestToken", { guestToken: newGuestToken });
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
}
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));
+ // Register the resolvePermalinkUrl method for the iframe to call
+ // Returns null if no callback provided or on error, allowing iframe to use
default URL
+ ourPort.start();
+ ourPort.defineMethod(
+ "resolvePermalinkUrl",
+ async ({ key }: { key: string }): Promise<string | null> => {
+ if (!resolvePermalinkUrl) {
+ return null;
+ }
+ try {
+ return await resolvePermalinkUrl({ key });
+ } catch (error) {
+ log("Error in resolvePermalinkUrl callback:", error);
+ return null;
+ }
+ }
+ );
+
function unmount() {
- log('unmounting');
+ log("unmounting");
//@ts-ignore
mountPoint.replaceChildren();
}
- const getScrollSize = () => ourPort.get<Size>('getScrollSize');
+ const getScrollSize = () => ourPort.get<Size>("getScrollSize");
const getDashboardPermalink = (anchor: string) =>
- ourPort.get<string>('getDashboardPermalink', { anchor });
- const getActiveTabs = () => ourPort.get<string[]>('getActiveTabs')
+ ourPort.get<string>("getDashboardPermalink", { anchor });
+ const getActiveTabs = () => ourPort.get<string[]>("getActiveTabs");
return {
getScrollSize,
diff --git
a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
index d36a43cfab..3036118a3d 100644
--- a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
+++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
@@ -52,13 +52,15 @@ export default function URLShortLinkButton({
const getCopyUrl = async () => {
try {
- const url = await getDashboardPermalink({
+ const result = await getDashboardPermalink({
dashboardId,
dataMask,
activeTabs,
anchor: anchorLinkId,
});
- setShortUrl(url);
+ if (result?.url) {
+ setShortUrl(result.url);
+ }
} catch (error) {
if (error) {
addDangerToast(
diff --git
a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
index 6c5468da24..019ea4d7d4 100644
--- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
+++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
@@ -64,12 +64,16 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
);
async function generateUrl() {
- return getDashboardPermalink({
+ const result = await getDashboardPermalink({
dashboardId,
dataMask,
activeTabs,
anchor: dashboardComponentId,
});
+ if (!result?.url) {
+ throw new Error('Failed to generate permalink URL');
+ }
+ return result.url;
}
async function onCopyLink() {
diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx
b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
index fb8bb6c05d..bb34e3dea3 100644
--- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx
+++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
@@ -167,10 +167,11 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug
}: PageProps) => {
// activeTabs is initialized with undefined so that it doesn't override
// the currently stored value when hydrating
let activeTabs: string[] | undefined;
+ let anchor: string | undefined;
if (permalinkKey) {
const permalinkValue = await getPermalinkValue(permalinkKey);
- if (permalinkValue) {
- ({ dataMask, activeTabs } = permalinkValue.state);
+ if (permalinkValue?.state) {
+ ({ dataMask, activeTabs, anchor } = permalinkValue.state);
}
} else if (nativeFilterKeyValue) {
dataMask = await getFilterValue(id, nativeFilterKeyValue);
@@ -192,6 +193,17 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }:
PageProps) => {
dataMask,
}),
);
+
+ // Scroll to anchor element if specified in permalink state
+ if (anchor) {
+ // Use setTimeout to ensure the DOM has been updated after hydration
+ setTimeout(() => {
+ const element = document.getElementById(anchor);
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, 0);
+ }
}
return null;
}
diff --git a/superset-frontend/src/embedded/api.tsx
b/superset-frontend/src/embedded/api.tsx
index 9d37daf2e0..1b097394e4 100644
--- a/superset-frontend/src/embedded/api.tsx
+++ b/superset-frontend/src/embedded/api.tsx
@@ -51,12 +51,14 @@ const getDashboardPermalink = async ({
activeTabs: state.dashboardState?.activeTabs,
};
- return getDashboardPermalinkUtil({
+ const { url } = await getDashboardPermalinkUtil({
dashboardId,
dataMask,
activeTabs,
anchor,
});
+
+ return url;
};
const getActiveTabs = () => store?.getState()?.dashboardState?.activeTabs ||
[];
diff --git a/superset-frontend/src/explore/components/EmbedCodeContent.jsx
b/superset-frontend/src/explore/components/EmbedCodeContent.jsx
index 19ccfeb54c..da3a614930 100644
--- a/superset-frontend/src/explore/components/EmbedCodeContent.jsx
+++ b/superset-frontend/src/explore/components/EmbedCodeContent.jsx
@@ -49,9 +49,11 @@ const EmbedCodeContent = ({ formData, addDangerToast }) => {
const updateUrl = useCallback(() => {
setUrl('');
getChartPermalink(formData)
- .then(url => {
- setUrl(url);
- setErrorMessage('');
+ .then(result => {
+ if (result?.url) {
+ setUrl(result.url);
+ setErrorMessage('');
+ }
})
.catch(() => {
setErrorMessage(t('Error'));
diff --git
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index 21cf69cec4..df9df5bd6b 100644
---
a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++
b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -139,8 +139,13 @@ export const useExploreAdditionalActionsMenu = (
const shareByEmail = useCallback(async () => {
try {
const subject = t('Superset Chart');
- const url = await getChartPermalink(latestQueryFormData);
- const body = encodeURIComponent(t('%s%s', 'Check out this chart: ',
url));
+ const result = await getChartPermalink(latestQueryFormData);
+ if (!result?.url) {
+ throw new Error('Failed to generate permalink');
+ }
+ const body = encodeURIComponent(
+ t('%s%s', 'Check out this chart: ', result.url),
+ );
window.location.href = `mailto:?Subject=${subject}%20&Body=${body}`;
} catch (error) {
addDangerToast(t('Sorry, something went wrong. Try again later.'));
@@ -201,7 +206,13 @@ export const useExploreAdditionalActionsMenu = (
if (!latestQueryFormData) {
throw new Error();
}
- await copyTextToClipboard(() => getChartPermalink(latestQueryFormData));
+ await copyTextToClipboard(async () => {
+ const result = await getChartPermalink(latestQueryFormData);
+ if (!result?.url) {
+ throw new Error('Failed to generate permalink');
+ }
+ return result.url;
+ });
addSuccessToast(t('Copied to clipboard!'));
} catch (error) {
addDangerToast(t('Sorry, something went wrong. Try again later.'));
diff --git a/superset-frontend/src/utils/urlUtils.ts
b/superset-frontend/src/utils/urlUtils.ts
index 80e8948d22..2b3ee0b25a 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -22,6 +22,7 @@ import {
QueryFormData,
SupersetClient,
} from '@superset-ui/core';
+import Switchboard from '@superset-ui/switchboard';
import rison from 'rison';
import { isEmpty } from 'lodash';
import {
@@ -31,6 +32,7 @@ import {
} from '../constants';
import { getActiveFilters } from '../dashboard/util/activeDashboardFilters';
import serializeActiveFilterValues from
'../dashboard/util/serializeActiveFilterValues';
+import getBootstrapData from './getBootstrapData';
export type UrlParamType = 'string' | 'number' | 'boolean' | 'object' |
'rison';
export type UrlParam = (typeof URL_PARAMS)[keyof typeof URL_PARAMS];
@@ -139,24 +141,69 @@ export function getDashboardUrlParams(
return getUrlParamEntries(urlParams);
}
-function getPermalink(endpoint: string, jsonPayload: JsonObject) {
+export type PermalinkResult = {
+ key: string;
+ url: string;
+};
+
+function getPermalink(
+ endpoint: string,
+ jsonPayload: JsonObject,
+): Promise<PermalinkResult> {
return SupersetClient.post({
endpoint,
jsonPayload,
- }).then(result => result.json.url as string);
+ }).then(result => ({
+ key: result.json.key as string,
+ url: result.json.url as string,
+ }));
}
-export function getChartPermalink(
+/**
+ * Resolves a permalink URL using the host app's custom callback if in
embedded mode.
+ * Falls back to the default URL if not embedded or if no callback is provided.
+ */
+async function resolvePermalinkUrl(
+ result: PermalinkResult,
+): Promise<PermalinkResult> {
+ const { key, url } = result;
+
+ // In embedded mode, check if the host app has a custom resolvePermalinkUrl
callback
+ const bootstrapData = getBootstrapData();
+ if (bootstrapData.embedded) {
+ try {
+ // Ask the SDK to resolve the permalink URL
+ // Returns null if no callback was provided by the host
+ const resolvedUrl = await Switchboard.get<string | null>(
+ 'resolvePermalinkUrl',
+ { key },
+ );
+
+ // If callback returned a valid URL string, use it; otherwise use
Superset's default URL
+ if (typeof resolvedUrl === 'string' && resolvedUrl.length > 0) {
+ return { key, url: resolvedUrl };
+ }
+ } catch (error) {
+ // Silently fall back to default URL if Switchboard call fails
+ // (e.g., if not in embedded context or callback throws)
+ }
+ }
+
+ return { key, url };
+}
+
+export async function getChartPermalink(
formData: Pick<QueryFormData, 'datasource'>,
excludedUrlParams?: string[],
-) {
- return getPermalink('/api/v1/explore/permalink', {
+): Promise<PermalinkResult> {
+ const result = await getPermalink('/api/v1/explore/permalink', {
formData,
urlParams: getChartUrlParams(excludedUrlParams),
});
+ return resolvePermalinkUrl(result);
}
-export function getDashboardPermalink({
+export async function getDashboardPermalink({
dashboardId,
dataMask,
activeTabs,
@@ -176,14 +223,19 @@ export function getDashboardPermalink({
* and highlighted upon page load.
*/
anchor?: string;
-}) {
- // only encode filter state if non-empty
- return getPermalink(`/api/v1/dashboard/${dashboardId}/permalink`, {
+}): Promise<PermalinkResult> {
+ const payload: JsonObject = {
urlParams: getDashboardUrlParams(),
dataMask,
activeTabs,
anchor,
- });
+ };
+
+ const result = await getPermalink(
+ `/api/v1/dashboard/${dashboardId}/permalink`,
+ payload,
+ );
+ return resolvePermalinkUrl(result);
}
const externalUrlRegex =