DEV Community

Cover image for Superset embedded dashboard setup: templ components, golang backend
Inna
Inna

Posted on

Superset embedded dashboard setup: templ components, golang backend

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Get the access token
  2. Get the csrf token & cookie (!)
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Templ changes:

There are a few pieces required for the ui to work properly:

  1. Load superset’s embedded sdk via CDN.
  2. 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.
  3. We need to be able to resize the dashboard (aka the iframe).
  4. A component invoking the said scripts and rendering the parent container for the dashboard.

Let’s start.

  1. Add superset’s embedded sdk to your code:
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
Enter fullscreen mode Exit fullscreen mode
  1. 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,
        },
        },
    });
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)