From 9cfe6946d1faa9c813bef2466c0b8a72a355345e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 8 Aug 2025 18:48:41 +0000 Subject: [PATCH 1/2] Create DBT Admin UI with project management and job tracking Co-authored-by: akshay.agrawal1101 --- dbt-admin/README.md | 26 +++ dbt-admin/app/__init__.py | 0 .../app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 133 bytes dbt-admin/app/__pycache__/db.cpython-313.pyc | Bin 0 -> 1640 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 8712 bytes .../app/__pycache__/models.cpython-313.pyc | Bin 0 -> 2458 bytes .../app/__pycache__/services.cpython-313.pyc | Bin 0 -> 10938 bytes dbt-admin/app/db.py | 31 +++ dbt-admin/app/main.py | 143 +++++++++++++ dbt-admin/app/models.py | 42 ++++ dbt-admin/app/services.py | 201 ++++++++++++++++++ dbt-admin/app/templates/base.html | 20 ++ dbt-admin/app/templates/home.html | 19 ++ dbt-admin/app/templates/new_project.html | 29 +++ dbt-admin/app/templates/project_detail.html | 121 +++++++++++ dbt-admin/data/app.sqlite3 | Bin 0 -> 12288 bytes dbt-admin/requirements.txt | 10 + 17 files changed, 642 insertions(+) create mode 100644 dbt-admin/README.md create mode 100644 dbt-admin/app/__init__.py create mode 100644 dbt-admin/app/__pycache__/__init__.cpython-313.pyc create mode 100644 dbt-admin/app/__pycache__/db.cpython-313.pyc create mode 100644 dbt-admin/app/__pycache__/main.cpython-313.pyc create mode 100644 dbt-admin/app/__pycache__/models.cpython-313.pyc create mode 100644 dbt-admin/app/__pycache__/services.cpython-313.pyc create mode 100644 dbt-admin/app/db.py create mode 100644 dbt-admin/app/main.py create mode 100644 dbt-admin/app/models.py create mode 100644 dbt-admin/app/services.py create mode 100644 dbt-admin/app/templates/base.html create mode 100644 dbt-admin/app/templates/home.html create mode 100644 dbt-admin/app/templates/new_project.html create mode 100644 dbt-admin/app/templates/project_detail.html create mode 100644 dbt-admin/data/app.sqlite3 create mode 100644 dbt-admin/requirements.txt diff --git a/dbt-admin/README.md b/dbt-admin/README.md new file mode 100644 index 0000000..d59aec1 --- /dev/null +++ b/dbt-admin/README.md @@ -0,0 +1,26 @@ +# DBT Admin UI + +A lightweight FastAPI app to manage multiple dbt Core projects in a mesh environment. + +## Features +- Per-project dbt-core version via isolated venvs +- Project registry with root path, profiles dir, and extra adapter packages +- Install/Update dbt for a project +- Evaluate project (dbt parse) and read `manifest.json` for stats +- Simple job history with logs + +## Quickstart + +```bash +cd /workspace/dbt-admin +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +Open http://localhost:8000 and create your first project. + +Notes: +- Use absolute paths for `root_path` and `profiles_dir`. +- Add adapter packages (e.g., `dbt-postgres==1.8.1`) in Extra Packages. \ No newline at end of file diff --git a/dbt-admin/app/__init__.py b/dbt-admin/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dbt-admin/app/__pycache__/__init__.cpython-313.pyc b/dbt-admin/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f26490c63a762f60db917d0cc8df3a410072fe1 GIT binary patch literal 133 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 Bh#CL@ literal 0 HcmV?d00001 diff --git a/dbt-admin/app/__pycache__/models.cpython-313.pyc b/dbt-admin/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9ec57086cec7ef15c3f249844c7de1212c04d9e GIT binary patch literal 2458 zcma)8&rcgy5Z+y{ch|pxU_wIEP(nzYHqf*sA!=#mfJrdKO|ztzI$ACE+H7L)Iv){~P zI~EHIIG$#{UHVfNgx_e=8EPAd+utL2B0=hUnQS~disL7`t55_ILbpsV?CULMEOXh4MPHPnap>7!g9Mtvk7(xZCpvJ5e< zi=!^Fr;C@nQr%m$RZ3(L({Wti^ljJiaHrw^(yH;t5$2&Y0aw(N*hT)i1%P?5jFsg2`ULhPcjC*ynvb&NnjFJtWU$Gs_ zadBCl&FKs2`FW;gK2OizN-tzs(lBs;-!2%Y?}NQq_btybAdY8m)1}$a*aH{tdNs3P zjTIOD^JcMXJ7Z?8HioBK6>qe*40Ka22zXHxUkk1H(Z~dSBo{RNxS|e?hrF@Y6zGMrKQTK$kB|NQz)y(Umi3hfG&=Z4ZU^P?#eL zlN3RVD9v{Fi0N_ML*x2HN1qSf>_w9UL+`pQLPYPz9eQ5FGRiHF@RNHXR+dsprrrSe zj#cotC?C0n#}ZSER>`bad={yJT{U6ZxN9x{6YJE>7q;WyW|CcG0nY~}6KPopT-P^h zroY4@$Ts7?1s+z|D_&@On?-Bjmhkg>M$v|qi&Jj3YM%G3nh97S#lf0g8$`n1HOrRw z&Y(3~9vvj=)?CjogXK+5UKpJiy^xZCBEW+zuK+eLWfr%-^ufd%w7nmTEU_DgQ8Ej@ z3(G*=hJ_1Y(OFnFEL^Wqb0EyAl8PV3Hc-(I&XF?~<}1ww%k5oSdXEI(T&ep7$9(|f zq`64qFd9TD~~vzlvl_crC;*3d{} ze*Jt?9@!jDJsEyB{nf_s!$xlXdQ*P5IWYL7|LMt}PHqgO8`*WODW?yrp3CVrtLfYY zY=J@FT&!4!^P8c~>++j(R@F7+p^DvG(c8S<$LYLwlgjqMz-}%=SdXL6OO{7&_8&0A zDQ9`?CCjn(#`PoU*UOnKL4wbcyR$C657&`(=;01}^d)(yMJJ15ibxQm95eLp2~TJkQ#6l%_O z58kJZi5r{~D?NKf)OnR|voM;%E+Zo)+p)c+HYLZk~F#U_dM(I)JX z|0gV5qLV)%!F%6HLTM8GN)v=QSFaG7A~8)OL*g*%en<->D0$uribJTo**EZGbfa$^ zsTgR=4DTqCczms}{=trbU?(h#ed~esg&hIGPFxeOh-*vV75^3p?evAjvukI+ L8{QTW@ZkOh*5XbM literal 0 HcmV?d00001 diff --git a/dbt-admin/app/__pycache__/services.cpython-313.pyc b/dbt-admin/app/__pycache__/services.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..308bdef55ca318b39face48013aa4fc636e5ec51 GIT binary patch literal 10938 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 literal 0 HcmV?d00001 diff --git a/dbt-admin/app/db.py b/dbt-admin/app/db.py new file mode 100644 index 0000000..5983d02 --- /dev/null +++ b/dbt-admin/app/db.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator + +from sqlmodel import SQLModel, create_engine, Session + +DATA_DIR = Path(__file__).resolve().parent.parent / "data" +DATA_DIR.mkdir(parents=True, exist_ok=True) +DB_PATH = DATA_DIR / "app.sqlite3" + +engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, connect_args={"check_same_thread": False}) + + +def init_db() -> None: + from .models import Project, Job # noqa: F401 # ensure models are imported for table creation + SQLModel.metadata.create_all(engine) + + +@contextmanager +def get_session() -> Iterator[Session]: + session = Session(engine) + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() \ No newline at end of file diff --git a/dbt-admin/app/main.py b/dbt-admin/app/main.py new file mode 100644 index 0000000..0dc1b16 --- /dev/null +++ b/dbt-admin/app/main.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import threading +from datetime import datetime +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, Form, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from .db import init_db, get_session +from .models import Job, JobStatus, JobType, Project +from .services import compute_stats_from_manifest, create_job, run_job + +BASE_DIR = Path(__file__).resolve().parent +TEMPLATES_DIR = BASE_DIR / "templates" +STATIC_DIR = BASE_DIR / "static" + +app = FastAPI(title="DBT Admin UI") +app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) + + +@app.on_event("startup") +def on_startup() -> None: + init_db() + + +@app.get("/", response_class=HTMLResponse) +def home(request: Request): # type: ignore[no-untyped-def] + with get_session() as session: + projects = session.query(Project).order_by(Project.created_at.desc()).all() + return templates.TemplateResponse("home.html", {"request": request, "projects": projects}) + + +@app.get("/projects/new", response_class=HTMLResponse) +def new_project(request: Request): # type: ignore[no-untyped-def] + return templates.TemplateResponse("new_project.html", {"request": request}) + + +@app.post("/projects") +def create_project( # type: ignore[no-untyped-def] + name: str = Form(...), + root_path: str = Form(...), + dbt_version: str = Form(...), + profiles_dir: Optional[str] = Form(None), + extra_packages: Optional[str] = Form(None), +): + project = Project( + name=name, + root_path=root_path, + dbt_version=dbt_version, + profiles_dir=profiles_dir, + extra_packages=extra_packages, + updated_at=datetime.utcnow(), + ) + with get_session() as session: + session.add(project) + session.flush() + session.refresh(project) + return RedirectResponse(url=f"/projects/{project.id}", status_code=303) + + +@app.get("/projects/{project_id}", response_class=HTMLResponse) +def show_project(request: Request, project_id: int): # type: ignore[no-untyped-def] + with get_session() as session: + project = session.get(Project, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + jobs = session.query(Job).filter(Job.project_id == project_id).order_by(Job.created_at.desc()).limit(20).all() + stats = compute_stats_from_manifest(project) + return templates.TemplateResponse("project_detail.html", {"request": request, "project": project, "jobs": jobs, "stats": stats}) + + +@app.post("/projects/{project_id}/update") +def update_project( # type: ignore[no-untyped-def] + project_id: int, + name: str = Form(...), + root_path: str = Form(...), + dbt_version: str = Form(...), + profiles_dir: Optional[str] = Form(None), + extra_packages: Optional[str] = Form(None), +): + with get_session() as session: + project = session.get(Project, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + project.name = name + project.root_path = root_path + project.dbt_version = dbt_version + project.profiles_dir = profiles_dir + project.extra_packages = extra_packages + project.updated_at = datetime.utcnow() + session.add(project) + return RedirectResponse(url=f"/projects/{project_id}", status_code=303) + + +@app.post("/projects/{project_id}/delete") +def delete_project(project_id: int): # type: ignore[no-untyped-def] + with get_session() as session: + project = session.get(Project, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + session.delete(project) + return RedirectResponse(url="/", status_code=303) + + +@app.post("/projects/{project_id}/install") +def install_project_env(project_id: int): # type: ignore[no-untyped-def] + with get_session() as session: + project = session.get(Project, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + job = create_job(project, JobType.INSTALL) + thread = threading.Thread(target=run_job, args=(job, project), daemon=True) + thread.start() + return RedirectResponse(url=f"/projects/{project_id}", status_code=303) + + +@app.post("/projects/{project_id}/evaluate") +def evaluate_project(project_id: int): # type: ignore[no-untyped-def] + with get_session() as session: + project = session.get(Project, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + job = create_job(project, JobType.EVALUATE) + thread = threading.Thread(target=run_job, args=(job, project), daemon=True) + thread.start() + return RedirectResponse(url=f"/projects/{project_id}", status_code=303) + + +@app.get("/jobs/{job_id}/logs") +def get_job_logs(job_id: int): # type: ignore[no-untyped-def] + with get_session() as session: + job = session.get(Job, job_id) + if not job or not job.log_path: + raise HTTPException(status_code=404, detail="Logs not found") + log_path = Path(job.log_path) + if not log_path.exists(): + raise HTTPException(status_code=404, detail="Logs not found") + return FileResponse(path=str(log_path), media_type="text/plain") \ No newline at end of file diff --git a/dbt-admin/app/models.py b/dbt-admin/app/models.py new file mode 100644 index 0000000..2e38a45 --- /dev/null +++ b/dbt-admin/app/models.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class JobType(str, Enum): + INSTALL = "install" + EVALUATE = "evaluate" + + +class JobStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + SUCCESS = "success" + FAILED = "failed" + + +class Project(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + root_path: str + dbt_version: str + profiles_dir: Optional[str] = None + extra_packages: Optional[str] = Field(default=None, description="Comma-separated pip packages, e.g. dbt-postgres==1.8.1") + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + +class Job(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + project_id: int = Field(foreign_key="project.id") + job_type: JobType + status: JobStatus = Field(default=JobStatus.PENDING) + created_at: datetime = Field(default_factory=datetime.utcnow) + started_at: Optional[datetime] = None + finished_at: Optional[datetime] = None + log_path: Optional[str] = None + error_message: Optional[str] = None \ No newline at end of file diff --git a/dbt-admin/app/services.py b/dbt-admin/app/services.py new file mode 100644 index 0000000..11ac822 --- /dev/null +++ b/dbt-admin/app/services.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +import json +import os +import shlex +import subprocess +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from .models import Job, JobStatus, JobType, Project +from .db import DATA_DIR, get_session + +ENVS_DIR = DATA_DIR / "envs" +ENVS_DIR.mkdir(parents=True, exist_ok=True) +JOBS_LOGS_DIR = DATA_DIR / "job_logs" +JOBS_LOGS_DIR.mkdir(parents=True, exist_ok=True) + + +@dataclass +class CommandResult: + returncode: int + stdout: str + stderr: str + + +def get_project_env_dir(project_id: int) -> Path: + return ENVS_DIR / f"project_{project_id}" + + +def get_env_python_bin(env_dir: Path) -> Path: + # Linux path + python_bin = env_dir / "bin" / "python" + return python_bin + + +def get_env_pip_bin(env_dir: Path) -> Path: + pip_bin = env_dir / "bin" / "pip" + return pip_bin + + +def _create_env_with_virtualenv(env_dir: Path) -> bool: + try: + proc = subprocess.run(["python3", "-m", "virtualenv", str(env_dir)], capture_output=True, text=True) + return proc.returncode == 0 + except Exception: + return False + + +def _create_env_with_venv(env_dir: Path) -> bool: + try: + proc = subprocess.run(["python3", "-m", "venv", str(env_dir)], capture_output=True, text=True) + return proc.returncode == 0 + except Exception: + return False + + +def ensure_env(project: Project) -> Tuple[Path, Path, Path]: + env_dir = get_project_env_dir(project.id) # type: ignore[arg-type] + env_dir.mkdir(parents=True, exist_ok=True) + + python_bin = get_env_python_bin(env_dir) + pip_bin = get_env_pip_bin(env_dir) + + if not python_bin.exists(): + created = _create_env_with_virtualenv(env_dir) + if not created: + created = _create_env_with_venv(env_dir) + if not created: + raise RuntimeError("Failed to create a virtual environment. Ensure 'virtualenv' or 'venv' is available.") + + # Ensure pip is up to date (best-effort) + subprocess.run([str(pip_bin), "install", "--upgrade", "pip", "setuptools", "wheel"], check=False) + + return env_dir, python_bin, pip_bin + + +def install_dbt_for_project(project: Project) -> CommandResult: + env_dir, _python_bin, pip_bin = ensure_env(project) + + packages: List[str] = [f"dbt-core=={project.dbt_version}"] + if project.extra_packages: + packages.extend([pkg.strip() for pkg in project.extra_packages.split(",") if pkg.strip()]) + + proc = subprocess.run([str(pip_bin), "install", *packages], capture_output=True, text=True) + return CommandResult(returncode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr) + + +def run_dbt_parse(project: Project) -> CommandResult: + env_dir = get_project_env_dir(project.id) # type: ignore[arg-type] + dbt_bin = env_dir / "bin" / "dbt" + # Fallback: Some envs may only have module entrypoint + if not dbt_bin.exists(): + cmd = [str(get_env_python_bin(env_dir)), "-m", "dbt", "parse", "--project-dir", project.root_path] + else: + cmd = [str(dbt_bin), "parse", "--project-dir", project.root_path] + + if project.profiles_dir: + cmd += ["--profiles-dir", project.profiles_dir] + + proc = subprocess.run(cmd, capture_output=True, text=True, cwd=project.root_path) + return CommandResult(returncode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr) + + +def compute_stats_from_manifest(project: Project) -> Dict[str, int]: + manifest_path = Path(project.root_path) / "target" / "manifest.json" + if not manifest_path.exists(): + return {} + + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = json.load(f) + + nodes = manifest.get("nodes", {}) + exposures = manifest.get("exposures", {}) + metrics = manifest.get("metrics", {}) or {} + + stats = { + "models": 0, + "tests": 0, + "snapshots": 0, + "analyses": 0, + "seeds": 0, + "sources": 0, + "macros": len(manifest.get("macros", {})), + "exposures": len(exposures), + "metrics": len(metrics), + "packages": len(manifest.get("metadata", {}).get("dependencies", [])), + } + + for node in nodes.values(): + resource_type = node.get("resource_type") + if resource_type == "model": + stats["models"] += 1 + elif resource_type == "test": + stats["tests"] += 1 + elif resource_type == "snapshot": + stats["snapshots"] += 1 + elif resource_type == "analysis": + stats["analyses"] += 1 + elif resource_type == "seed": + stats["seeds"] += 1 + elif resource_type == "source": + stats["sources"] += 1 + + return stats + + +def create_job(project: Project, job_type: JobType) -> Job: + log_file = JOBS_LOGS_DIR / f"job_{project.id}_{job_type.value}_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}.log" + job = Job(project_id=project.id, job_type=job_type, status=JobStatus.PENDING, log_path=str(log_file)) + with get_session() as session: + session.add(job) + session.flush() + session.refresh(job) + return job + + +def run_job(job: Job, project: Project) -> None: + with get_session() as session: + job.started_at = datetime.utcnow() + job.status = JobStatus.RUNNING + session.add(job) + + def write_log(text: str) -> None: + if job.log_path: + with open(job.log_path, "a", encoding="utf-8") as lf: + lf.write(text) + + try: + if job.job_type == JobType.INSTALL: + write_log(f"Installing packages for project {project.name}...\n") + res = install_dbt_for_project(project) + write_log(res.stdout) + if res.stderr: + write_log("\n[stderr]\n" + res.stderr) + success = res.returncode == 0 + elif job.job_type == JobType.EVALUATE: + write_log(f"Running dbt parse for project {project.name}...\n") + res = run_dbt_parse(project) + write_log(res.stdout) + if res.stderr: + write_log("\n[stderr]\n" + res.stderr) + success = res.returncode == 0 + else: + write_log("Unknown job type\n") + success = False + + with get_session() as session2: + job.status = JobStatus.SUCCESS if success else JobStatus.FAILED + job.finished_at = datetime.utcnow() + if not success: + job.error_message = "Job failed. See logs." + session2.add(job) + except Exception as exc: # noqa: BLE001 + write_log(f"Exception: {exc}\n") + with get_session() as session3: + job.status = JobStatus.FAILED + job.finished_at = datetime.utcnow() + job.error_message = str(exc) + session3.add(job) \ No newline at end of file diff --git a/dbt-admin/app/templates/base.html b/dbt-admin/app/templates/base.html new file mode 100644 index 0000000..43c7a8a --- /dev/null +++ b/dbt-admin/app/templates/base.html @@ -0,0 +1,20 @@ + + + + + + DBT Admin UI + + + + +
+ {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/dbt-admin/app/templates/home.html b/dbt-admin/app/templates/home.html new file mode 100644 index 0000000..bf94cf8 --- /dev/null +++ b/dbt-admin/app/templates/home.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} +

