mirror of https://github.com/apache/superset.git
feat: Embedded SDK (#18250)
* feat: embedded sdk
* correct values
* better version
* readme stuff
* release script
* doc
* oops
* better package description
* license
* that was invalid json
* Apply suggestions from code review
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
* Update superset-embedded-sdk/README.md
* a github workflow to make sure the build succeeds
* fix github workflows
* writing
* try a different trigger
* no point in a single unit matrix
* Revert "no point in a single unit matrix"
This reverts commit 90f78bfc98
.
* workflow changes
* fix some scripts
* pull request types
* slight rename
* test list
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
92cc7c0e7d
commit
1c2936ba7b
|
@ -0,0 +1,23 @@
|
|||
name: Embedded SDK Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm run ci:release
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@ -0,0 +1,23 @@
|
|||
name: Embedded SDK PR Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "superset-embedded-sdk/**"
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
embedded-sdk-test:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-20.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
|
@ -0,0 +1,3 @@
|
|||
bundle
|
||||
dist
|
||||
lib
|
|
@ -0,0 +1,77 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Contributing to the Superset Embedded SDK
|
||||
|
||||
The superset-embedded-sdk directory is a self contained sub-project in the Superset codebase.
|
||||
|
||||
This is because the SDK has different requirements from other parts of the Superset codebase:
|
||||
Namely, we need to export a lightweight frontend library that can be used in as many environments as possible.
|
||||
Having separate configs allows for better separation of concerns and allows the SDK code to remain simple.
|
||||
|
||||
## Testing
|
||||
|
||||
The functions used in the sdk so far are very closely tied to browser behavior,
|
||||
and therefore are not easily unit-testable. We have instead opted to test the sdk behavior using end-to-end tests.
|
||||
This way, the tests can assert that the sdk actually mounts the iframe and communicates with it correctly.
|
||||
|
||||
At time of writing, these tests are not written yet, because we haven't yet put together the demo app that they will leverage.
|
||||
### Things to e2e test once we have a demo app:
|
||||
|
||||
**happy path:**
|
||||
|
||||
fetch valid guest token and pass it to the sdk, verify that the dashboard shows up
|
||||
|
||||
**security:**
|
||||
|
||||
it should fail if you pass a fake guest token
|
||||
it should fail if your guest token doesn't have permission to access this resource
|
||||
it should apply rls filters correctly
|
||||
it should not apply rls filters to a dataset that isn't included
|
||||
|
||||
**edge cases:**
|
||||
|
||||
what happens if superset is offline
|
||||
what happens if the superset domain is invalid or incorrect
|
||||
what happens if dashboard id doesn't exist
|
||||
|
||||
## Publishing
|
||||
|
||||
To publish a new version, first determine whether it will be a major/minor/patch version according to [semver rules](https://semver.org/).
|
||||
Run `npm version [major|minor|patch]`, and include the resulting version change in your PR.
|
||||
|
||||
Building the package and publishing to npm will be handled by github actions automatically on merge to master,
|
||||
provided that the currently specified package version isn't already published.
|
||||
|
||||
## Building
|
||||
|
||||
Builds are handled by CI, so there is no need to run the build yourself unless you are curious about it.
|
||||
|
||||
The library is built in two modes: one for consumption by package managers
|
||||
and subsequent build systems, and one for consumption directly by a web browser.
|
||||
|
||||
Babel is used to build the sdk into a relatively modern js package in the `lib` directory.
|
||||
This is used by consumers who install the embedded sdk via npm, yarn, or other package manager.
|
||||
|
||||
Webpack is used to bundle the `bundle` directory,
|
||||
for use directly in the browser with no build step e.g. when importing via unpkg.
|
||||
|
||||
Typescript outputs type definition files to the `dist` directory.
|
||||
|
||||
Which of these outputs is used by the library consumer is determined by our package.json's `main`, `module`, and `types` fields.
|
|
@ -0,0 +1,90 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Superset Embedded SDK
|
||||
|
||||
The Embedded SDK allows you to embed dashboards from Superset into your own app,
|
||||
using your app's authentication.
|
||||
|
||||
Embedding is done by inserting an iframe, containing a Superset page, into the host application.
|
||||
|
||||
## Embedding a Dashboard
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save @superset-ui/embedded-sdk
|
||||
```
|
||||
|
||||
```js
|
||||
import { embedDashboard } from "@superset-ui/embedded-sdk";
|
||||
|
||||
embedDashboard({
|
||||
id: "abc123", // given by the Superset embedding UI
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint: document.getElementById("my-superset-container"), // any html element that can contain an iframe
|
||||
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
||||
});
|
||||
```
|
||||
|
||||
You can also load the Embedded SDK from a CDN. The SDK will be available as `supersetEmbeddedSdk` globally:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
|
||||
|
||||
<script>
|
||||
supersetEmbeddedSdk.embedDashboard({
|
||||
// ... here you supply the same parameters as in the example above
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## Authentication/Authorization with Guest Tokens
|
||||
|
||||
Embedded resources use a special auth token called a Guest Token to grant Superset access to your users,
|
||||
without requiring your users to log in to Superset directly. Your backend must create a Guest Token
|
||||
by requesting Superset's `POST /security/guest_token` endpoint, and pass that guest token to your frontend.
|
||||
|
||||
The Embedding SDK takes the guest token and use it to embed a dashboard.
|
||||
|
||||
### Creating a Guest Token
|
||||
|
||||
From the backend, http `POST` to `/security/guest_token` with some parameters to define what the guest token will grant access to.
|
||||
Guest tokens can have Row Level Security rules which filter data for the user carrying the token.
|
||||
|
||||
The agent making the `POST` request must be authenticated with the `can_grant_guest_token` permission.
|
||||
|
||||
Example `POST /security/guest_token` payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"username": "stan_lee",
|
||||
"first_name": "Stan",
|
||||
"last_name": "Lee"
|
||||
},
|
||||
"resources": [{
|
||||
"type": "dashboard",
|
||||
"id": "abc123"
|
||||
}],
|
||||
"rls": [
|
||||
{ "clause": "publisher = 'Nintendo'" }
|
||||
]
|
||||
}
|
||||
```
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-env"
|
||||
],
|
||||
sourceMaps: true,
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "@superset-ui/embedded-sdk",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"description": "SDK for embedding resources from Superset into your own application",
|
||||
"access": "public",
|
||||
"keywords": [
|
||||
"superset",
|
||||
"embed",
|
||||
"embedded",
|
||||
"sdk",
|
||||
"iframe",
|
||||
"dashboard",
|
||||
"chart",
|
||||
"analytics"
|
||||
],
|
||||
"files": [
|
||||
"bundle",
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"main": "bundle/index.js",
|
||||
"module": "lib/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc & babel src --out-dir lib --extensions '.ts,.tsx' & webpack --mode production",
|
||||
"ci:release": "node ./release-if-necessary.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 chrome versions",
|
||||
"last 3 firefox versions",
|
||||
"last 3 safari versions",
|
||||
"last 3 edge versions"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.8",
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"axios": "^0.25.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.67.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apache/superset.git"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset"
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const axios = require('axios');
|
||||
const { name, version } = require('./package.json');
|
||||
|
||||
(async () => {
|
||||
console.log(`checking if ${name}@${version} needs releasing`);
|
||||
|
||||
const packageUrl = `https://registry.npmjs.org/${name}/${version}`;
|
||||
// npm commands output a bunch of garbage in the edge cases,
|
||||
// and require sending semi-validated strings to the command line,
|
||||
// so let's just use good old http.
|
||||
const { status } = await axios.get(packageUrl, {
|
||||
validateStatus: (status) => true // we literally just want the status so any status is valid
|
||||
});
|
||||
|
||||
if (status === 200) {
|
||||
console.log('version already exists on npm, exiting');
|
||||
} else if (status === 404) {
|
||||
console.log('release required, building');
|
||||
execSync('npm run build');
|
||||
execSync('npm publish --access public');
|
||||
console.log(`published ${version} to npm`);
|
||||
} else {
|
||||
console.error(`ERROR: Received unexpected http status code ${status} from GET ${packageUrl}
|
||||
The embedded sdk release script might need to be fixed, or maybe you just need to try again later.`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const IFRAME_COMMS_MESSAGE_TYPE = "__embedded_comms__";
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { IFRAME_COMMS_MESSAGE_TYPE } from './const';
|
||||
|
||||
/**
|
||||
* The function to fetch a guest token from your Host App's backend server.
|
||||
* The Host App backend must supply an API endpoint
|
||||
* which returns a guest token with appropriate resource access.
|
||||
*/
|
||||
export type GuestTokenFetchFn = () => Promise<string>;
|
||||
|
||||
export type EmbedDashboardParams = {
|
||||
/** The id provided by the embed configuration UI in Superset */
|
||||
id: string
|
||||
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
|
||||
supersetDomain: string
|
||||
/** The html element within which to mount the iframe */
|
||||
mountPoint: HTMLElement
|
||||
/** A function to fetch a guest token from the Host App's backend server */
|
||||
fetchGuestToken: GuestTokenFetchFn
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a Superset dashboard into the page using an iframe.
|
||||
*/
|
||||
export async function embedDashboard({
|
||||
id,
|
||||
supersetDomain,
|
||||
mountPoint,
|
||||
fetchGuestToken
|
||||
}: EmbedDashboardParams) {
|
||||
function log(...info: unknown[]) {
|
||||
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
|
||||
}
|
||||
|
||||
log('embedding');
|
||||
|
||||
async function mountIframe(): Promise<MessagePort> {
|
||||
return new Promise(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
// setup the iframe's sandbox configuration
|
||||
iframe.sandbox.add("allow-same-origin"); // needed for postMessage to work
|
||||
iframe.sandbox.add("allow-scripts"); // obviously the iframe needs scripts
|
||||
iframe.sandbox.add("allow-presentation"); // for fullscreen charts
|
||||
// add these ones if it turns out we need them:
|
||||
// iframe.sandbox.add("allow-top-navigation");
|
||||
// iframe.sandbox.add("allow-forms");
|
||||
|
||||
// add the event listener before setting src, to be 100% sure that we capture the load event
|
||||
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();
|
||||
const ourPort = commsChannel.port1;
|
||||
const theirPort = commsChannel.port2;
|
||||
|
||||
// Send one of the message channel ports to the iframe to initialize embedded comms
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
|
||||
// we know the content window isn't null because we are in the load event handler.
|
||||
iframe.contentWindow!.postMessage(
|
||||
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: "port transfer" },
|
||||
supersetDomain,
|
||||
[theirPort],
|
||||
)
|
||||
log('sent message channel to the iframe');
|
||||
|
||||
// return our port from the promise
|
||||
resolve(ourPort);
|
||||
});
|
||||
|
||||
iframe.src = `${supersetDomain}/dashboard/${id}/embedded`;
|
||||
mountPoint.replaceChildren(iframe);
|
||||
log('placed the iframe')
|
||||
});
|
||||
}
|
||||
|
||||
const [guestToken, ourPort] = await Promise.all([
|
||||
fetchGuestToken(),
|
||||
mountIframe()
|
||||
]);
|
||||
|
||||
ourPort.postMessage({ guestToken });
|
||||
log('sent guest token');
|
||||
|
||||
function unmount() {
|
||||
log('unmounting');
|
||||
mountPoint.replaceChildren();
|
||||
}
|
||||
|
||||
return {
|
||||
unmount
|
||||
};
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// syntax rules
|
||||
"strict": true,
|
||||
|
||||
// environment
|
||||
"target": "es6",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"module": "esnext",
|
||||
|
||||
// output
|
||||
"outDir": "./dist",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true
|
||||
},
|
||||
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
"exclude": [
|
||||
"tests",
|
||||
"dist",
|
||||
"lib",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.ts',
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'bundle'),
|
||||
|
||||
// this exposes the library's exports under a global variable
|
||||
library: {
|
||||
name: "supersetEmbeddedSdk",
|
||||
type: "umd"
|
||||
}
|
||||
},
|
||||
devtool: "source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
// babel-loader is faster than ts-loader because it ignores types.
|
||||
// We do type checking in a separate process, so that's fine.
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts'],
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue