Motivation
I wanted to collect all the information I gathered that's necessary to do this in a single place. I also didn't find any examples utilising this tech stack, so I decided to share my own.
Below I'll go through all the code changes necessary to make embedding superset dashboards possible.
Superset instance changes
First of all, make sure your version is apache/superset:3.1.0 or lower. I wasn’t able to get it working at 3.1.1.
Embedded superset dashboard is under a feature flag, so we need to activate it by setting it to true.
superset_config.py:
FEATURE_FLAGS = {"EMBEDDED_SUPERSET": True}
TALISMAN_ENABLED = False
Configure the environment variable with a path for your configuration file above (superset_config.py), if you haven’t already:
SUPERSET_CONFIG_PATH: /home/webserver/app/superset_config.py
This variable should be part of your Superset’s instance environment. For example, I have it under the ‘environment' key of the definition in my docker compose:
superset:
image: apache/superset:3.1.0
ports:
- 8088:8088
environment:
SUPERSET_CONFIG_PATH: /home/webserver/app/superset_config.py
Back-end changes:
This was probably the most painful part with all the authentication it took. The break down of all the calls to get the guest token that we’re going to be usin in our templ script below ( to call superset’s embedded api) goes something like this:
- Get the access token
- Get the csrf token & cookie (!)
- Get the guest token
Easy peasy, right?
You can use your http framework/package of choice and look at superset’s API docs - https://superset.apache.org/docs/api/; your own instance also has docs that can be found at your-url/swagger/v1
.
So, the composite method returning the token will look something like this:
func AuthenticateAsGuest(resources []Resource) (string, error) {
//Get access token
accessTok, err := Login()
if err != nil {
return "", err
}
// Get CSRF token
csrfTok, cookies, err := GetCSRFToken(accessTok)
if err != nil {
return "", err
}
// Get guest token
guestTokenReq := GuestTokenRequest{
Resources: resources,
User: User{
Username: os.Getenv("SUPERSET_USERNAME"),
},
RLS: []RLS{},
}
guestToken, err := GetGuestToken(guestTokenReq, csrfTok, accessTok, cookies)
if err != nil {
return "", err
}
return guestToken, nil
}
Now let's take a look at all the methods that the above function consists of:
type LoginTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type Resource struct {
ID string `json:"id"`
Type string `json:"type"`
}
type User struct {
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// RLS represents a row-level security in the payload
type RLS struct {
Clause string `json:"clause"`
Dataset int `json:"dataset"`
}
// GuestTokenRequest represents the payload structure for /api/v1/security/guest_token
type GuestTokenRequest struct {
Resources []Resource `json:"resources"`
User User `json:"user"`
RLS []RLS `json:"rls"`
}
// Login calls /login endpoint in superset and returns an access token
func Login() (string, error) {
payload := map[string]interface{}{
"username": os.Getenv("SUPERSET_USERNAME"),
"password": os.Getenv("SUPERSET_PASSWORD"),
"provider": "db",
"refresh": true,
}
// Marshal payload to JSON
payloadBytes, err := json.Marshal(payload)
if err != nil {
log.Err(err).Msg("Error marshalling supersetLogin payload")
return "", err
}
// Make POST request
resp, err := http.Post(os.Getenv("SUPERSET_URL")+"/api/v1/security/login", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
log.Err(err).Msg("Error making superset Login POST request")
return "", err
}
defer resp.Body.Close()
var tokens LoginTokens
err = json.NewDecoder(resp.Body).Decode(&tokens)
if err != nil {
log.Err(err).Msg("Error decoding LoginTokens JSON")
}
return tokens.AccessToken, err
}
// GetGuestToken makes a request to superset's /guest_token endpoint and returns the guest token
func GetGuestToken(g GuestTokenRequest, csrfToken, accessToken string, cookies []*http.Cookie) (string, error) {
payloadBytes, err := json.Marshal(g)
if err != nil {
log.Err(err).Msg("Error marshalling superset guest token payload")
return "", err
}
client := &http.Client{}
//create the req and set the headers
req, err := http.NewRequest("POST", os.Getenv("SUPERSET_URL")+"/api/v1/security/guest_token/", bytes.NewBuffer(payloadBytes))
if err != nil {
log.Err(err).Msg("Error creating GetGuestToken request")
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-CSRFToken", csrfToken)
req.Header.Set("Authorization", "Bearer "+accessToken)
// Add cookies to the request
for _, cookie := range cookies {
req.AddCookie(cookie)
}
resp, err := client.Do(req)
if err != nil {
log.Err(err).Msg("Error making GetGuestToken POST request")
return "", err
}
defer resp.Body.Close()
var response map[string]string
if resp.StatusCode != http.StatusOK {
//error case print
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Err(err).Msg("Error reading response body")
return "", err
}
return "", fmt.Errorf("%s", string(body))
} else {
//happy path decode
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
log.Err(err).Msg("Error decoding GetGuestToken response")
return "", err
}
}
return response["token"], nil
}
// GetCSRFToken makes a request to superset's /csrf_token endpoint and returns csrf token and a session cookie
func GetCSRFToken(accessToken string) (string, []*http.Cookie, error) {
client := &http.Client{}
//create the req and set the headers
req, err := http.NewRequest("GET", os.Getenv("SUPERSET_URL")+"/api/v1/security/csrf_token", nil)
if err != nil {
log.Err(err).Msg("Error creating getCSRFToken request")
return "", nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := client.Do(req)
if err != nil {
log.Err(err).Msg("Error making GetCSRFToken request")
return "", nil, err
}
defer resp.Body.Close()
// Decode response JSON
var response map[string]string
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
log.Err(err).Msg("Error decoding GetCSRFToken response")
return "", nil, err
}
cookies := resp.Cookies()
return response["result"], cookies, nil
}
Templ changes:
There are a few pieces required for the ui to work properly:
- Load superset’s embedded sdk via CDN.
- We need a script that’s going to make the call to superset’s embdedded api with our token and parameters, requesting the right resource. One of the parameters is the ‘mountPoint’. The iframe containing the dashboard is going to be inserted in the DOM as the child of that mountPoint element.
- We need to be able to resize the dashboard (aka the iframe).
- A component invoking the said scripts and rendering the parent container for the dashboard.
Let’s start.
- Add superset’s embedded sdk to your code:
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
- Now we need an HTMX script component that makes the call to superset’s embedded api:
script EmbedScript(sed EmbeddedDashboard) {
supersetEmbeddedSdk.embedDashboard({
id: sed.ID, // given by the Superset's UI: create dashboard -> three dots menu-> embed dashboard
supersetDomain: sed.SupersetDomain,
mountPoint: document.getElementById(sed.DivID), // any html element that can contain an iframe
fetchGuestToken: () => sed.GuestToken,
dashboardUiConfig: { // dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional), urlParams (optional)
hideTitle: sed.HideTitle,
filters: {
expanded: sed.FiltersExpanded,
},
},
});
}
Another script component, this time to hack the size of the embedded superset iframe at run time:
script ModifyDashboardSize(containerID string, styles IframeStyles) {
document.getElementById(containerID).children[0].width=styles.Width;
document.getElementById(containerID).children[0].height=styles.Height;
document.getElementById(containerID).children[0].scrolling=styles.Scrollable;
}
And, finally, the component containing these two scripts together with the parent component for our superset dashboard. The call to superset’s embedded api is gonna insert the iframe as a child of this container:
templ Dashboard(sed EmbeddedDashboard) {
<div>
<div id={ sed.DivID } class="h-screen"></div>
@EmbedScript(sed)
@ModifyDashboardSize(sed.DivID, sed.Styles)
</div>
}
Usage
All that’s left is to serve the dashboard component as a prt of your page with all the relevant params!
Top comments (0)