Projects

+
+ {% for project in projects %} + +
+
+
{{ project.name }}
+
dbt {{ project.dbt_version }}
+
+
{{ project.root_path }}
+
+
+ {% else %} +
No projects yet. Create one.
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/dbt-admin/app/templates/new_project.html b/dbt-admin/app/templates/new_project.html new file mode 100644 index 0000000..cf8c7e2 --- /dev/null +++ b/dbt-admin/app/templates/new_project.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block content %} +

New Project

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/dbt-admin/app/templates/project_detail.html b/dbt-admin/app/templates/project_detail.html new file mode 100644 index 0000000..6619beb --- /dev/null +++ b/dbt-admin/app/templates/project_detail.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} +{% block content %} +
+

{{ project.name }}

+
+ +
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + + +
+ +
+
+ + +
+

Recent Jobs

+ + + + + + + + + + + + + {% for job in jobs %} + + + + + + + + + {% endfor %} + +
IDTypeStatusStartedFinishedLogs
{{ job.id }}{{ job.job_type }} + {% if job.status == 'success' %} + {{ job.status }} + {% elif job.status == 'failed' %} + {{ job.status }} + {% elif job.status == 'running' %} + {{ job.status }} + {% else %} + {{ job.status }} + {% endif %} + {{ job.started_at or '' }}{{ job.finished_at or '' }} + {% if job.log_path %} + View + {% endif %} +
+
+
+ +
+
+

