Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLEEP-1499 Support embedding of Superset dashboard in Iaso #1698

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,17 @@ services:
WFP_EMAIL_RECIPIENTS_NEW_ACCOUNT:
DISABLE_PASSWORD_LOGINS:
MAINTENANCE_MODE:
SERVER_URL: # Limit logging in dev to not overflow terminal
SERVER_URL:
SUPERSET_DB_NAME:
SUPERSET_DB_HOSTNAME:
SUPERSET_DB_PASSWORD:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that here?

SUPERSET_PORT:
SUPERSET_DB_USERNAME:
SUPERSET_SECRET_KEY:
SUPERSET_IASO_USER_TOKEN:
SUPERSET_URL:
SUPERSET_ADMIN_USERNAME:
SUPERSET_ADMIN_PASSWORD: # Limit logging in dev to not overflow terminal
logging: &iaso_logging
driver: "json-file"
options:
Expand Down
5 changes: 5 additions & 0 deletions hat/assets/js/apps/Iaso/domains/pages/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MESSAGES from './messages';
export const RAW = 'RAW';
export const TEXT = 'TEXT';
export const IFRAME = 'IFRAME';
export const SUPERSET = 'SUPERSET';

export const PAGES_TYPES = [
{
Expand All @@ -17,4 +18,8 @@ export const PAGES_TYPES = [
value: 'IFRAME',
label: MESSAGES.iframe,
},
{
value: 'SUPERSET',
label: MESSAGES.superset,
},
];
4 changes: 4 additions & 0 deletions hat/assets/js/apps/Iaso/domains/pages/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ const MESSAGES = defineMessages({
id: 'iaso.label.rawHtml',
defaultMessage: 'Raw html',
},
superset: {
id: 'iaso.label.superset',
defaultMessage: 'Superset',
},
needsAuthentication: {
id: 'iaso.label.needsAuthentication',
defaultMessage: 'Authentification required',
Expand Down
6 changes: 6 additions & 0 deletions hat/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@ def sentry_error_sampler(_, hint):
CELERY_RESULT_SERIALIZER = "json"
CELERY_RESULT_EXTENDED = True

# Superset dashboard/chart embedding configuration
SUPERSET_URL = os.environ.get("SUPERSET_URL", None)
SUPERSET_ADMIN_USERNAME = os.environ.get("SUPERSET_ADMIN_USERNAME", None)
SUPERSET_ADMIN_PASSWORD = os.environ.get("SUPERSET_ADMIN_PASSWORD", None)


# Plugin config
print("Enabled plugins:", PLUGINS)
for plugin_name in PLUGINS:
Expand Down
57 changes: 57 additions & 0 deletions hat/templates/iaso/pages/superset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!doctype html>
<html>
<head>
<title>{{title}}</title>
{{ analytics_script | safe }}
<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we should have this copied in our code base and not depending on an external source

</head>

<body>
<div id="my-superset-container"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not very important, but why "my" ?


<script>
async function fetchGuestTokenFromBackend() {
response = await fetch('/api/superset/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dashboard_id: '{{dashboard_id}}',
}),
});
const json_resp = await response.json();
return json_resp.token;
}

const containerRef = document.getElementById(
'my-superset-container',
);

supersetEmbeddedSdk.embedDashboard({
id: '{{dashboard_id}}',
supersetDomain: 'https://superset.trypelim.org',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain name seems hardcoded?

mountPoint: containerRef,
fetchGuestToken: () => fetchGuestTokenFromBackend(),
dashboardUiConfig: {
hideTitle: true,
hideTab: true,
hideChartControls: true,
filters: {
visible: false,
expanded: false,
},
},
});

const iframe = containerRef.querySelector('iframe');
if (iframe) {
iframe.style.width = '100%';
iframe.style.height = '97vh';
iframe.style.border = '0';
}
</script>

{% include "iaso/pages/refresh_data_set_snippet.html" %}
</body>
</html>
1 change: 1 addition & 0 deletions iaso/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ class AlgorithmRunAdmin(admin.ModelAdmin):
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
formfield_overrides = {models.JSONField: {"widget": IasoJSONEditorWidget}}
list_display = ("name", "slug", "type", "account")


@admin.register(EntityDuplicate)
Expand Down
59 changes: 59 additions & 0 deletions iaso/api/superset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import requests

from django.conf import settings
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status, viewsets
from rest_framework.response import Response


@swagger_auto_schema()
class SupersetTokenViewSet(viewsets.ViewSet):
"""
POST /api/superset/token

This endpoint creates a "guest token" to embed private Superset dashboards
in an iframe in Iaso (typically via a "Page")

See:
https://www.npmjs.com/package/@superset-ui/embedded-sdk
"""

def create(self, request):
dashboard_id = request.data.get("dashboard_id")

base_url = settings.SUPERSET_URL
headers = {"Content-Type": "application/json"}

