diff --git a/.gitignore b/.gitignore
index 1b0fdd0..bf199a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ node_modules
**/node_modules
Cargo.toml
Cargo.lock
+/askama.toml
databuild/databuild.rs
generated_number
target
diff --git a/databuild/BUILD.bazel b/databuild/BUILD.bazel
index a9f5ab4..7111579 100644
--- a/databuild/BUILD.bazel
+++ b/databuild/BUILD.bazel
@@ -24,8 +24,11 @@ rust_library(
srcs = glob(["**/*.rs"]) + [
":generate_databuild_rust",
],
+ compile_data = glob(["web/templates/**"]) + ["askama.toml"],
crate_root = "lib.rs",
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"],
deps = [
"@crates//:askama",
diff --git a/databuild/askama.toml b/databuild/askama.toml
new file mode 100644
index 0000000..c1ac760
--- /dev/null
+++ b/databuild/askama.toml
@@ -0,0 +1,2 @@
+[general]
+dirs = ["web/templates"]
diff --git a/databuild/web/templates.rs b/databuild/web/templates.rs
index 5bae4be..d48d35e 100644
--- a/databuild/web/templates.rs
+++ b/databuild/web/templates.rs
@@ -1,54 +1,4 @@
//! Askama template structs for the DataBuild dashboard.
-//!
-//! # TODO: Bazel + Askama Template Integration
-//!
-//! ## Current State
-//!
-//! Templates are embedded inline using Askama's `source` attribute rather than
-//! file-based `.html` templates. This is a **workaround** due to Bazel/Askama
-//! incompatibility, not our preferred approach.
-//!
-//! ## The Problem
-//!
-//! Askama's derive macro resolves template files at compile time relative to
-//! `CARGO_MANIFEST_DIR`. In Bazel's sandboxed builds, this environment variable
-//! either isn't set or points to the wrong location, causing template files to
-//! not be found even when included via `compile_data` or `data` attributes.
-//!
-//! We attempted several fixes:
-//! - `rustc_env = {"CARGO_MANIFEST_DIR": "..."}` - doesn't work because the
-//! sandbox paths don't align with where Askama looks for files
-//! - `compile_data` with template files - files exist but Askama can't find them
-//!
-//! ## Current Workarounds
-//!
-//! 1. **Inline templates via `source` attribute**: All template content is embedded
-//! directly in Rust source strings. This works but has downsides:
-//! - No syntax highlighting or editor support for HTML
-//! - Shared components (head, nav, footer) must be duplicated as Askama macros
-//! in each template since Rust's `concat!` doesn't work with proc-macro attributes
-//!
-//! 2. **Double-hash raw strings `r##"..."##`**: HTML color codes like `stroke="#333"`
-//! contain `"#` which prematurely closes single-hash raw strings `r#"..."#`.
-//! Using double-hash avoids this. **Watch out for this footgun when editing templates!**
-//!
-//! ## Path Forward
-//!
-//! Our bias is to move to proper file-based `.html` templates. Options to explore:
-//!
-//! 1. **Custom Bazel rule or genrule**: Pre-process templates or set up the environment
-//! correctly before Askama's proc-macro runs
-//!
-//! 2. **Askama `source` with shared snippets**: If Askama supports importing/including
-//! from other `source`-defined templates, we could reduce duplication while staying
-//! inline. (Needs investigation - Askama macros currently require definition in the
-//! same template source string)
-//!
-//! 3. **Build-time template concatenation**: Generate the Rust file with templates
-//! from separate `.html` files during the build, before rustc runs
-//!
-//! Until resolved, this file contains all dashboard templates inline with duplicated
-//! macro definitions for head/nav/footer.
use askama::Template;
@@ -282,93 +232,7 @@ impl Default for BaseContext {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Home - DataBuild") %}
-{% call nav("", base.graph_label) %}
-
-Dashboard
-
-
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "home.html")]
pub struct HomePage {
pub base: BaseContext,
pub active_wants_count: u64,
@@ -381,137 +245,7 @@ pub struct HomePage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Wants - DataBuild") %}
-{% call nav("wants", base.graph_label) %}
-
-Wants
-
-
-
-
- Want ID
- Status
- Partitions
- Comment
-
-
-
- {% for want in wants %}
-
- {{ want.want_id }}
-
- {% match want.status %}
- {% when Some with (s) %}{{ s.name }}
- {% when None %}Unknown
- {% endmatch %}
-
- {{ want.partitions.len() }}
- {{ want.comment_display }}
-
- {% endfor %}
- {% if wants.is_empty() %}
-
- No wants found
-
- {% endif %}
-
-
-
-
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "wants/list.html")]
pub struct WantsListPage {
pub base: BaseContext,
pub wants: Vec,
@@ -540,151 +274,7 @@ impl WantsListPage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Want - DataBuild") %}
-{% call nav("wants", base.graph_label) %}
-
-
-
-
-
Details
-
-
- Data Timestamp
- {{ want.data_timestamp }}
-
-
- TTL (seconds)
- {{ want.ttl_seconds }}
-
-
- SLA (seconds)
- {{ want.sla_seconds }}
-
-
- Last Updated
- {{ want.last_updated_timestamp }}
-
-
-
-
-{% match want.comment %}
- {% when Some with (c) %}
-
- {% when None %}
-{% endmatch %}
-
-
-
Requested Partitions ({{ want.partitions.len() }})
-
- {% for p in want.partitions %}
- {{ p.partition_ref }}
- {% endfor %}
- {% if want.partitions.is_empty() %}
- No partitions
- {% endif %}
-
-
-
-{% if !want.upstreams.is_empty() %}
-
-
Upstream Dependencies ({{ want.upstreams.len() }})
-
-
-{% endif %}
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "wants/detail.html")]
pub struct WantDetailPage {
pub base: BaseContext,
pub want: WantDetailView,
@@ -695,146 +285,7 @@ pub struct WantDetailPage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Partitions - DataBuild") %}
-{% call nav("partitions", base.graph_label) %}
-
-Partitions
-
-
-
-
- Partition Ref
- Status
- Last Updated
-
-
-
- {% for p in partitions %}
-
-
- {% if p.has_partition_ref %}
- {{ p.partition_ref }}
- {% else %}
- -
- {% endif %}
-
-
- {% match p.status %}
- {% when Some with (s) %}{{ s.name }}
- {% when None %}Unknown
- {% endmatch %}
-
-
- {% match p.last_updated_timestamp %}
- {% when Some with (ts) %}{{ ts }}
- {% when None %}-
- {% endmatch %}
-
-
- {% endfor %}
- {% if partitions.is_empty() %}
-
- No partitions found
-
- {% endif %}
-
-
-
-
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "partitions/list.html")]
pub struct PartitionsListPage {
pub base: BaseContext,
pub partitions: Vec,
@@ -863,151 +314,7 @@ impl PartitionsListPage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Partition - DataBuild") %}
-{% call nav("partitions", base.graph_label) %}
-
-
-
-
-
Details
-
-
- UUID
- {{ partition.uuid }}
-
-
- Last Updated
-
- {% match partition.last_updated_timestamp %}
- {% when Some with (ts) %}{{ ts }}
- {% when None %}-
- {% endmatch %}
-
-
-
-
-
-{% if !partition.job_run_ids.is_empty() %}
-
-
Job Runs ({{ partition.job_run_ids.len() }})
-
- {% for id in partition.job_run_ids %}
- {{ id }}
- {% endfor %}
-
-
-{% endif %}
-
-{% if !partition.want_ids.is_empty() %}
-
-
Referenced by Wants ({{ partition.want_ids.len() }})
-
- {% for id in partition.want_ids %}
- {{ id }}
- {% endfor %}
-
-
-{% endif %}
-
-{% if !partition.taint_ids.is_empty() %}
-
-
Taints ({{ partition.taint_ids.len() }})
-
- {% for id in partition.taint_ids %}
- {{ id }}
- {% endfor %}
-
-
-{% endif %}
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "partitions/detail.html")]
pub struct PartitionDetailPage {
pub base: BaseContext,
pub partition: PartitionDetailView,
@@ -1018,139 +325,7 @@ pub struct PartitionDetailPage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Job Runs - DataBuild") %}
-{% call nav("job_runs", base.graph_label) %}
-
-Job Runs
-
-
-
-
- Job Run ID
- Status
- Building Partitions
- Last Heartbeat
-
-
-
- {% for jr in job_runs %}
-
- {{ jr.id }}
-
- {% match jr.status %}
- {% when Some with (s) %}{{ s.name }}
- {% when None %}Unknown
- {% endmatch %}
-
- {{ jr.building_partitions.len() }}
-
- {% match jr.last_heartbeat_at %}
- {% when Some with (ts) %}{{ ts }}
- {% when None %}-
- {% endmatch %}
-
-
- {% endfor %}
- {% if job_runs.is_empty() %}
-
- No job runs found
-
- {% endif %}
-
-
-
-
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "job_runs/list.html")]
pub struct JobRunsListPage {
pub base: BaseContext,
pub job_runs: Vec,
@@ -1179,137 +354,7 @@ impl JobRunsListPage {
// =============================================================================
#[derive(Template)]
-#[template(
- ext = "html",
- source = r##"{% macro head(title) %}
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-{% endmacro %}
-
-{% macro nav(active, graph_label) %}
-
-
-
-
-
-
-
-
- DataBuild
-
-
- {{ graph_label }}
-
-
-{% endmacro %}
-
-{% macro footer() %}
-
-
-
-{% endmacro %}
-
-{% call head("Job Run - DataBuild") %}
-{% call nav("job_runs", base.graph_label) %}
-
-
-
-
-
Details
-
-
- Last Heartbeat
-
- {% match job_run.last_heartbeat_at %}
- {% when Some with (ts) %}{{ ts }}
- {% when None %}-
- {% endmatch %}
-
-
-
-
-
-
-
Building Partitions ({{ job_run.building_partitions.len() }})
-
- {% for p in job_run.building_partitions %}
- {{ p.partition_ref }}
- {% endfor %}
- {% if job_run.building_partitions.is_empty() %}
- No partitions
- {% endif %}
-
-
-
-{% if !job_run.servicing_wants.is_empty() %}
-
-
Servicing Wants ({{ job_run.servicing_wants.len() }})
- {% for sw in job_run.servicing_wants %}
-
-
-
- {% for p in sw.partitions %}
- {{ p.partition_ref }}
- {% endfor %}
-
-
- {% endfor %}
-
-{% endif %}
-
-{% call footer() %}
-"##
-)]
+#[template(ext = "html", path = "job_runs/detail.html")]
pub struct JobRunDetailPage {
pub base: BaseContext,
pub job_run: JobRunDetailView,
diff --git a/databuild/web/templates/home.html b/databuild/web/templates/home.html
new file mode 100644
index 0000000..1dc79c7
--- /dev/null
+++ b/databuild/web/templates/home.html
@@ -0,0 +1,83 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Home - DataBuild") %}
+{% call nav("", base.graph_label) %}
+
+Dashboard
+
+
+
+{% call footer() %}
diff --git a/databuild/web/templates/job_runs/detail.html b/databuild/web/templates/job_runs/detail.html
new file mode 100644
index 0000000..a4132fb
--- /dev/null
+++ b/databuild/web/templates/job_runs/detail.html
@@ -0,0 +1,127 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Job Run - DataBuild") %}
+{% call nav("job_runs", base.graph_label) %}
+
+
+
+
+
Details
+
+
+ Last Heartbeat
+
+ {% match job_run.last_heartbeat_at %}
+ {% when Some with (ts) %}{{ ts }}
+ {% when None %}-
+ {% endmatch %}
+
+
+
+
+
+
+
Building Partitions ({{ job_run.building_partitions.len() }})
+
+ {% for p in job_run.building_partitions %}
+ {{ p.partition_ref }}
+ {% endfor %}
+ {% if job_run.building_partitions.is_empty() %}
+ No partitions
+ {% endif %}
+
+
+
+{% if !job_run.servicing_wants.is_empty() %}
+
+
Servicing Wants ({{ job_run.servicing_wants.len() }})
+ {% for sw in job_run.servicing_wants %}
+
+
+
+ {% for p in sw.partitions %}
+ {{ p.partition_ref }}
+ {% endfor %}
+
+
+ {% endfor %}
+
+{% endif %}
+
+{% call footer() %}
diff --git a/databuild/web/templates/job_runs/list.html b/databuild/web/templates/job_runs/list.html
new file mode 100644
index 0000000..a0f60e8
--- /dev/null
+++ b/databuild/web/templates/job_runs/list.html
@@ -0,0 +1,129 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Job Runs - DataBuild") %}
+{% call nav("job_runs", base.graph_label) %}
+
+Job Runs
+
+
+
+
+ Job Run ID
+ Status
+ Building Partitions
+ Last Heartbeat
+
+
+
+ {% for jr in job_runs %}
+
+ {{ jr.id }}
+
+ {% match jr.status %}
+ {% when Some with (s) %}{{ s.name }}
+ {% when None %}Unknown
+ {% endmatch %}
+
+ {{ jr.building_partitions.len() }}
+
+ {% match jr.last_heartbeat_at %}
+ {% when Some with (ts) %}{{ ts }}
+ {% when None %}-
+ {% endmatch %}
+
+
+ {% endfor %}
+ {% if job_runs.is_empty() %}
+
+ No job runs found
+
+ {% endif %}
+
+
+
+
+
+{% call footer() %}
diff --git a/databuild/web/templates/partitions/detail.html b/databuild/web/templates/partitions/detail.html
new file mode 100644
index 0000000..554283b
--- /dev/null
+++ b/databuild/web/templates/partitions/detail.html
@@ -0,0 +1,141 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Partition - DataBuild") %}
+{% call nav("partitions", base.graph_label) %}
+
+
+
+
+
Details
+
+
+ UUID
+ {{ partition.uuid }}
+
+
+ Last Updated
+
+ {% match partition.last_updated_timestamp %}
+ {% when Some with (ts) %}{{ ts }}
+ {% when None %}-
+ {% endmatch %}
+
+
+
+
+
+{% if !partition.job_run_ids.is_empty() %}
+
+
Job Runs ({{ partition.job_run_ids.len() }})
+
+ {% for id in partition.job_run_ids %}
+ {{ id }}
+ {% endfor %}
+
+
+{% endif %}
+
+{% if !partition.want_ids.is_empty() %}
+
+
Referenced by Wants ({{ partition.want_ids.len() }})
+
+ {% for id in partition.want_ids %}
+ {{ id }}
+ {% endfor %}
+
+
+{% endif %}
+
+{% if !partition.taint_ids.is_empty() %}
+
+
Taints ({{ partition.taint_ids.len() }})
+
+ {% for id in partition.taint_ids %}
+ {{ id }}
+ {% endfor %}
+
+
+{% endif %}
+
+{% call footer() %}
diff --git a/databuild/web/templates/partitions/list.html b/databuild/web/templates/partitions/list.html
new file mode 100644
index 0000000..3d37423
--- /dev/null
+++ b/databuild/web/templates/partitions/list.html
@@ -0,0 +1,136 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Partitions - DataBuild") %}
+{% call nav("partitions", base.graph_label) %}
+
+Partitions
+
+
+
+
+ Partition Ref
+ Status
+ Last Updated
+
+
+
+ {% for p in partitions %}
+
+
+ {% if p.has_partition_ref %}
+ {{ p.partition_ref }}
+ {% else %}
+ -
+ {% endif %}
+
+
+ {% match p.status %}
+ {% when Some with (s) %}{{ s.name }}
+ {% when None %}Unknown
+ {% endmatch %}
+
+
+ {% match p.last_updated_timestamp %}
+ {% when Some with (ts) %}{{ ts }}
+ {% when None %}-
+ {% endmatch %}
+
+
+ {% endfor %}
+ {% if partitions.is_empty() %}
+
+ No partitions found
+
+ {% endif %}
+
+
+
+
+
+{% call footer() %}
diff --git a/databuild/web/templates/wants/detail.html b/databuild/web/templates/wants/detail.html
new file mode 100644
index 0000000..333f420
--- /dev/null
+++ b/databuild/web/templates/wants/detail.html
@@ -0,0 +1,141 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Want - DataBuild") %}
+{% call nav("wants", base.graph_label) %}
+
+
+
+
+
Details
+
+
+ Data Timestamp
+ {{ want.data_timestamp }}
+
+
+ TTL (seconds)
+ {{ want.ttl_seconds }}
+
+
+ SLA (seconds)
+ {{ want.sla_seconds }}
+
+
+ Last Updated
+ {{ want.last_updated_timestamp }}
+
+
+
+
+{% match want.comment %}
+{% when Some with (c) %}
+
+{% when None %}
+{% endmatch %}
+
+
+
Requested Partitions ({{ want.partitions.len() }})
+
+ {% for p in want.partitions %}
+ {{ p.partition_ref }}
+ {% endfor %}
+ {% if want.partitions.is_empty() %}
+ No partitions
+ {% endif %}
+
+
+
+{% if !want.upstreams.is_empty() %}
+
+
Upstream Dependencies ({{ want.upstreams.len() }})
+
+
+{% endif %}
+
+{% call footer() %}
diff --git a/databuild/web/templates/wants/list.html b/databuild/web/templates/wants/list.html
new file mode 100644
index 0000000..3861e80
--- /dev/null
+++ b/databuild/web/templates/wants/list.html
@@ -0,0 +1,127 @@
+{% macro head(title) %}
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+{% endmacro %}
+
+{% macro nav(active, graph_label) %}
+
+
+
+
+
+
+
+
+ DataBuild
+
+
+ {{ graph_label }}
+
+
+ {% endmacro %}
+
+ {% macro footer() %}
+
+
+
+{% endmacro %}
+
+{% call head("Wants - DataBuild") %}
+{% call nav("wants", base.graph_label) %}
+
+Wants
+
+
+
+
+ Want ID
+ Status
+ Partitions
+ Comment
+
+
+
+ {% for want in wants %}
+
+ {{ want.want_id }}
+
+ {% match want.status %}
+ {% when Some with (s) %}{{ s.name }}
+ {% when None %}Unknown
+ {% endmatch %}
+
+ {{ want.partitions.len() }}
+ {{ want.comment_display }}
+
+ {% endfor %}
+ {% if wants.is_empty() %}
+
+ No wants found
+
+ {% endif %}
+
+
+
+
+
+{% call footer() %}