Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(embedded): Embed Charts using GuestToken similar to SIP-75 #30076

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions superset-frontend/src/dashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ export type EmbeddedDashboard = {
allowed_domains: string[];
};

export type EmbeddedChart = {
uuid: string;
chart_id: string;
allowed_domains: string[];
};

export type Slice = {
slice_id: number;
slice_name: string;
Expand Down
41 changes: 29 additions & 12 deletions superset-frontend/src/embedded/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,39 @@ const LazyDashboardPage = lazy(
),
);

const EmbeddedRoute = () => (
<Suspense fallback={<Loading />}>
<RootContextProviders>
<ErrorBoundary>
<LazyDashboardPage idOrSlug={bootstrapData.embedded!.dashboard_id} />
</ErrorBoundary>
<ToastContainer position="top" />
</RootContextProviders>
</Suspense>
const LazyChartPage = lazy(
() => import(/* webpackChunkName: "Chart" */ 'src/pages/Chart'),
);

const EmbeddedRoute = () =>
bootstrapData.embedded!.dashboard_id ? (
<Suspense fallback={<Loading />}>
<RootContextProviders>
<ErrorBoundary>
{(bootstrapData.embedded!.dashboard_id && (
<LazyDashboardPage
idOrSlug={bootstrapData.embedded!.dashboard_id}
/>
)) ||
(bootstrapData.embedded!.chart_id && <LazyChartPage />)}
</ErrorBoundary>
<ToastContainer position="top" />
</RootContextProviders>
</Suspense>
) : (
<Suspense fallback={<Loading />} />
);

const EmbeddedApp = () => (
<Router>
{/* todo (embedded) remove this line after uuids are deployed */}
<Route path="/dashboard/:idOrSlug/embedded/" component={EmbeddedRoute} />
<Route path="/embedded/:uuid/" component={EmbeddedRoute} />
<Route
path={[
'/embedded/:uuid/',
'/embedded/dashboard/:uuid/',
'/embedded/chart/:uuid/',
]}
component={EmbeddedRoute}
/>
</Router>
);

Expand Down
4 changes: 3 additions & 1 deletion superset-frontend/src/explore/actions/hydrateExplore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ export const hydrateExplore =
form_data: initialFormData,
slice: initialSlice,
controlsTransferred: explore.controlsTransferred,
standalone: getUrlParam(URL_PARAMS.standalone),
// TODO is there a better way?
standalone:
getUrlParam(URL_PARAMS.standalone) ?? getUrlParam(URL_PARAMS.uiConfig),
force: getUrlParam(URL_PARAMS.force),
metadata,
saveAction,
Expand Down
211 changes: 211 additions & 0 deletions superset-frontend/src/explore/components/ChartEmbedControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* 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 React, { useCallback, useEffect, useState } from 'react';

Check failure on line 19 in superset-frontend/src/explore/components/ChartEmbedControls.tsx

View workflow job for this annotation

GitHub Actions / frontend-build

Default React import is not required due to automatic JSX runtime in React 16.4
import { makeApi, styled, SupersetApiError, t } from '@superset-ui/core';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
import { Chart, EmbeddedChart } from '../../dashboard/types';
import { Input } from '../../components/Input';
import { FormItem } from '../../components/Form';
import Button from '../../components/Button';
import { useToasts } from '../../components/MessageToasts/withToasts';
import Modal from '../../components/Modal';
import Loading from '../../components/Loading';

// TODO is it possible to reuse code from Dashboard's embed?
const ButtonRow = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
`;

const stringToList = (stringyList: string): string[] =>
stringyList.split(/(?:\s|,)+/).filter(x => x);

type EmbeddedApiPayload = { allowed_domains: string[] };

type Props = {
chart: Chart;
};

const ChartEmbedControls = ({ chart }: Props) => {
const { addInfoToast, addDangerToast } = useToasts();
const [ready, setReady] = useState(true); // whether we have initialized yet
const [loading, setLoading] = useState(false); // whether we are currently doing an async thing
const [embedded, setEmbedded] = useState<EmbeddedChart | null>(null); // the embedded dashboard config
const [allowedDomains, setAllowedDomains] = useState<string>('');

const endpoint = `/api/v1/chart/${chart.id}/embedded`;

const enableEmbedded = useCallback(() => {
setLoading(true);
makeApi<EmbeddedApiPayload, { result: EmbeddedChart }>({
method: 'POST',
endpoint,
})({
allowed_domains: stringToList(allowedDomains),
})
.then(
({ result }) => {
setEmbedded(result);
setAllowedDomains(result.allowed_domains.join(', '));
addInfoToast(t('Changes saved.'));
},
err => {
console.error(err);
addDangerToast(
t(
t('Sorry, something went wrong. The changes could not be saved.'),
),
);
},
)
.finally(() => {
setLoading(false);
});
}, [endpoint, allowedDomains]);

const disableEmbedded = useCallback(() => {
Modal.confirm({
title: t('Disable embedding?'),
content: t('This will remove your current embed configuration.'),
okType: 'danger',
onOk: () => {
setLoading(true);
makeApi<{}>({ method: 'DELETE', endpoint })({})
.then(
() => {
setEmbedded(null);
setAllowedDomains('');
addInfoToast(t('Embedding deactivated.'));
// TODO onHide();
},
err => {
console.error(err);
addDangerToast(
t(
'Sorry, something went wrong. Embedding could not be deactivated.',
),
);
},
)
.finally(() => {
setLoading(false);
});
},
});
}, [endpoint]);

useEffect(() => {
setReady(false);
makeApi<{}, { result: EmbeddedChart }>({
method: 'GET',
endpoint,
})({})
.catch(err => {
if ((err as SupersetApiError).status === 404) {
// 404 just means the dashboard isn't currently embedded
return { result: null };
}
addDangerToast(t('Sorry, something went wrong. Please try again.'));
throw err;
})
.then(({ result }) => {
setReady(true);
setEmbedded(result);
setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
});
}, [chart]);

if (!ready) {
return <Loading />;
}

const isDirty =
!embedded ||
stringToList(allowedDomains).join() !== embedded.allowed_domains.join();

return (
<>
{embedded ? (
<p>
{t(
'This dashboard is ready to embed. In your application, pass the following id to the SDK:',
)}
<br />
<code>{embedded.uuid}</code>
</p>
) : (
<p>
{t(
'Configure this chart to embed it into an external web application.',
)}
</p>
)}
<h3>{t('Settings')}</h3>
<FormItem>
<label htmlFor="allowed-domains">
{t('Allowed Domains (comma separated)')}{' '}
<InfoTooltipWithTrigger
tooltip={t(
'A list of domain names that can embed this dashboard. Leaving this field empty will allow embedding from any domain.',
)}
/>
</label>
<Input
name="allowed-domains"
id="allowed-domains"
value={allowedDomains}
placeholder="superset.example.com"
onChange={event => setAllowedDomains(event.target.value)}
/>
</FormItem>
<ButtonRow>
{embedded ? (
<>
<Button
onClick={disableEmbedded}
buttonStyle="secondary"
loading={loading}
>
{t('Deactivate')}
</Button>
<Button
onClick={enableEmbedded}
buttonStyle="primary"
disabled={!isDirty}
loading={loading}
>
{t('Save changes')}
</Button>
</>
) : (
<Button
onClick={enableEmbedded}
buttonStyle="primary"
loading={loading}
>
{t('Enable embedding')}
</Button>
)}
</ButtonRow>
</>
);
};

export default ChartEmbedControls;
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import DashboardsSubMenu from './DashboardsSubMenu';
import ChartEmbedControls from '../ChartEmbedControls';

const MENU_KEYS = {
EDIT_PROPERTIES: 'edit_properties',
Expand Down Expand Up @@ -383,9 +384,9 @@ export const useExploreAdditionalActionsMenu = (
<Menu.Item key={MENU_KEYS.EMBED_CODE}>
<ModalTrigger
triggerNode={
<span data-test="embed-code-button">{t('Embed code')}</span>
<span data-test="embed-code-button">{t('Embed iframe')}</span>
}
modalTitle={t('Embed code')}
modalTitle={t('Embed using iframe code')}
modalBody={
<EmbedCodeContent
formData={latestQueryFormData}
Expand All @@ -398,6 +399,22 @@ export const useExploreAdditionalActionsMenu = (
/>
</Menu.Item>
) : null}
{isFeatureEnabled(FeatureFlag.EmbeddedSuperset) ? (
<Menu.Item key={MENU_KEYS.EMBED_CODE}>
<ModalTrigger
triggerNode={
<span data-test="embed-chart-button">{t('Embed chart')}</span>
}
modalTitle={t('Embed chart using guest token')}
modalBody={<ChartEmbedControls chart={chart} />}
maxWidth={`${theme.gridUnit * 100}px`}
destroyOnClose
responsive
draggable
resizable
/>
</Menu.Item>
) : null}
</Menu.SubMenu>
<Menu.Divider />
{showReportSubMenu ? (
Expand Down
3 changes: 2 additions & 1 deletion superset-frontend/src/types/bootstrapTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ export interface BootstrapData {
common: CommonBootstrapData;
config?: any;
embedded?: {
dashboard_id: string;
dashboard_id?: string;
chart_id?: string;
};
requested_query?: JsonObject;
}
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/views/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const CombinedDatasourceReducers = (
const reducers = {
sqlLab: sqlLabReducer,
localStorageUsageInKilobytes: noopReducer(0),
embedded: noopReducer(0),
messageToasts: messageToastReducer,
common: noopReducer(bootstrapData.common),
user: userReducer,
Expand Down
Loading
Loading