# Log in to Superset to get access_token
payload = {
"username": settings.SUPERSET_ADMIN_USERNAME,
"password": settings.SUPERSET_ADMIN_PASSWORD,
"provider": "db",
"refresh": True,
}
response = requests.post(base_url + "/api/v1/security/login", json=payload, headers=headers)
access_token = response.json()["access_token"]
headers["Authorization"] = f"Bearer {access_token}"

# Fetch CSRF token
response = requests.get(base_url + "/api/v1/security/csrf_token/", headers=headers)
headers["X-CSRF-TOKEN"] = response.json()["result"]
headers["Cookie"] = response.headers.get("Set-Cookie")
headers["Referer"] = base_url

# Fetch Guest token
current_user = request.user
payload = {
"user": {
"username": current_user.username,
"first_name": current_user.first_name,
"last_name": current_user.last_name,
},
"resources": [{"type": "dashboard", "id": dashboard_id}],
"rls": [],
}

response = requests.post(base_url + "/api/v1/security/guest_token/", json=payload, headers=headers)
guest_token = response.json()["token"]

return Response({"token": guest_token}, status=status.HTTP_201_CREATED)
27 changes: 27 additions & 0 deletions iaso/migrations/0300_alter_page_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.14 on 2024-09-26 15:03

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("iaso", "0299_merge_0297_entity_merged_to_0298_profile_organization"),
]

operations = [
migrations.AlterField(
model_name="page",
name="type",
field=models.CharField(
choices=[
("RAW", "Raw html"),
("TEXT", "Text"),
("IFRAME", "Iframe"),
("POWERBI", "PowerBI report"),
("SUPERSET", "Superset dashboard"),
],
default="RAW",
max_length=40,
),
),
]
2 changes: 1 addition & 1 deletion iaso/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .org_unit import OrgUnit, OrgUnitType, OrgUnitChangeRequest
from .org_unit_change_request_configuration import OrgUnitChangeRequestConfiguration
from .project import Project
from .pages import Page, RAW, TEXT, IFRAME, POWERBI
from .pages import Page, RAW, TEXT, IFRAME, POWERBI, SUPERSET
from .comment import CommentIaso
from .import_gpkg import ImportGPKG
from .entity import EntityType, Entity
Expand Down
2 changes: 2 additions & 0 deletions iaso/models/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
TEXT = "TEXT"
IFRAME = "IFRAME"
POWERBI = "POWERBI"
SUPERSET = "SUPERSET"

PAGES_TYPES = [
(RAW, _("Raw html")),
(TEXT, _("Text")),
(IFRAME, _("Iframe")),
(POWERBI, _("PowerBI report")),
(SUPERSET, _("Superset dashboard")),
]


Expand Down
4 changes: 2 additions & 2 deletions iaso/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
from .api.setup_account import SetupAccountViewSet
from .api.source_versions import SourceVersionViewSet
from .api.storage import StorageBlacklistedViewSet, StorageLogViewSet, StorageViewSet, logs_per_device
from .api.superset import SupersetTokenViewSet
from .api.tasks import TaskSourceViewSet
from .api.tasks.create.export_mobile_setup import ExportMobileSetupViewSet
from .api.tasks.create.import_gpkg import ImportGPKGViewSet
Expand Down Expand Up @@ -203,9 +204,8 @@
router.register(r"mobile/metadata/lastupdates", LastUpdatesViewSet, basename="lastupdates")
router.register(r"modules", ModulesViewSet, basename="modules")
router.register(r"configs", ConfigViewSet, basename="jsonconfigs")


router.register(r"mobile/bulkupload", MobileBulkUploadsViewSet, basename="mobilebulkupload")
router.register(r"superset/token", SupersetTokenViewSet, basename="supersettoken")
router.registry.extend(plugins_router.registry)

urlpatterns: URLList = [
Expand Down
10 changes: 9 additions & 1 deletion iaso/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.shortcuts import get_object_or_404, render, resolve_url
from bs4 import BeautifulSoup as Soup # type: ignore
from hat.__version__ import DEPLOYED_BY, DEPLOYED_ON, VERSION
from iaso.models import IFRAME, POWERBI, TEXT, Account, Page
from iaso.models import IFRAME, POWERBI, SUPERSET, TEXT, Account, Page
from iaso.utils.powerbi import get_powerbi_report_token


Expand Down Expand Up @@ -75,6 +75,14 @@ def page(request, page_slug):
"iaso/pages/powerbi.html",
content,
)
elif page.type == SUPERSET:
# TODO: Use dedicated column for dashboard_id?
content.update({"dashboard_id": page.content, "title": page.name, "page": page})
response = render(
request,
"iaso/pages/superset.html",
content,
)
else:
raw_html = page.content
if analytics_script and raw_html is not None:
Expand Down