Superset embedded dashboard setup: templ components, golang backend
Tags: webdev, javascript, go, templ
Published: 2024-05-15 18:43:35

## 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:

```jsx
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:

```jsx
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:

```yaml
 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:

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/](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:

```go
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:
```go
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:

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:

```jsx
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
```

1. Now we need an HTMX script component that makes the call to superset’s embedded api:

```react
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:

```jsx
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:

```html
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!