fix askama template shenanigans

This commit is contained in:
Stuart Axelbrooke 2025-11-26 10:05:28 +08:00
parent f531730a6b
commit 4a1ff75ea9
11 changed files with 897 additions and 962 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ node_modules
**/node_modules **/node_modules
Cargo.toml Cargo.toml
Cargo.lock Cargo.lock
/askama.toml
databuild/databuild.rs databuild/databuild.rs
generated_number generated_number
target target

View file

@ -24,8 +24,11 @@ rust_library(
srcs = glob(["**/*.rs"]) + [ srcs = glob(["**/*.rs"]) + [
":generate_databuild_rust", ":generate_databuild_rust",
], ],
compile_data = glob(["web/templates/**"]) + ["askama.toml"],
crate_root = "lib.rs", crate_root = "lib.rs",
edition = "2021", edition = "2021",
# This is required to point to the `askama.toml`, which then points to the appropriate place for templates
rustc_env = {"CARGO_MANIFEST_DIR": "$(BINDIR)/" + package_name()},
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"@crates//:askama", "@crates//:askama",

2
databuild/askama.toml Normal file
View file

@ -0,0 +1,2 @@
[general]
dirs = ["web/templates"]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,83 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:1.5rem}
.stat-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem;text-decoration:none;color:inherit}
.stat-card:hover{border-color:var(--color-brand)}
.stat-card .value{font-size:1.5rem;font-weight:600}
.stat-card .label{font-size:.75rem;color:var(--color-text-muted)}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Home - DataBuild") %}
{% call nav("", base.graph_label) %}
<h1>Dashboard</h1>
<div class="stats-grid">
<a href="/wants" class="stat-card">
<div class="value">{{ active_wants_count }}</div>
<div class="label">Active Wants</div>
</a>
<a href="/job_runs" class="stat-card">
<div class="value">{{ active_job_runs_count }}</div>
<div class="label">Active Job Runs</div>
</a>
<a href="/partitions" class="stat-card">
<div class="value">{{ live_partitions_count }}</div>
<div class="label">Live Partitions</div>
</a>
</div>
{% call footer() %}

View file

