Sign-in by customer: realm petrolyst, row rules, guest role (PD-21)
Keycloak import for realm petrolyst: two clients, petrolyst with MFA for Superset and Traefik, petrolyst-app password only. It lists only app.petrolyst.com and traefik.petrolyst.com, and no customer names, as this repository is public. public.v_operator shows each customer's group. Dashboards overview and site, role Petrolyst Guest, filtered by row. Superset refuses a user with no mapped role. The realm travels with the release: each release's compose files name their realm and secret file, and a rollback needs only its target's secret. Not deployed. Checks earlier on this content: tests/run.sh 109 of 109 and read-only, dashboards import twice, column drift 0, compose config on old and new trees, the secret check on a scratch folder, a rehearsal on a throwaway Keycloak 26.4. Never a real deploy or rollback; shellcheck not installed today, clean when last run. Codex inspected once; its fixes are not re-inspected, on the owner's word. Bands: persistence, Superset. Rules: 1, 2, 3, 6, 14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ab553526f6
commit
78a498f1ce
20 files changed
+279
-78
No files matched your search
@@ -4,7 +4,7 @@ Superset dashboards as code and the operator scripts that import them. Not baked
|
||||
image, so changes here do not rebuild or restart Superset.
|
||||
|
||||
- `atoma/`: ATOMA's two dashboards for the Petrolyst app (PD-12, PD-13; SPEC.md §10):
|
||||
`atoma-overview` (fleet totals) and `atoma-site` (one site, its four sections as tabs). The
|
||||
`overview` (fleet totals) and `site` (one site, its four sections as tabs), named `atoma-overview` and `atoma-site` until 30 Oct. The
|
||||
app embeds them; admins open them in Superset, with the filter column. `SPEC.md` is the
|
||||
design; `build_bundle.py` generates the import bundle and `import_bundle.py` imports it
|
||||
(overwriting the objects the bundle defines).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# ATOMA dashboards: Overview and Site, for the Petrolyst app
|
||||
|
||||
**Status (29 Oct 2026, PD-13): two dashboards, `atoma-overview` and `atoma-site` (§10), built and
|
||||
**Status (30 Oct 2026, PD-21: every customer sees these two, limited by row; slugs `overview` and `site`, formerly
|
||||
`atoma-overview` and `atoma-site`, which the rest of this file still uses). Two dashboards (§10), built and
|
||||
tested on a local Superset; not imported live.** The Petrolyst app embeds them, and admins open
|
||||
them in Superset with the filter column.
|
||||
- `build_bundle.py` generates the dashboards, every dataset and every chart from this
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate ATOMA's dashboards for the Petrolyst app as one Superset 6.1 import bundle.
|
||||
"""Generate the dashboards of the Petrolyst app as one Superset 6.1 import bundle.
|
||||
|
||||
Implements SPEC.md in this folder: the two dashboards the Petrolyst app embeds, "ATOMA · Overview"
|
||||
(atoma-overview) and "ATOMA · Site" (atoma-site, the four detail tabs), one dataset per
|
||||
public.v_* view and every chart. Nothing in the bundle names a site.
|
||||
Implements SPEC.md in this folder: the two dashboards the app embeds, "Overview" (overview) and
|
||||
"Site" (site, the four detail tabs), one dataset per public.v_* view and every chart. Every
|
||||
customer sees the same dashboards, limited by row: the app's guest pass names the customer (PD-16).
|
||||
Nothing in the bundle names a site or a customer.
|
||||
|
||||
build_bundle.py --database-uuid <uuid of "Fuel"> [--out atoma-dashboards.zip]
|
||||
|
||||
@@ -15,7 +16,8 @@ dashboards (the stock dashboard import leaves existing datasets and charts
|
||||
unchanged).
|
||||
|
||||
Before writing, the bundle is checked: every column a chart or filter uses
|
||||
exists in its dataset, every chart in a Site filter's scope has site_label, the
|
||||
exists in its dataset, every dataset has operator_uid (the guest pass's row rule needs
|
||||
it), every chart in a Site filter's scope has site_label, the
|
||||
Overview sums recorded values only and money only per currency, and no row is
|
||||
wider than 12. Requires PyYAML.
|
||||
"""
|
||||
@@ -496,8 +498,11 @@ def timeseries_common(x: str, metrics: list[Any], groupby: list[str], grain: str
|
||||
|
||||
# --- Overview (PD-12, embedded in the Petrolyst app) ---------------------------------------
|
||||
|
||||
OVERVIEW_SLUG = "atoma-overview"
|
||||
SITE_SLUG = "atoma-site" # one embedded dashboard with the DETAIL_TABS as its tabs
|
||||
OVERVIEW_SLUG = "overview"
|
||||
SITE_SLUG = "site" # one embedded dashboard with the DETAIL_TABS as its tabs
|
||||
# A dashboard's identity, apart from its slug: the names it was first built under, so a re-import
|
||||
# updates the existing two (and keeps their embedding ids) rather than adding two new ones.
|
||||
DASHBOARD_KEYS = {OVERVIEW_SLUG: "atoma-overview", SITE_SLUG: "atoma-site"}
|
||||
|
||||
# SPEC §10: only sums and counts of recorded values, never an average of per-site rates; money
|
||||
# only grouped by currency. The value says whether the metric is money.
|
||||
@@ -935,7 +940,7 @@ def dashboard_yaml(key: str, title: str, slug: str, layout: Layout, metadata: di
|
||||
"certified_by": None,
|
||||
"certification_details": None,
|
||||
"published": True,
|
||||
"uuid": uid("dashboard", key),
|
||||
"uuid": uid("dashboard", DASHBOARD_KEYS[key]),
|
||||
"position": layout.nodes,
|
||||
"metadata": metadata,
|
||||
"version": VERSION,
|
||||
@@ -1000,7 +1005,7 @@ def build(database_uuid: str, columns_path: Path) -> dict[str, Any]:
|
||||
all_charts = {**{c.key: c for c in detail_charts()}, **overview}
|
||||
dashboards: list[tuple[Layout, dict[str, Chart], dict[str, Any]]] = []
|
||||
for key, title in [(OVERVIEW_SLUG, "Overview"), (SITE_SLUG, "Site")]:
|
||||
page = Layout(f"ATOMA · {title}", all_charts)
|
||||
page = Layout(title, all_charts)
|
||||
page.markdown.update({f"notes-{k}": v for k, v in TAB_NOTES.items()})
|
||||
page.markdown["notes-overview"] = OVERVIEW_NOTES
|
||||
if key == OVERVIEW_SLUG:
|
||||
@@ -1013,7 +1018,7 @@ def build(database_uuid: str, columns_path: Path) -> dict[str, Any]:
|
||||
placed = {k: all_charts[k] for k in page.ids}
|
||||
page_metadata = base_metadata(page, filters, cross_filters=False)
|
||||
page_metadata["label_colors"].update(STOCK_STATUS_COLOURS)
|
||||
files[f"dashboards/{key}.yaml"] = dashboard_yaml(key, f"ATOMA · {title}", key, page,
|
||||
files[f"dashboards/{key}.yaml"] = dashboard_yaml(key, title, key, page,
|
||||
page_metadata)
|
||||
dashboards.append((page, placed, page_metadata))
|
||||
|
||||
@@ -1031,6 +1036,12 @@ def check(files: dict[str, Any], views: dict[str, list[tuple[str, str]]],
|
||||
problems = []
|
||||
columns = {v: {c for c, _ in cols} | {c[0] for c in CALCULATED.get(v, [])}
|
||||
for v, cols in views.items()}
|
||||
for view, cols in columns.items():
|
||||
if "operator_uid" not in cols:
|
||||
problems.append(f"{view}: no operator_uid, so the guest pass's row rule cannot hold")
|
||||
for name, (_, _, sql) in VIRTUAL.items():
|
||||
if "operator_uid" not in sql:
|
||||
problems.append(f"{name}: its SQL does not select operator_uid")
|
||||
for view in list(MAIN_DTTM) + list(CALCULATED) + list(METRICS):
|
||||
if view not in columns:
|
||||
problems.append(f"{view}: configured but not in the column snapshot")
|
||||
|
||||
@@ -17,7 +17,7 @@ live view and any drift is reported (exit status 2).
|
||||
The database is never created or changed: the bundle's database uuid must
|
||||
already exist under the name "Fuel", otherwise nothing is imported.
|
||||
|
||||
The dashboards (atoma-overview, atoma-site) get no role: admins see them in Superset, and
|
||||
The dashboards (overview, site) get no role: admins see them in Superset, and
|
||||
the Petrolyst app shows them with guest passes (PD-12). With --embedded-out FILE
|
||||
--embed-domain URL, each of them is made embeddable from that origin (its embedding id stays
|
||||
the same across imports), and FILE receives {slug: embedding id} for the Petrolyst server:
|
||||
@@ -36,7 +36,7 @@ import zipfile
|
||||
import yaml
|
||||
|
||||
DATABASE_NAME = "Fuel"
|
||||
EMBEDDED_PREFIX = "atoma-"
|
||||
EMBEDDED_SLUGS = {"overview", "site"}
|
||||
STEPS = [("datasets", "SqlaTable"), ("charts", "Slice"), ("dashboards", "Dashboard")]
|
||||
|
||||
|
||||
@@ -104,12 +104,17 @@ def main() -> int:
|
||||
if path.startswith("dashboards/"):
|
||||
dashboard = db.session.query(Dashboard).filter_by(
|
||||
uuid=yaml.safe_load(text)["uuid"]).one()
|
||||
if dashboard.slug.startswith(EMBEDDED_PREFIX) and args.embedded_out:
|
||||
if dashboard.slug in EMBEDDED_SLUGS and args.embedded_out:
|
||||
from superset.daos.dashboard import EmbeddedDashboardDAO
|
||||
|
||||
config = EmbeddedDashboardDAO.upsert(dashboard, [args.embed_domain])
|
||||
db.session.flush()
|
||||
embedded[dashboard.slug] = str(config.uuid)
|
||||
# An empty or short list would stop the app's dashboards from opening.
|
||||
if args.embedded_out and set(embedded) != EMBEDDED_SLUGS:
|
||||
db.session.rollback()
|
||||
print(f"embedded: expected {sorted(EMBEDDED_SLUGS)}, got {sorted(embedded)}", file=sys.stderr)
|
||||
return 3
|
||||
db.session.commit()
|
||||
if args.embedded_out:
|
||||
with open(args.embedded_out, "w") as out:
|
||||
|
||||
Reference in new issue
Block a user