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)