@ -0,0 +1,127 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-succeeded,.status-jobrunsucceeded{background:#dcfce7;color:#166534}
.status-running,.status-jobrunrunning{background:#ede9fe;color:#5b21b6}
.status-failed,.status-jobrunfailed,.status-depmiss,.status-jobrundepmiss{background:#fee2e2;color:#991b1b}
.status-new,.status-jobrunnew{background:var(--color-brand-light);color:#92400e}
.detail-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem}
.detail-header h1{margin-bottom:0}
.detail-section{background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem;margin-bottom:1rem}
.detail-section h2{font-size:.875rem;font-weight:500;color:var(--color-text-muted);margin-bottom:.75rem}
.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}
.detail-item label{display:block;font-size:.75rem;color:var(--color-text-muted);margin-bottom:.25rem}
.partition-list{list-style:none;font-family:monospace;font-size:.8125rem}
.partition-list li{padding:.25rem 0}
.partition-list a{color:var(--color-brand);text-decoration:none}
.partition-list a:hover{text-decoration:underline}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Job Run - DataBuild") %}
{% call nav("job_runs", base.graph_label) %}
<div class="detail-header" style="view-transition-name:job-run-header">
<h1>Job Run: {{ job_run.id }}</h1>
{% match job_run.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}
{% endmatch %}
</div>
<div class="detail-section">
<h2>Details</h2>
<div class="detail-grid">
<div class="detail-item">
<label>Last Heartbeat</label>
<span>
{% match job_run.last_heartbeat_at %}
{% when Some with (ts) %}{{ ts }}
{% when None %}-
{% endmatch %}
</span>
</div>
</div>
</div>
<div class="detail-section">
<h2>Building Partitions ({{ job_run.building_partitions.len() }})</h2>
<ul class="partition-list">
{% for p in job_run.building_partitions %}
<li><a href="/partitions/{{ p.partition_ref_encoded }}">{{ p.partition_ref }}</a></li>
{% endfor %}
{% if job_run.building_partitions.is_empty() %}
<li style="color:var(--color-text-muted)">No partitions</li>
{% endif %}
</ul>
</div>
{% if !job_run.servicing_wants.is_empty() %}
<div class="detail-section">
<h2>Servicing Wants ({{ job_run.servicing_wants.len() }})</h2>
{% for sw in job_run.servicing_wants %}
<div style="margin-bottom:.75rem">
<div><a href="/wants/{{ sw.want_id }}">{{ sw.want_id }}</a></div>
<ul class="partition-list" style="margin-left:1rem">
{% for p in sw.partitions %}
<li>{{ p.partition_ref }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endif %}
{% call footer() %}

View file

@ -0,0 +1,129 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
table{width:100%;background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;border-collapse:collapse;font-size:.875rem}
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid var(--color-border)}
th{background:var(--color-bg);font-weight:500;color:var(--color-text-muted)}
tr:last-child td{border-bottom:none}
tr:hover{background:var(--color-bg)}
td a{color:var(--color-brand);text-decoration:none}
td a:hover{text-decoration:underline}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-succeeded,.status-jobrunsucceeded{background:#dcfce7;color:#166534}
.status-running,.status-jobrunrunning{background:#ede9fe;color:#5b21b6}
.status-failed,.status-jobrunfailed,.status-depmiss,.status-jobrundepmiss{background:#fee2e2;color:#991b1b}
.status-new,.status-jobrunnew{background:var(--color-brand-light);color:#92400e}
.pagination{display:flex;gap:.5rem;margin-top:1rem;align-items:center;justify-content:center}
.pagination a,.pagination span{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:.25rem;text-decoration:none;color:var(--color-text);font-size:.875rem}
.pagination a:hover{background:var(--color-bg)}
.pagination .disabled{color:var(--color-text-muted)}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Job Runs - DataBuild") %}
{% call nav("job_runs", base.graph_label) %}
<h1>Job Runs</h1>
<table>
<thead>
<tr>
<th>Job Run ID</th>
<th>Status</th>
<th>Building Partitions</th>
<th>Last Heartbeat</th>
</tr>
</thead>
<tbody>
{% for jr in job_runs %}
<tr style="view-transition-name: job-run-{{ loop.index }}">
<td><a href="/job_runs/{{ jr.id }}">{{ jr.id }}</a></td>
<td>
{% match jr.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}<span class="status">Unknown</span>
{% endmatch %}
</td>
<td>{{ jr.building_partitions.len() }}</td>
<td>
{% match jr.last_heartbeat_at %}
{% when Some with (ts) %}{{ ts }}
{% when None %}-
{% endmatch %}
</td>
</tr>
{% endfor %}
{% if job_runs.is_empty() %}
<tr>
<td colspan="4" style="text-align:center;color:var(--color-text-muted)">No job runs found</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="pagination">
{% if self.has_prev() %}
<a href="?page={{ self.prev_page() }}">Previous</a>
{% else %}
<span class="disabled">Previous</span>
{% endif %}
<span>Page {{ page + 1 }} of {{ (total_count + page_size - 1) / page_size }}</span>
{% if self.has_next() %}
<a href="?page={{ self.next_page() }}">Next</a>
{% else %}
<span class="disabled">Next</span>
{% endif %}
</div>
{% call footer() %}

View file

@ -0,0 +1,141 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-live,.status-partitionlive{background:#dcfce7;color:#166534}
.status-building,.status-partitionbuilding{background:#ede9fe;color:#5b21b6}
.status-failed,.status-partitionfailed{background:#fee2e2;color:#991b1b}
.status-idle,.status-partitionidle{background:#e0f2fe;color:#075985}
.status-tainted,.status-partitiontainted{background:#fef3c7;color:#92400e}
.status-upstreambuilding,.status-partitionupstreambuilding{background:#fae8ff;color:#86198f}
.status-upstreamfailed,.status-partitionupstreamfailed{background:#ffe4e6;color:#be123c}
.detail-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem}
.detail-header h1{margin-bottom:0}
.detail-section{background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem;margin-bottom:1rem}
.detail-section h2{font-size:.875rem;font-weight:500;color:var(--color-text-muted);margin-bottom:.75rem}
.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}
.detail-item label{display:block;font-size:.75rem;color:var(--color-text-muted);margin-bottom:.25rem}
.partition-list{list-style:none;font-family:monospace;font-size:.8125rem}
.partition-list li{padding:.25rem 0}
.partition-list a{color:var(--color-brand);text-decoration:none}
.partition-list a:hover{text-decoration:underline}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Partition - DataBuild") %}
{% call nav("partitions", base.graph_label) %}
<div class="detail-header" style="view-transition-name:partition-header">
<h1 style="font-family:monospace;font-size:1.25rem">
{% if partition.has_partition_ref %}{{ partition.partition_ref }}{% else %}Unknown{% endif %}
</h1>
{% match partition.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}
{% endmatch %}
</div>
<div class="detail-section">
<h2>Details</h2>
<div class="detail-grid">
<div class="detail-item">
<label>UUID</label>
<span style="font-family:monospace;font-size:.8125rem">{{ partition.uuid }}</span>
</div>
<div class="detail-item">
<label>Last Updated</label>
<span>
{% match partition.last_updated_timestamp %}
{% when Some with (ts) %}{{ ts }}
{% when None %}-
{% endmatch %}
</span>
</div>
</div>
</div>
{% if !partition.job_run_ids.is_empty() %}
<div class="detail-section">
<h2>Job Runs ({{ partition.job_run_ids.len() }})</h2>
<ul class="partition-list">
{% for id in partition.job_run_ids %}
<li><a href="/job_runs/{{ id }}">{{ id }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if !partition.want_ids.is_empty() %}
<div class="detail-section">
<h2>Referenced by Wants ({{ partition.want_ids.len() }})</h2>
<ul class="partition-list">
{% for id in partition.want_ids %}
<li><a href="/wants/{{ id }}">{{ id }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if !partition.taint_ids.is_empty() %}
<div class="detail-section">
<h2>Taints ({{ partition.taint_ids.len() }})</h2>
<ul class="partition-list">
{% for id in partition.taint_ids %}
<li>{{ id }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% call footer() %}

View file

@ -0,0 +1,136 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
table{width:100%;background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;border-collapse:collapse;font-size:.875rem}
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid var(--color-border)}
th{background:var(--color-bg);font-weight:500;color:var(--color-text-muted)}
tr:last-child td{border-bottom:none}
tr:hover{background:var(--color-bg)}
td a{color:var(--color-brand);text-decoration:none}
td a:hover{text-decoration:underline}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-live,.status-partitionlive{background:#dcfce7;color:#166534}
.status-building,.status-partitionbuilding{background:#ede9fe;color:#5b21b6}
.status-failed,.status-partitionfailed{background:#fee2e2;color:#991b1b}
.status-idle,.status-partitionidle{background:#e0f2fe;color:#075985}
.status-tainted,.status-partitiontainted{background:#fef3c7;color:#92400e}
.status-upstreambuilding,.status-partitionupstreambuilding{background:#fae8ff;color:#86198f}
.status-upstreamfailed,.status-partitionupstreamfailed{background:#ffe4e6;color:#be123c}
.pagination{display:flex;gap:.5rem;margin-top:1rem;align-items:center;justify-content:center}
.pagination a,.pagination span{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:.25rem;text-decoration:none;color:var(--color-text);font-size:.875rem}
.pagination a:hover{background:var(--color-bg)}
.pagination .disabled{color:var(--color-text-muted)}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Partitions - DataBuild") %}
{% call nav("partitions", base.graph_label) %}
<h1>Partitions</h1>
<table>
<thead>
<tr>
<th>Partition Ref</th>
<th>Status</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
{% for p in partitions %}
<tr style="view-transition-name: partition-{{ loop.index }}">
<td>
{% if p.has_partition_ref %}
<a href="/partitions/{{ p.partition_ref_encoded }}">{{ p.partition_ref }}</a>
{% else %}
<span style="color:var(--color-text-muted)">-</span>
{% endif %}
</td>
<td>
{% match p.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}<span class="status">Unknown</span>
{% endmatch %}
</td>
<td>
{% match p.last_updated_timestamp %}
{% when Some with (ts) %}{{ ts }}
{% when None %}-
{% endmatch %}
</td>
</tr>
{% endfor %}
{% if partitions.is_empty() %}
<tr>
<td colspan="3" style="text-align:center;color:var(--color-text-muted)">No partitions found</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="pagination">
{% if self.has_prev() %}
<a href="?page={{ self.prev_page() }}">Previous</a>
{% else %}
<span class="disabled">Previous</span>
{% endif %}
<span>Page {{ page + 1 }} of {{ (total_count + page_size - 1) / page_size }}</span>
{% if self.has_next() %}
<a href="?page={{ self.next_page() }}">Next</a>
{% else %}
<span class="disabled">Next</span>
{% endif %}
</div>
{% call footer() %}

View file

@ -0,0 +1,141 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-successful,.status-wantsuccessful{background:#dcfce7;color:#166534}
.status-building,.status-wantbuilding{background:#ede9fe;color:#5b21b6}
.status-failed,.status-wantfailed{background:#fee2e2;color:#991b1b}
.status-canceled,.status-wantcanceled{background:#f1f5f9;color:#475569}
.status-new,.status-wantnew,.status-queued,.status-wantqueued{background:var(--color-brand-light);color:#92400e}
.status-upstreambuilding,.status-wantupstreambuilding{background:#fae8ff;color:#86198f}
.status-upstreamfailed,.status-wantupstreamfailed{background:#ffe4e6;color:#be123c}
.detail-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem}
.detail-header h1{margin-bottom:0}
.detail-section{background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem;margin-bottom:1rem}
.detail-section h2{font-size:.875rem;font-weight:500;color:var(--color-text-muted);margin-bottom:.75rem}
.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}
.detail-item label{display:block;font-size:.75rem;color:var(--color-text-muted);margin-bottom:.25rem}
.partition-list{list-style:none;font-family:monospace;font-size:.8125rem}
.partition-list li{padding:.25rem 0}
.partition-list a{color:var(--color-brand);text-decoration:none}
.partition-list a:hover{text-decoration:underline}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Want - DataBuild") %}
{% call nav("wants", base.graph_label) %}
<div class="detail-header" style="view-transition-name:want-header">
<h1>Want: {{ want.want_id }}</h1>
{% match want.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}
{% endmatch %}
</div>
<div class="detail-section">
<h2>Details</h2>
<div class="detail-grid">
<div class="detail-item">
<label>Data Timestamp</label>
<span>{{ want.data_timestamp }}</span>
</div>
<div class="detail-item">
<label>TTL (seconds)</label>
<span>{{ want.ttl_seconds }}</span>
</div>
<div class="detail-item">
<label>SLA (seconds)</label>
<span>{{ want.sla_seconds }}</span>
</div>
<div class="detail-item">
<label>Last Updated</label>
<span>{{ want.last_updated_timestamp }}</span>
</div>
</div>
</div>
{% match want.comment %}
{% when Some with (c) %}
<div class="detail-section">
<h2>Comment</h2>
<p>{{ c }}</p>
</div>
{% when None %}
{% endmatch %}
<div class="detail-section">
<h2>Requested Partitions ({{ want.partitions.len() }})</h2>
<ul class="partition-list">
{% for p in want.partitions %}
<li><a href="/partitions/{{ p.partition_ref_encoded }}">{{ p.partition_ref }}</a></li>
{% endfor %}
{% if want.partitions.is_empty() %}
<li style="color:var(--color-text-muted)">No partitions</li>
{% endif %}
</ul>
</div>
{% if !want.upstreams.is_empty() %}
<div class="detail-section">
<h2>Upstream Dependencies ({{ want.upstreams.len() }})</h2>
<ul class="partition-list">
{% for p in want.upstreams %}
<li><a href="/partitions/{{ p.partition_ref_encoded }}">{{ p.partition_ref }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% call footer() %}

View file

@ -0,0 +1,127 @@
{% macro head(title) %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">
<title>{{ title }}</title>
<style>
@view-transition{navigation:auto}
:root{--color-brand:#F2994A;--color-brand-light:#fef3e2;--color-text:#333;--color-text-muted:#6b7280;--color-bg:#f9fafb;--color-surface:#fff;--color-border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.5}
nav{background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:.75rem 1.5rem;display:flex;align-items:center;gap:2rem;view-transition-name:nav}
.logo{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--color-text)}
.logo svg{width:28px;height:25px}
.logo span{font-family:'Roboto Slab',serif;font-weight:600;font-size:1.125rem}
nav .links{display:flex;gap:1.5rem}
nav .links a{color:var(--color-text-muted);text-decoration:none;font-size:.875rem;padding:.25rem 0;border-bottom:2px solid transparent}
nav .links a:hover{color:var(--color-text)}
nav .links a.active{color:var(--color-brand);border-bottom-color:var(--color-brand)}
nav .graph-label{margin-left:auto;color:var(--color-text-muted);font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:1.5rem}
h1{font-size:1.5rem;font-weight:600;margin-bottom:1rem}
table{width:100%;background:var(--color-surface);border:1px solid var(--color-border);border-radius:.5rem;border-collapse:collapse;font-size:.875rem}
th,td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid var(--color-border)}
th{background:var(--color-bg);font-weight:500;color:var(--color-text-muted)}
tr:last-child td{border-bottom:none}
tr:hover{background:var(--color-bg)}
td a{color:var(--color-brand);text-decoration:none}
td a:hover{text-decoration:underline}
.status{display:inline-block;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500}
.status-successful,.status-wantsuccessful{background:#dcfce7;color:#166534}
.status-building,.status-wantbuilding{background:#ede9fe;color:#5b21b6}
.status-failed,.status-wantfailed{background:#fee2e2;color:#991b1b}
.status-canceled,.status-wantcanceled{background:#f1f5f9;color:#475569}
.status-new,.status-wantnew,.status-queued,.status-wantqueued{background:var(--color-brand-light);color:#92400e}
.status-upstreambuilding,.status-wantupstreambuilding{background:#fae8ff;color:#86198f}
.status-upstreamfailed,.status-wantupstreamfailed{background:#ffe4e6;color:#be123c}
.pagination{display:flex;gap:.5rem;margin-top:1rem;align-items:center;justify-content:center}
.pagination a,.pagination span{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:.25rem;text-decoration:none;color:var(--color-text);font-size:.875rem}
.pagination a:hover{background:var(--color-bg)}
.pagination .disabled{color:var(--color-text-muted)}
</style>
</head>
<body>
{% endmacro %}
{% macro nav(active, graph_label) %}
<nav>
<a href="/" class="logo">
<svg viewBox="0 0 243 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.5 77L149.048 121.25H97.9523L123.5 77Z" fill="#F2994A"/>
<path d="M224.772 125.035L155.772 125.035L109.52 45.3147L86.7722 45.3147L40.2722 124.463L16.7725 124.463" stroke="#333" stroke-width="20"/>
<path d="M86.6196 5.18886L121.12 64.9444L75.2062 144.86L86.58 164.56L178.375 165.256L190.125 185.608" stroke="#333" stroke-width="20"/>
<path d="M51.966 184.847L86.4659 125.092L178.632 124.896L190.006 105.196L144.711 25.3514L156.461 5.00002" stroke="#333" stroke-width="20"/>
</svg>
<span>DataBuild</span>
</a>
<div class="links">
<a href="/wants"{% if active == "wants" %} class="active"{% endif %}>Wants</a>
<a href="/partitions"{% if active == "partitions" %} class="active"{% endif %}>Partitions</a>
<a href="/job_runs"{% if active == "job_runs" %} class="active"{% endif %}>Job Runs</a>
</div>
<span class="graph-label">{{ graph_label }}</span>
</nav>
<main>
{% endmacro %}
{% macro footer() %}
</main>
</body>
</html>
{% endmacro %}
{% call head("Wants - DataBuild") %}
{% call nav("wants", base.graph_label) %}
<h1>Wants</h1>
<table>
<thead>
<tr>
<th>Want ID</th>
<th>Status</th>
<th>Partitions</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
{% for want in wants %}
<tr style="view-transition-name: want-{{ loop.index }}">
<td><a href="/wants/{{ want.want_id }}">{{ want.want_id }}</a></td>
<td>
{% match want.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>
{% when None %}<span class="status">Unknown</span>
{% endmatch %}
</td>
<td>{{ want.partitions.len() }}</td>
<td>{{ want.comment_display }}</td>
</tr>
{% endfor %}
{% if wants.is_empty() %}
<tr>
<td colspan="4" style="text-align:center;color:var(--color-text-muted)">No wants found</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="pagination">
{% if self.has_prev() %}
<a href="?page={{ self.prev_page() }}">Previous</a>
{% else %}
<span class="disabled">Previous</span>
{% endif %}
<span>Page {{ page + 1 }} of {{ (total_count + page_size - 1) / page_size }}</span>
{% if self.has_next() %}
<a href="?page={{ self.next_page() }}">Next</a>
{% else %}
<span class="disabled">Next</span>
{% endif %}
</div>
{% call footer() %}