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 dashboard configuration #19364

Merged
merged 29 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
eb0e4a9
embedded dashboard model
suddjian Mar 23, 2022
0732aca
embedded dashboard endpoints
suddjian Mar 25, 2022
d718e6f
DRY up using the with_dashboard decorator elsewhere
suddjian Mar 25, 2022
16f08d0
wip
suddjian Mar 26, 2022
5f431b5
check feature flags and permissions
suddjian Mar 28, 2022
451e47d
wip
suddjian Mar 29, 2022
bbdee14
sdk
suddjian Mar 29, 2022
8051d99
urls
suddjian Mar 29, 2022
703a076
dao option for id column
suddjian Mar 29, 2022
7e5cb37
got it working
suddjian Mar 29, 2022
4250165
Update superset/embedded/view.py
suddjian Mar 29, 2022
c50e920
use the curator check
suddjian Mar 29, 2022
ae9a873
Merge branch 'embedded-setup-ui' of github.com:preset-io/incubator-su…
suddjian Mar 29, 2022
e181fba
put back old endpoint, for now
suddjian Mar 29, 2022
6d03361
allow access by either embedded.uuid or dashboard.id
suddjian Mar 29, 2022
fd543a4
keep the old endpoint around, for the time being
suddjian Mar 29, 2022
9e1762b
Merge branch 'master' into embedded-setup-ui
suddjian Mar 30, 2022
d704ecd
openapi
suddjian Mar 30, 2022
5d7546a
lint
suddjian Mar 30, 2022
2180db2
lint
suddjian Mar 30, 2022
4bbbd72
lint
suddjian Mar 30, 2022
86ff579
test stuff
suddjian Mar 30, 2022
b05ca22
lint, test
suddjian Mar 30, 2022
4d713bf
typo
suddjian Mar 30, 2022
4237402
Update superset-frontend/src/embedded/index.tsx
suddjian Mar 30, 2022
067b0b4
Update superset-frontend/src/embedded/index.tsx
suddjian Mar 30, 2022
7edb89f
fix tests
suddjian Mar 30, 2022
54abe55
Merge branch 'embedded-setup-ui' of github.com:preset-io/superset int…
suddjian Mar 30, 2022
62b16f8
bump sdk
suddjian Mar 30, 2022
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
2 changes: 1 addition & 1 deletion superset-embedded-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export async function embedDashboard({
resolve(new Switchboard({ port: ourPort, name: 'superset-embedded-sdk', debug }));
});

iframe.src = `${supersetDomain}/dashboard/${id}/embedded${dashboardConfig}`;
iframe.src = `${supersetDomain}/embedded/${id}${dashboardConfig}`;
mountPoint.replaceChildren(iframe);
log('placed the iframe')
});
Expand Down
227 changes: 227 additions & 0 deletions superset-frontend/src/dashboard/components/DashboardEmbedControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* 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';
import { makeApi, styled, SupersetApiError, t } from '@superset-ui/core';
import Modal from 'src/components/Modal';
import Loading from 'src/components/Loading';
import Button from 'src/components/Button';
import { EmbeddedDashboard } from '../types';
import { Input } from 'src/components/Input';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { FormItem } from 'src/components/Form';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';

type Props = {
dashboardId: string;
show: boolean;
onHide: () => void;
};

type EmbeddedApiPayload = { allowed_domains: string[] };

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

const ButtonRow = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
`;

export const DashboardEmbedControls = ({ dashboardId, onHide }: 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<EmbeddedDashboard | null>(null); // the embedded dashboard config
const [allowedDomains, setAllowedDomains] = useState<string>('');

const endpoint = `/api/v1/dashboard/${dashboardId}/embedded`;
// whether saveable changes have been made to the config
const isDirty =
!embedded ||
stringToList(allowedDomains).join() !== embedded.allowed_domains.join();

const enableEmbedded = useCallback(() => {
setLoading(true);
makeApi<EmbeddedApiPayload, { result: EmbeddedDashboard }>({
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.'));
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: EmbeddedDashboard }>({
method: 'GET',
endpoint,
})({})
.catch(err => {
if ((err as SupersetApiError).status === 404) {
// 404 just means the dashboard isn't currently embedded
return { result: null };
}
throw err;
})
.then(({ result }) => {
setReady(true);
setEmbedded(result);
setAllowedDomains(result ? result.allowed_domains.join(', ') : '');
});
}, [dashboardId]);

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

return (
<>
<p>
{embedded ? (
<>
{t(
'This dashboard is ready to embed. In your application, pass the following id to the SDK:',
)}
<br />
<code>{embedded.uuid}</code>
</>
) : (
t(
'Configure this dashboard to embed it into an external web application.',
)
)}
</p>
<p>
{t('For further instructions, consult the')}{' '}
<a
href="https://www.npmjs.com/package/@superset-ui/embedded-sdk"
target="_blank"
>
{t('Superset Embedded SDK documentation.')}
</a>
</p>
<h3>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"
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 const DashboardEmbedModal = (props: Props) => {
const { show, onHide } = props;

return (
<Modal show={show} onHide={onHide} title={t('Embed')} hideFooter>
<DashboardEmbedControls {...props} />
</Modal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ const propTypes = {
userCanEdit: PropTypes.bool.isRequired,
userCanShare: PropTypes.bool.isRequired,
userCanSave: PropTypes.bool.isRequired,
userCanCurate: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
layout: PropTypes.object.isRequired,
expandedSlices: PropTypes.object.isRequired,
onSave: PropTypes.func.isRequired,
showPropertiesModal: PropTypes.func.isRequired,
manageEmbedded: PropTypes.func.isRequired,
refreshLimit: PropTypes.number,
refreshWarning: PropTypes.string,
lastModifiedTime: PropTypes.number.isRequired,
Expand All @@ -88,6 +90,7 @@ const MENU_KEYS = {
EDIT_CSS: 'edit-css',
DOWNLOAD_AS_IMAGE: 'download-as-image',
TOGGLE_FULLSCREEN: 'toggle-fullscreen',
MANAGE_EMBEDDED: 'manage-embedded',
};

const DropdownButton = styled.div`
Expand Down Expand Up @@ -182,6 +185,10 @@ class HeaderActionsDropdown extends React.PureComponent {
window.location.replace(url);
break;
}
case MENU_KEYS.MANAGE_EMBEDDED: {
this.props.manageEmbedded();
break;
}
default:
break;
}
Expand All @@ -204,6 +211,7 @@ class HeaderActionsDropdown extends React.PureComponent {
userCanEdit,
userCanShare,
userCanSave,
userCanCurate,
isLoading,
refreshLimit,
refreshWarning,
Expand Down Expand Up @@ -313,6 +321,12 @@ class HeaderActionsDropdown extends React.PureComponent {
</Menu.Item>
)}

