Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

505 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Databricks Ops Console — Frontend

Single-page application that consumes the Databricks Ops Console backend API. Origin is Azure Storage static website ($web container) behind Azure Front Door (which owns TLS, the custom domain, and the security-headers rule set). Deployed via Azure DevOps.

The backend API this SPA talks to lives in a separate repo: Databricks Ops Console Backend. It's a Node/Express service deployed to Azure Container Apps.


Tech stack

Layer Technology
UI Vanilla HTML / CSS / JavaScript (no build step)
Charts Chart.js 4.4 (bundled locally)
Origin Azure Storage static website ($web container)
Edge Azure Front Door (TLS, custom domain, CSP + sec headers, edge cache)
Access Public via AFD dbxops.asami.ai; storage firewall accepts the AFD profile (Resource Access Rules) only — direct blob URLs return 403
CI/CD Azure DevOps → az storage blob upload-batchaz afd endpoint purge

No bundler, no transpiler, no node_modules in production. The files in src/ are uploaded as-is. This keeps deploys fast and debuggable — every line the browser runs is a line in this repo.


Folder layout

.
├── src/                         ← everything uploaded to $web
│   ├── index.html               SPA shell. Pre-paint dark-theme script + script tags load /config.js first.
│   ├── 404.html                 Dark-themed fallback for any unknown path.
│   ├── config.js                Runtime config. Sets window.__API_BASE__.
│   ├── chart.min.js             Chart.js 4.4 (bundled, no CDN dependency)
│   ├── assets/                  Static images + favicons
│   │   ├── favicon.ico / favicon.png
│   │   ├── databricks-emblem.png  Sidebar logo
│   │   └── aws.png / azure.png  Cloud provider logos (rendered in js/core/api.js)
│   ├── css/
│   │   ├── tokens.css           Design tokens (light + dark mode)
│   │   ├── layout.css           Sidebar, topbar, page frame
│   │   ├── components.css       Cards, buttons, tables, skeletons
│   │   └── auth.css             Login / signup page-specific styles
│   ├── login/                   Static sign-in / sign-up SPA (separate page)
│   │   └── index.html
│   ├── data/                    Pricing-calculator static dataset
│   │   ├── databricks-pricing.json   Generated; consumed by pages/pricing.js
│   │   ├── build-pricing.mjs         Rebuilds the JSON from upstream feeds
│   │   ├── verify-pricing.mjs        Cross-source audit (~5,500 checks)
│   │   ├── edge-cases.test.mjs       Playwright UI regression (116 checks)
│   │   └── README.md                 Pricing-data architecture + sourcing runbook
│   └── js/
│       ├── core/
│       │   ├── api.js           App state, data fetchers, account switcher
│       │   ├── auth.js          /api/auth/me bootstrap + 401 redirect
│       │   ├── sidebar.js       Navigation, theme toggle, bootstrap init
│       │   └── utils.js         Formatters, chart helpers, SVG icons
│       └── pages/               One file per page — keeps concerns isolated
│           ├── home.js          Account-level dashboard (workspaces / DBU / regions)
│           ├── workspaces.js    Per-workspace resources + cost
│           ├── cost.js          Cost Explorer (monthly trend + breakdown)
│           ├── finops.js        FinOps findings + recommendations
│           ├── anomalies.js     Alerts (acknowledge / resolve)
│           ├── budgets.js       Budget CRUD + consumption forecast
│           ├── pricing.js       Pricing Calculator (no backend; reads /data)
│           ├── secrets.js       Secrets Manager
│           ├── access.js        Access Control (users + roles)
│           └── settings.js      Account settings + integrations
│
├── azure-pipelines.yml          Single-stage deploy to $web (Front Door fronts the bucket)
├── .gitignore
└── README.md

Why one file per page?

Each page's render logic lives in its own module so adding a new page or editing one doesn't force a rebase on unrelated work. Pages share state through a small set of globals defined in js/core/api.js (allWorkspaces, costData, historyData, explorerData, activeAccountId).

Why src/ instead of uploading from the repo root?

Keeps non-shipping files (README, pipeline, .gitignore) out of $web. az storage blob upload-batch recurses the source folder verbatim, so src/index.html$web/index.html, src/js/core/api.js$web/js/core/api.js.


Runtime configuration

src/config.js is loaded before any other JavaScript. It sets window.__API_BASE__ which every fetch in js/core/api.js and the page modules prepends to its URL:

// src/config.js
window.__API_BASE__ = 'https://dbx-ops-dashboard.<hash>.eastus.azurecontainerapps.io';

To point at a different backend (local dev, staging, alt region), edit this one file and redeploy. No bundle rebuild, no env var injection.

Why config.js is a separate file

One static bundle has to work against many environments (prod / local / future AWS mirror). Keeping the backend URL in a dedicated, no-cache file means:

  • Ops can flip the backend URL without touching app code.
  • The pipeline sets Cache-Control: no-cache only on config.js, index.html and 404.html — every other asset stays cacheable.
  • The SPA JS files never reference a hard-coded hostname, so they're byte-identical across environments.