Stats

+ {% if stats %} +
+
Models{{ stats.models }}
+
Tests{{ stats.tests }}
+
Snapshots{{ stats.snapshots }}
+
Analyses{{ stats.analyses }}
+
Seeds{{ stats.seeds }}
+
Sources{{ stats.sources }}
+
Macros{{ stats.macros }}
+
Exposures{{ stats.exposures }}
+
Metrics{{ stats.metrics }}
+
Packages{{ stats.packages }}
+
+ {% else %} +
Run Evaluate to generate manifest and stats.
+ {% endif %} +
+ +
+

Info

+
Project Root: {{ project.root_path }}
+
Profiles Dir: {{ project.profiles_dir or '—' }}
+
dbt-core: {{ project.dbt_version }}
+
Extra packages: {{ project.extra_packages or '—' }}
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/dbt-admin/data/app.sqlite3 b/dbt-admin/data/app.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..dbf350c83b88c1aeaa1eaaba53f91b7d1fdf9c12 GIT binary patch literal 12288 zcmeI#K~KUk7zW^NnixqiCLTO+upUfUV!U|JqZ5k~acG?;CML^Z1%|+|t42H${g3_} zTf(LzLGSu5jBVe`$J(c-USHKC$w@mFgFupfW8E-Ks zsluq^QQ|qrbxKr$lnbj-NNTP}PbnjA!z1oZy}nBdt0NKjcuV>VV%6-+@raXKhgHrT zRz9?I?THNJQ<9|z`Lrc?AbIGkJ>iKOkXO5)^W9=D*JEl&!zk(I`*gh@cl=QxyXot6 zjSCSAf54L@=wzeR!6mC*IP8v`(>qd*LR;6W4MuCHuCA58CTR~X*Mwnv|aV&Ea%cWsc`p;Y>Q9Mj1p3BvX+fkn< zei(`L2UD3BDFXGktp_EOEs;)sTb8F$_~Ww8(-v&KH&>f}91ws21Rwwb2tWV=5P$## mAOHaf{9ys?|NrpfVrme800bZa0SG_<0uX=z1Rwx`nZPF(mcv&7 literal 0 HcmV?d00001 diff --git a/dbt-admin/requirements.txt b/dbt-admin/requirements.txt new file mode 100644 index 0000000..4dbdc21 --- /dev/null +++ b/dbt-admin/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.111.0 +uvicorn[standard]==0.30.1 +jinja2==3.1.4 +aiofiles==24.1.0 +sqlmodel==0.0.21 +sqlalchemy==2.0.32 +pydantic==2.8.2 +httpx==0.27.0 +python-multipart==0.0.9 +virtualenv==20.26.3 \ No newline at end of file From e72e6ad34b2dd3e5105aebce3d43ff0c8ba7a529 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 8 Aug 2025 18:52:09 +0000 Subject: [PATCH 2/2] Add preview routes and chart.js for project stats visualization Co-authored-by: akshay.agrawal1101 --- .../app/__pycache__/main.cpython-313.pyc | Bin 8712 -> 10926 bytes dbt-admin/app/main.py | 47 +++++++++++++++++- dbt-admin/app/templates/base.html | 6 ++- dbt-admin/app/templates/project_detail.html | 29 +++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/dbt-admin/app/__pycache__/main.cpython-313.pyc b/dbt-admin/app/__pycache__/main.cpython-313.pyc index 675c88342d1e44c48b087228730293c22f02ae8e..e45e10fadc9585c839235c85aa4f022eb53c179f 100644 GIT binary patch delta 5048 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 delta 2999 zcma)8O>7%Q6yCMh_BwW)KgV&L#BtO7belF!f6_LkK#LL}>!y@!TZHOjoOly&y$(S7a{%xLMkgEBo2rpLP7{%kvMTfNZin(TnZOBfhvL26YtI1P8^(yNc;KC%zHE6 z`c+cskq7PVZ5;kRnE$x&e($|VoR8j{I9y3_3a1EZLH2=<$-cB-4nTm({bjKtdjX$pLu~2H8S;dPq(}QXYn3IR&W{m+_5onL{%?vF5m?dHb`rL}lq| zceoc}G&8D1j)yo7#!%{D(s(8~!YQ3f*DMbcnTe(6_gc8J(w*tXBP9IB>&GZPv%J!w zL}$H7AX-};Yphmq8IIz1FSOXMx1lzv#1h=PFjG^4V`y=!g+*T`qQqxC$}y$CNq_19 z{Y0fh zqI>E9x{Yi>nw2A4N11xq+Vpc1Wv?>p&VR2G9+GKHV%_E(%zZPP{yd&mwmTGAms>D0OwVYMJ zU-7w;r5SpzSoQk0c)rVi%|9C(#R5s-X9f`Na=!_ow-#We`h$OwpWY4@j16_FU~Lp7 zj}rj(m9lDDj#vU?U0tzEDG1$Y<@j(OwjgFNh+}-hHpKYk5t%w^&;$B&y@;%Uig9kR^39{#WOXL|ttayY+57XVmD7s9M{%&-Q`;=-PP%aw zn4d65+QPcAY&t&E%3*9fYt+|N3z%Efso^L=kG0*&k`&UTp)Oi;39$)VSd0>n%MQ@pop{N!IPvsOK^#RG9`(?`2W}Nyql|C&rA{Z zU4I&hDwpU&G!k_kH5RM)yT9c3vmc`aciH<}v8S{aRZDe=d0k?Sz&00p#8qvaOm9RU zKa;|W(X57hf8mwp5MGzOj^D}wrrr_o$HW73>IPnTS6{J0v zxXY{6?-64Z$?eD$W6ezIhLu`1%6c9a>|2B573x)P9rmE)h(%*9Tf)`|>}HYgj$eIM zGc6O^Y2Y<2kOaq%5z1m$Ts3qRTOPGJpo*2++&)CXb(;~BSVv+KwWWvc^(KA`+1+Mc3QV%DH4@s1 z<=F9Rx`jz$x5lE`Bv(OvyUb3!xFNN+N}9d~)MRctfz=$AR7nGRZkXE;B#Bbsbk!E8us@d!9bXT?|KDo0!BWyw z*PtG$t*mNA)QQ-ikH_aaXa`Sz8M?@Xy-WTL18<)Rly0CF47~)@g&fgTVN+W{bJi@` zo$0q9j$f;=08#UFL_^Q2o2qWXDA`>lpeKvHK8{c_Fa`58rn?N(Q-H2FyCOhU$_fDv zku*ZUo<8v+(kd6#C*VDtqX*8kJskI^m*@E%ZY2CG*SW(D?QroOZlHR9V#^cy%Omph HJlo=5k4ixI diff --git a/dbt-admin/app/main.py b/dbt-admin/app/main.py index 0dc1b16..35676c7 100644 --- a/dbt-admin/app/main.py +++ b/dbt-admin/app/main.py @@ -1,9 +1,10 @@ from __future__ import annotations import threading -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Optional +from types import SimpleNamespace from fastapi import FastAPI, Request, Form, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse @@ -140,4 +141,46 @@ def get_job_logs(job_id: int): # type: ignore[no-untyped-def] log_path = Path(job.log_path) if not log_path.exists(): raise HTTPException(status_code=404, detail="Logs not found") - return FileResponse(path=str(log_path), media_type="text/plain") \ No newline at end of file + return FileResponse(path=str(log_path), media_type="text/plain") + + +# Preview routes with mock data +@app.get("/preview", response_class=HTMLResponse) +def preview_home(request: Request): # type: ignore[no-untyped-def] + projects = [ + SimpleNamespace(id=1, name="Marketing Warehouse", dbt_version="1.8.6", root_path="/srv/dbt/marketing"), + SimpleNamespace(id=2, name="Finance Lakehouse", dbt_version="1.7.13", root_path="/srv/dbt/finance"), + ] + return templates.TemplateResponse("home.html", {"request": request, "projects": projects}) + + +@app.get("/preview/projects/{project_id}", response_class=HTMLResponse) +def preview_project(request: Request, project_id: int): # type: ignore[no-untyped-def] + project = SimpleNamespace( + id=project_id, + name="Marketing Warehouse" if project_id == 1 else "Finance Lakehouse", + root_path="/srv/dbt/marketing" if project_id == 1 else "/srv/dbt/finance", + dbt_version="1.8.6" if project_id == 1 else "1.7.13", + profiles_dir="/home/app/.dbt", + extra_packages="dbt-postgres==1.8.6, dbt-redshift==1.8.6", + ) + now = datetime.utcnow() + jobs = [ + SimpleNamespace(id=301, project_id=project_id, job_type="install", status="success", started_at=now - timedelta(hours=6), finished_at=now - timedelta(hours=6, minutes=2), log_path=None), + SimpleNamespace(id=302, project_id=project_id, job_type="evaluate", status="success", started_at=now - timedelta(hours=5), finished_at=now - timedelta(hours=5, minutes=1), log_path=None), + SimpleNamespace(id=303, project_id=project_id, job_type="evaluate", status="failed", started_at=now - timedelta(hours=3), finished_at=now - timedelta(hours=3, minutes=1), log_path=None), + SimpleNamespace(id=304, project_id=project_id, job_type="evaluate", status="running", started_at=now - timedelta(minutes=10), finished_at=None, log_path=None), + ] + stats = { + "models": 128, + "tests": 420, + "snapshots": 6, + "analyses": 4, + "seeds": 12, + "sources": 23, + "macros": 37, + "exposures": 5, + "metrics": 9, + "packages": 3, + } + return templates.TemplateResponse("project_detail.html", {"request": request, "project": project, "jobs": jobs, "stats": stats}) \ No newline at end of file diff --git a/dbt-admin/app/templates/base.html b/dbt-admin/app/templates/base.html index 43c7a8a..8adb4dc 100644 --- a/dbt-admin/app/templates/base.html +++ b/dbt-admin/app/templates/base.html @@ -5,12 +5,16 @@ DBT Admin UI +
diff --git a/dbt-admin/app/templates/project_detail.html b/dbt-admin/app/templates/project_detail.html index 6619beb..8619f5f 100644 --- a/dbt-admin/app/templates/project_detail.html +++ b/dbt-admin/app/templates/project_detail.html @@ -104,6 +104,35 @@

Stats

Metrics{{ stats.metrics }}
Packages{{ stats.packages }}
+
+ +
+ {% else %}
Run Evaluate to generate manifest and stats.
{% endif %}