{!editMode && userCanCurate && (
<Menu.Item key={MENU_KEYS.MANAGE_EMBEDDED}>
{t('Embed dashboard')}
</Menu.Item>
)}

{!editMode && (
<Menu.Item key={MENU_KEYS.DOWNLOAD_AS_IMAGE}>
{t('Download as image')}
Expand Down
24 changes: 24 additions & 0 deletions superset-frontend/src/dashboard/components/Header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ import setPeriodicRunner, {
} from 'src/dashboard/util/setPeriodicRunner';
import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal';
import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
import Modal from 'src/components/Modal';
import { DashboardEmbedModal } from '../DashboardEmbedControls';
import findPermission from 'src/dashboard/util/findPermission';

const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
Expand Down Expand Up @@ -420,6 +423,14 @@ class Header extends React.PureComponent {
this.setState({ showingReportModal: false });
}

showEmbedModal = () => {
this.setState({ showingEmbedModal: true });
};

hideEmbedModal = () => {
this.setState({ showingEmbedModal: false });
};

renderReportModal() {
const attachedReportExists = !!Object.keys(this.props.reports).length;
return attachedReportExists ? (
Expand Down Expand Up @@ -498,6 +509,9 @@ class Header extends React.PureComponent {
const userCanSaveAs =
dashboardInfo.dash_save_perm &&
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING;
const userCanCurate =
isFeatureEnabled(FeatureFlag.EMBEDDED_SUPERSET) &&
findPermission('can_set_embedded', 'Dashboard', user.roles);
const shouldShowReport = !editMode && this.canAddReports();
const refreshLimit =
dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
Expand Down Expand Up @@ -658,6 +672,14 @@ class Header extends React.PureComponent {
/>
)}

{isFeatureEnabled(FeatureFlag.EMBEDDED_SUPERSET) && (
suddjian marked this conversation as resolved.
Show resolved Hide resolved
<DashboardEmbedModal
show={this.state.showingEmbedModal}
onHide={this.hideEmbedModal}
dashboardId={dashboardInfo.id}
/>
)}

<HeaderActionsDropdown
addSuccessToast={this.props.addSuccessToast}
addDangerToast={this.props.addDangerToast}
Expand All @@ -683,8 +705,10 @@ class Header extends React.PureComponent {
userCanEdit={userCanEdit}
userCanShare={userCanShare}
userCanSave={userCanSaveAs}
userCanCurate={userCanCurate}
isLoading={isLoading}
showPropertiesModal={this.showPropertiesModal}
manageEmbedded={this.showEmbedModal}
refreshLimit={refreshLimit}
refreshWarning={refreshWarning}
lastModifiedTime={lastModifiedTime}
Expand Down
8 changes: 5 additions & 3 deletions superset-frontend/src/dashboard/containers/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
} from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { Global } from '@emotion/react';
import { useParams } from 'react-router-dom';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import Loading from 'src/components/Loading';
import FilterBoxMigrationModal from 'src/dashboard/components/FilterBoxMigrationModal';
Expand Down Expand Up @@ -79,14 +78,17 @@ const DashboardContainer = React.lazy(

const originalDocumentTitle = document.title;

const DashboardPage: FC = () => {
type PageProps = {
idOrSlug: string;
};

export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
const dispatch = useDispatch();
const theme = useTheme();
const user = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
const { addDangerToast } = useToasts();
const { idOrSlug } = useParams<{ idOrSlug: string }>();
const { result: dashboard, error: dashboardApiError } =
useDashboard(idOrSlug);
const { result: charts, error: chartsApiError } =
Expand Down
Loading