Theme tokens for the slate surfaces and borders, the logo with the green Y, chart boxes at 16 px with the card shadow. Full figures with the unit in the subheader; every layer of the tabs transparent, so the site's charts sit as boxes in Firefox and Safari too. The Overview gets a Sites filter and the period's scope returns to the time charts (PD-25). The embedded frame can shrink again. A change to the image's inputs: Superset rebuilds on the next deploy. Bands: Superset. Rules: 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
282 lines
10 KiB
Python
282 lines
10 KiB
Python
"""Superset configuration for Petrolyst (ATOMA fuel monitoring).
|
|
|
|
Customization only, and no secrets: those are read from /run/secrets/<name>,
|
|
which stacks/fuel-monitoring/compose.yaml mounts from /opt/petrolyst/secrets/.
|
|
Baked into the image at /app/pythonpath/, which is on PYTHONPATH in
|
|
apache/superset:6.1.0, so Superset loads it without SUPERSET_CONFIG_PATH.
|
|
"""
|
|
|
|
import os
|
|
import pathlib
|
|
from copy import deepcopy
|
|
|
|
from flask_appbuilder.security.manager import AUTH_OAUTH
|
|
from sqlalchemy.engine.url import URL
|
|
|
|
from petrolyst_security import PetrolystSecurityManager
|
|
from superset import config as superset_defaults
|
|
|
|
|
|
def _read_secret(name: str, env_var: str | None = None) -> str:
|
|
"""Prefer /run/secrets/<name>; fall back to an env var for local dev.
|
|
|
|
Uses rstrip('\r\n') (not strip()) so passwords or keys that legitimately
|
|
contain leading/trailing spaces are not silently mutated. Raises on empty
|
|
values so boot fails loudly instead of accepting a blank secret.
|
|
"""
|
|
path = pathlib.Path(f"/run/secrets/{name}")
|
|
if path.is_file():
|
|
value = path.read_text().rstrip("\r\n")
|
|
else:
|
|
value = os.environ[env_var or name.upper()]
|
|
if not value:
|
|
raise RuntimeError(f"secret {name!r} must not be empty")
|
|
return value
|
|
|
|
|
|
SECRET_KEY = _read_secret("superset_secret_key", "SUPERSET_SECRET_KEY")
|
|
|
|
SQLALCHEMY_DATABASE_URI = URL.create(
|
|
"postgresql+psycopg2",
|
|
username="superset",
|
|
password=_read_secret("superset_db_password", "SUPERSET_DB_PASSWORD"),
|
|
host=os.environ.get("SUPERSET_DB_HOST", "superset-db"),
|
|
port=5432,
|
|
database="superset",
|
|
).render_as_string(hide_password=False)
|
|
|
|
# --- The data: fuel-db ----------------------------------------------------------
|
|
# Superset reads fuel-db directly as the read-only role fuel_ro;
|
|
# superset-init registers that connection, "Fuel", without a password. Passwords of data
|
|
# connections come from secret files at connect time, so none is kept in Superset's own
|
|
# database. A connection added in the UI gets no password this way.
|
|
_DATABASE_PASSWORD_SECRETS = {"fuel_ro": "fuel_db_ro_password"}
|
|
|
|
|
|
def _database_password(url: URL) -> str | None:
|
|
secret = _DATABASE_PASSWORD_SECRETS.get(url.username or "")
|
|
return _read_secret(secret) if secret else None
|
|
|
|
|
|
SQLALCHEMY_CUSTOM_PASSWORD_STORE = _database_password
|
|
|
|
# --- Sign-in through Keycloak (client "petrolyst") ----------------------------
|
|
# Users and roles are managed in Keycloak; there is no local admin account.
|
|
# Break-glass access: `superset fab ...` inside the container on the server.
|
|
KEYCLOAK_REALM_URL = os.environ.get(
|
|
"KEYCLOAK_REALM_URL", "https://iam.thedigitalbridge.io/realms/petrolyst"
|
|
).rstrip("/")
|
|
|
|
AUTH_TYPE = AUTH_OAUTH
|
|
CUSTOM_SECURITY_MANAGER = PetrolystSecurityManager
|
|
OAUTH_PROVIDERS = [
|
|
{
|
|
"name": "keycloak",
|
|
"icon": "fa-key",
|
|
"token_key": "access_token",
|
|
"remote_app": {
|
|
"client_id": os.environ.get("KEYCLOAK_CLIENT_ID", "petrolyst"),
|
|
"client_secret": _read_secret(
|
|
"keycloak_petrolyst_client_secret", "KEYCLOAK_CLIENT_SECRET"
|
|
),
|
|
"server_metadata_url": f"{KEYCLOAK_REALM_URL}/.well-known/openid-configuration",
|
|
"api_base_url": f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/",
|
|
"client_kwargs": {
|
|
"scope": "openid email profile",
|
|
"code_challenge_method": "S256",
|
|
},
|
|
},
|
|
}
|
|
]
|
|
|
|
# Keys are Keycloak client roles of "petrolyst" (claim petrolyst_roles).
|
|
AUTH_ROLES_MAPPING = {
|
|
"superset-admin": ["Admin"],
|
|
"superset-editor": ["Alpha"],
|
|
"superset-viewer": ["Gamma"],
|
|
# Customers' users have a role of client "petrolyst-app", not of this one: refused here.
|
|
}
|
|
AUTH_ROLES_SYNC_AT_LOGIN = True
|
|
AUTH_USER_REGISTRATION = True
|
|
# Public has no permissions. A Keycloak user without a mapped role never gets
|
|
# this far: PetrolystSecurityManager refuses the sign-in and makes no account.
|
|
AUTH_USER_REGISTRATION_ROLE = "Public"
|
|
|
|
# Viewers (Gamma) see a dashboard once it lists their role.
|
|
# FILTERBAR_CLOSED_BY_DEFAULT: a dashboard's filter column opens collapsed, a
|
|
# narrow strip with an expand button (owner, 29 Oct); ?expand_filters=true
|
|
# opens it from a link.
|
|
FEATURE_FLAGS = {"DASHBOARD_RBAC": True, "FILTERBAR_CLOSED_BY_DEFAULT": True}
|
|
|
|
# deck.gl maps: OpenStreetMap raster tiles only. The default list adds Mapbox
|
|
# styles, which fail without MAPBOX_API_KEY. The default CSP already allows the
|
|
# tile host.
|
|
DECKGL_BASE_MAP = [
|
|
["tile://https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap"],
|
|
]
|
|
|
|
# --- Behind Traefik -----------------------------------------------------------
|
|
ENABLE_PROXY_FIX = True
|
|
PREFERRED_URL_SCHEME = "https"
|
|
SESSION_COOKIE_SECURE = True
|
|
SESSION_COOKIE_HTTPONLY = True
|
|
SESSION_COOKIE_SAMESITE = "Lax"
|
|
TALISMAN_CONFIG = deepcopy(superset_defaults.TALISMAN_CONFIG)
|
|
TALISMAN_CONFIG["session_cookie_secure"] = True
|
|
|
|
# --- Redis: caches and rate limits (no Celery, so no results backend) ---------
|
|
REDIS_URL = os.environ.get("REDIS_URL", "redis://redis:6379")
|
|
|
|
CACHE_CONFIG = {
|
|
"CACHE_TYPE": "RedisCache",
|
|
"CACHE_DEFAULT_TIMEOUT": 300,
|
|
"CACHE_KEY_PREFIX": "superset_cache_",
|
|
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
|
}
|
|
DATA_CACHE_CONFIG = {
|
|
"CACHE_TYPE": "RedisCache",
|
|
"CACHE_DEFAULT_TIMEOUT": 3600,
|
|
"CACHE_KEY_PREFIX": "superset_data_",
|
|
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
|
}
|
|
FILTER_STATE_CACHE_CONFIG = {
|
|
"CACHE_TYPE": "RedisCache",
|
|
"CACHE_DEFAULT_TIMEOUT": 86400,
|
|
"CACHE_KEY_PREFIX": "superset_filter_",
|
|
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
|
}
|
|
EXPLORE_FORM_DATA_CACHE_CONFIG = {
|
|
"CACHE_TYPE": "RedisCache",
|
|
"CACHE_DEFAULT_TIMEOUT": 86400,
|
|
"CACHE_KEY_PREFIX": "superset_explore_",
|
|
"CACHE_REDIS_URL": f"{REDIS_URL}/0",
|
|
}
|
|
RATELIMIT_STORAGE_URI = f"{REDIS_URL}/1"
|
|
|
|
# --- Petrolyst look and feel --------------------------------------------------
|
|
# Values from petrolyst-web/design/brand/07_UI_Tokens/petrolyst_tokens.css.
|
|
# Assets live in assets/branding/ and are served from /static/assets/branding/.
|
|
BRANDING = "/static/assets/branding"
|
|
|
|
APP_NAME = "Petrolyst"
|
|
APP_ICON = f"{BRANDING}/petrolyst_wordmark_fullcolor.svg"
|
|
FAVICONS = [
|
|
{"href": f"{BRANDING}/favicon.svg", "type": "image/svg+xml"},
|
|
{"href": f"{BRANDING}/favicon.ico", "type": "image/x-icon"},
|
|
{"href": f"{BRANDING}/petrolyst_icon_180.png", "rel": "apple-touch-icon"},
|
|
]
|
|
|
|
THEME_DEFAULT = deepcopy(superset_defaults.THEME_DEFAULT)
|
|
THEME_DEFAULT["token"].update(
|
|
{
|
|
"brandAppName": APP_NAME,
|
|
"brandLogoAlt": "Petrolyst",
|
|
"brandLogoUrl": APP_ICON,
|
|
"brandLogoHref": "/",
|
|
# The wordmark alone (owner, 29 Oct): in the stacked logo, at 40px, its
|
|
# letters were about 5px high. Its own Y carries the brand accent. 24px
|
|
# (letters about 20px) and the margin fill the 60px navbar height. On the
|
|
# left, 15px more than the navbar's 16px: the P (1.4px inside the image)
|
|
# lines up with the dashboard's content at 32px.
|
|
"brandLogoHeight": "24px",
|
|
"brandLogoMargin": "18px 0 18px 15px",
|
|
# Action teal, not the raw brand teal #009688: the brand guide keeps
|
|
# raw teal off small text and controls.
|
|
"colorPrimary": "#00695C",
|
|
"colorLink": "#00695C",
|
|
"colorSuccess": "#0F7B6C",
|
|
"colorWarning": "#B45309",
|
|
"colorError": "#B3261E",
|
|
"colorInfo": "#2563EB",
|
|
"colorTextBase": "#0B1D33",
|
|
"colorBgLayout": "#F7F9FA",
|
|
"colorBorder": "#DDE5E9",
|
|
"borderRadius": 8,
|
|
"fontFamily": "Inter, system-ui, sans-serif",
|
|
}
|
|
)
|
|
|
|
THEME_DARK = deepcopy(THEME_DEFAULT)
|
|
THEME_DARK["algorithm"] = "dark"
|
|
THEME_DARK["token"].update(
|
|
{
|
|
"brandLogoUrl": f"{BRANDING}/petrolyst_wordmark_white_y.svg",
|
|
"colorPrimary": "#35C5B6",
|
|
"colorLink": "#35C5B6",
|
|
"colorSuccess": "#35C5B6",
|
|
"colorWarning": "#F59E0B",
|
|
"colorError": "#FF6B6B",
|
|
"colorInfo": "#60A5FA",
|
|
"colorTextBase": "#F8FAFC",
|
|
"colorBgBase": "#1F2A33",
|
|
"colorBgLayout": "#1F2A33",
|
|
"colorBgContainer": "#2A3945",
|
|
"colorBorder": "#76828E",
|
|
"colorEditorSelection": superset_defaults.THEME_DARK["token"][
|
|
"colorEditorSelection"
|
|
],
|
|
}
|
|
)
|
|
|
|
# Chart colours. Validated with the dataviz skill's validator on both chart
|
|
# surfaces (#FFFFFF light, #2A3945 dark, PD-26): all slots in both lightness bands and
|
|
# >= 3:1; worst adjacent CVD dE 13.7, normal-vision dE 26.2; the first three
|
|
# slots also pass all-pairs (scatter, maps). Brand teal leads; brand cyan
|
|
# #00B6E6 is snapped to #0097C4 because it is too light for the dark surface
|
|
# and below 3:1 on white. The other hues come from the validated reference set.
|
|
# Status meaning (warning, critical) never rides on these colours.
|
|
EXTRA_CATEGORICAL_COLOR_SCHEMES = [
|
|
{
|
|
"id": "petrolyst",
|
|
"description": "Petrolyst categorical palette, brand teal first",
|
|
"label": "Petrolyst",
|
|
"isDefault": True,
|
|
"colors": [
|
|
"#009688", # brand teal
|
|
"#d95926", # orange
|
|
"#9085e9", # violet
|
|
"#c98500", # amber
|
|
"#0097C4", # brand cyan, snapped
|
|
"#e34948", # red
|
|
"#3987e5", # blue
|
|
"#d55181", # magenta
|
|
],
|
|
}
|
|
]
|
|
|
|
# One hue, light to dark; the lightest step means "near zero" and may recede.
|
|
EXTRA_SEQUENTIAL_COLOR_SCHEMES = [
|
|
{
|
|
"id": "petrolyst_teal",
|
|
"description": "Petrolyst teal, light to dark",
|
|
"label": "Petrolyst Teal",
|
|
"isDiverging": False,
|
|
"isDefault": True,
|
|
"colors": [
|
|
"#E6F4F2",
|
|
"#B3DED8",
|
|
"#7FC8BE",
|
|
"#4CB2A4",
|
|
"#009688",
|
|
"#00695C",
|
|
"#004D44",
|
|
],
|
|
},
|
|
{
|
|
"id": "petrolyst_navy",
|
|
"description": "Petrolyst navy, light to dark",
|
|
"label": "Petrolyst Navy",
|
|
"isDiverging": False,
|
|
"isDefault": False,
|
|
"colors": [
|
|
"#E8EEF5",
|
|
"#C3D1E2",
|
|
"#8FA7C4",
|
|
"#5C7DA6",
|
|
"#2F5585",
|
|
"#183A63",
|
|
"#0B1D33",
|
|
],
|
|
},
|
|
]
|