Why chart.min.js is bundled locally

Chart.js 4.4 renders every graph in the app (spend trend, SKU doughnut, anomaly sparklines, resource distribution, etc.). We commit a local copy instead of pulling it from a CDN because:

  • The app runs behind an office-IP allowlist; a blocked CDN would break the dashboard.
  • No third-party tracking or availability dependency.
  • Version is pinned forever — a CDN version can be pulled at any time.
  • Loaded with defer so it never blocks first paint.

Local development

The backend listens on port 3000 by default (the same port the SPA's static server uses, below), so start the backend on a different port for local dev:

  1. Run the backend with PORT=3001 npm run dev (or any free port).
  2. Edit src/config.js to http://localhost:3001.
  3. Serve this folder over a local static server:
    npx http-server ./src -p 3000 -c-1
  4. Browse to http://localhost:3000.

Don't commit a local-dev config.js — just git checkout src/config.js when you're done or use .git/info/exclude to ignore it locally.


Page → data dependency

Page ensureWorkspaces ensureCost ensureHistory ensureExplorer Own fetch
home.js
workspaces.js /api/workspace/:fqdn/resources
cost.js
finops.js /api/finops/recommendations + /api/findings
anomalies.js /api/alerts (list / acknowledge / resolve)
budgets.js /api/budgets CRUD with consumption forecast
pricing.js None — reads /data/databricks-pricing.json (static)
secrets.js /api/secrets/*
access.js /api/users / /api/admin/* (admin-only)
settings.js /api/accounts CRUD

The ensure* helpers are single-flight: concurrent callers share the same in-flight promise, and once a result is cached in memory it's reused for the rest of the page's lifetime.


Deployment

main branch pushes trigger azure-pipelines.yml:

  1. az storage blob upload-batch uploads ./src to $web.
  2. index.html, 404.html and config.js get Cache-Control: no-cache.
  3. az afd endpoint purge invalidates the Azure Front Door edge cache so the next request hits the freshly-uploaded blobs instead of a stale AFD-cached copy.
  4. Live behind Azure Front Door at https://dbxops.asami.ai. The $web storage account has public_network_access_enabled = false; only AFD's profile ID (via the private_link_access Resource Access Rules entry) and AAD-authenticated Azure Services (bypass = ["AzureServices"] — covers the pipeline's az storage blob upload-batch step) can reach it. Direct blob URLs return 403.

Required Azure setup

  • Storage account (the frontend static-site account) exists in AVA-EUS-DBX-OPS-RG. Static-website feature enabled, index document = index.html. Error document is also index.html so the SPA's hash router handles any unknown path (and not the static 404.html — which is shipped as a real asset but only served if a request explicitly asks for /404.html).
  • Storage firewall restricts inbound traffic to Azure Front Door's origin only — direct browser access to the blob URL is blocked.
  • Azure Front Door profile + endpoint + origin group point at the storage account's $web host. AFD owns the public TLS termination, custom domain, and the CSP / security-headers rule set.
  • The service principal behind the Azure DevOps service connection has Storage Blob Data Contributor on the storage account and CDN Profile Contributor on the AFD profile (for the purge step).

Rollback

Blob storage keeps no revision history of its own. To roll back a bad deploy, re-run the previous pipeline (Azure DevOps → Pipelines → the frontend pipeline → select a prior successful run → Rerun all jobs).


Security

  • All inbound requests terminate at Azure Front Door. AFD owns TLS, the custom domain, and a managed rule set that adds the Content- Security-Policy + other security headers as real HTTP response headers (the <meta> CSP fallback was removed — see commit sec(spa): remove redundant meta CSP).
  • The storage account behind AFD has public_network_access_enabled = false. AFD reaches it via a private_link_access rule (Resource Access Rules pattern, the Standard-tier equivalent of Private Link). The firewall accepts only the AFD profile's ARM ID + tenant ID; direct blob URLs return 403. bypass = ["AzureServices"] permits the deploy pipeline's AAD-authenticated az storage blob upload-batch step.
  • The backend API sits behind its own access-control rules at the Container App ingress (Entra-authenticated sessions, CSRF, CORS allowlist).
  • No secrets of any kind ship in the static bundle. The backend URL is public knowledge; access to it is gated by the backend's own controls.
  • config.js is configuration, not a secret — safe to version-control.

Adding a new page

  1. Create src/js/pages/<name>.js exporting a render<Name>() function onto window (same pattern as existing pages).
  2. Add a <script src="/js/pages/<name>.js"></script> include in src/index.html.
  3. Add a sidebar entry in src/index.html and a case in js/core/sidebar.js navigate() that calls render<Name>().
  4. If the page needs server data, prefer the existing ensureWorkspaces/ ensureCost/etc. helpers instead of adding a new global fetch path.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages