Compare commits

...

2 commits

Author SHA1 Message Date
ce8bb92cdb add want create page
Some checks are pending
/ setup (push) Waiting to run
2025-11-26 22:01:47 +08:00
d744d2a63f add want create page 2025-11-26 10:53:57 +08:00
10 changed files with 216 additions and 14 deletions

View file

@ -3,8 +3,8 @@ use crate::build_state::BuildState;
use crate::commands::Command;
use crate::web::templates::{
BaseContext, HomePage, JobRunDetailPage, JobRunDetailView, JobRunsListPage,
PartitionDetailPage, PartitionDetailView, PartitionsListPage, WantDetailPage, WantDetailView,
WantsListPage,
PartitionDetailPage, PartitionDetailView, PartitionsListPage, WantCreatePage, WantDetailPage,
WantDetailView, WantsListPage,
};
use crate::{
CancelWantRequest, CreateWantRequest, CreateWantResponse, GetWantRequest, GetWantResponse,
@ -99,6 +99,7 @@ pub fn create_router(state: AppState) -> Router {
// HTML pages
.route("/", get(home_page))
.route("/wants", get(wants_list_page))
.route("/wants/create", get(want_create_page))
.route("/wants/:id", get(want_detail_page))
.route("/partitions", get(partitions_list_page))
.route("/partitions/*id", get(partition_detail_page))
@ -276,6 +277,18 @@ async fn want_detail_page(
}
}
/// Want create page
async fn want_create_page() -> impl IntoResponse {
let template = WantCreatePage {
base: BaseContext::default(),
};
match template.render() {
Ok(html) => Html(html).into_response(),
Err(e) => Html(format!("<h1>Template error: {}</h1>", e)).into_response(),
}
}
/// Partitions list page
async fn partitions_list_page(
State(state): State<AppState>,

View file

@ -351,3 +351,13 @@ pub struct JobRunDetailPage {
pub base: BaseContext,
pub job_run: JobRunDetailView,
}
// =============================================================================
// Want Create Page
// =============================================================================
#[derive(Template)]
#[template(ext = "html", path = "wants/create.html")]
pub struct WantCreatePage {
pub base: BaseContext,
}

View file

@ -161,10 +161,72 @@
.detail-item label { display: block; font-size: .75rem; color: var(--color-text-muted); margin-bottom: .25rem }
/* Partition Lists */
.partition-list { list-style: none; font-family: monospace; font-size: .8125rem }
.partition-list { list-style: none }
.partition-list li { padding: .25rem 0 }
.partition-list a { color: var(--color-brand); text-decoration: none }
.partition-list a { text-decoration: none }
.partition-list a:hover { text-decoration: underline }
/* Partition Ref Tags (inline code style) */
.partition-ref {
display: inline-block;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: .8125rem;
background: #f1f5f9;
color: #334155;
padding: .125rem .375rem;
border-radius: .25rem;
border: 1px solid var(--color-border);
}
a.partition-ref { color: var(--color-brand) }
a.partition-ref:hover { background: var(--color-brand-light); text-decoration: none }
/* Forms */
.form-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: .5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.form-section h2 { font-size: .875rem; font-weight: 500; color: var(--color-text-muted); margin-bottom: .75rem }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1rem }
.form-field { margin-bottom: .75rem }
.form-field:last-child { margin-bottom: 0 }
.form-field label { display: block; font-size: .75rem; color: var(--color-text-muted); margin-bottom: .25rem }
.form-field input, .form-field textarea {
width: 100%;
padding: .5rem .75rem;
border: 1px solid var(--color-border);
border-radius: .25rem;
font-size: .875rem;
font-family: inherit;
}
.form-field input:focus, .form-field textarea:focus {
outline: none;
border-color: var(--color-brand);
}
.form-field small { display: block; font-size: .75rem; color: var(--color-text-muted); margin-top: .25rem }
.btn-primary {
display: inline-block;
padding: .5rem 1rem;
background: var(--color-brand);
color: #fff;
border: none;
border-radius: .25rem;
font-size: .875rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
}
.btn-primary:hover { background: #e07b2a }
.error-message {
background: #fee2e2;
color: #991b1b;
padding: .75rem 1rem;
border-radius: .25rem;
margin-bottom: 1rem;
font-size: .875rem;
}
</style>
</head>
<body>

View file

@ -30,7 +30,7 @@
<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>
<li><a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a></li>
{% endfor %}
{% if job_run.building_partitions.is_empty() %}
<li style="color:var(--color-text-muted)">No partitions</li>
@ -46,7 +46,7 @@
<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>
<li><span class="partition-ref">{{ p.partition_ref }}</span></li>
{% endfor %}
</ul>
</div>

View file

@ -24,7 +24,12 @@
{% when None %}<span class="status">Unknown</span>
{% endmatch %}
</td>
<td>{{ jr.building_partitions.len() }}</td>
<td>
{% for p in jr.building_partitions %}
<a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a>{% if !loop.last %} {% endif %}
{% endfor %}
{% if jr.building_partitions.is_empty() %}<span style="color:var(--color-text-muted)">-</span>{% endif %}
</td>
<td>
{% match jr.last_heartbeat_at %}
{% when Some with (ts) %}{{ ts }}

View file

@ -4,8 +4,8 @@
{% call base::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>
{% if partition.has_partition_ref %}<span class="partition-ref" style="font-size:1.125rem">{{ partition.partition_ref }}</span>{% else %}Unknown{% endif %}
</h1>
{% match partition.status %}
{% when Some with (s) %}<span class="status status-{{ s.name_lowercase }}">{{ s.name }}</span>

View file

@ -18,7 +18,7 @@
<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>
<a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a>
{% else %}
<span style="color:var(--color-text-muted)">-</span>
{% endif %}

View file

@ -0,0 +1,104 @@
{% import "base.html" as base %}
{% call base::head("Create Want - DataBuild") %}
{% call base::nav("wants", base.graph_label) %}
<h1>Create Want</h1>
<form id="create-want-form">
<div class="form-section">
<h2>Partitions</h2>
<div class="form-field">
<label for="partitions">Partition References</label>
<textarea id="partitions" name="partitions" rows="4"
placeholder="Enter partition refs, one per line"></textarea>
<small>e.g., daily_summaries/category=comedy/date=2024-01-01</small>
</div>
</div>
<div class="form-section">
<h2>Settings</h2>
<div class="form-grid">
<div class="form-field">
<label for="data_timestamp">Data Timestamp</label>
<input type="datetime-local" id="data_timestamp">
</div>
<div class="form-field">
<label for="ttl_seconds">TTL (seconds)</label>
<input type="number" id="ttl_seconds" value="3600" min="0">
</div>
<div class="form-field">
<label for="sla_seconds">SLA (seconds)</label>
<input type="number" id="sla_seconds" value="300" min="0">
</div>
</div>
<div class="form-field">
<label for="comment">Comment (optional)</label>
<textarea id="comment" name="comment" rows="2"></textarea>
</div>
</div>
<div id="error-message" class="error-message" style="display:none"></div>
<button type="submit" class="btn-primary">Create Want</button>
</form>
<script>
// Set default data timestamp to now
const now = new Date();
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
document.getElementById('data_timestamp').value = now.toISOString().slice(0, 16);
document.getElementById('create-want-form').addEventListener('submit', async (e) => {
e.preventDefault();
const partitionsText = document.getElementById('partitions').value;
const partitions = partitionsText
.split(/[\n,]/)
.map(p => p.trim())
.filter(p => p.length > 0)
.map(ref => ({ ref }));
if (partitions.length === 0) {
document.getElementById('error-message').textContent = 'Please enter at least one partition reference';
document.getElementById('error-message').style.display = 'block';
return;
}
const dataTimestamp = new Date(document.getElementById('data_timestamp').value).getTime();
const ttlSeconds = parseInt(document.getElementById('ttl_seconds').value) || 3600;
const slaSeconds = parseInt(document.getElementById('sla_seconds').value) || 300;
const comment = document.getElementById('comment').value || undefined;
const payload = {
partitions,
data_timestamp: dataTimestamp,
ttl_seconds: ttlSeconds,
sla_seconds: slaSeconds,
source: { manually_triggered: { user: "web_ui" } },
...(comment && { comment })
};
try {
const resp = await fetch('/api/wants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (resp.ok) {
const data = await resp.json();
window.location.href = '/wants/' + data.data.want_id;
} else {
const err = await resp.json();
document.getElementById('error-message').textContent = err.error || 'Failed to create want';
document.getElementById('error-message').style.display = 'block';
}
} catch (err) {
document.getElementById('error-message').textContent = 'Network error: ' + err.message;
document.getElementById('error-message').style.display = 'block';
}
});
</script>
{% call base::footer() %}

View file

@ -46,7 +46,7 @@
<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>
<li><a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a></li>
{% endfor %}
{% if want.partitions.is_empty() %}
<li style="color:var(--color-text-muted)">No partitions</li>
@ -59,7 +59,7 @@
<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>
<li><a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a></li>
{% endfor %}
</ul>
</div>

View file

@ -3,7 +3,10 @@
{% call base::head("Wants - DataBuild") %}
{% call base::nav("wants", base.graph_label) %}
<h1>Wants</h1>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h1 style="margin-bottom:0">Wants</h1>
<a href="/wants/create" class="btn-primary">Create Want</a>
</div>
<table>
<thead>
@ -24,7 +27,12 @@
{% when None %}<span class="status">Unknown</span>
{% endmatch %}
</td>
<td>{{ want.partitions.len() }}</td>
<td>
{% for p in want.partitions %}
<a href="/partitions/{{ p.partition_ref_encoded }}" class="partition-ref">{{ p.partition_ref }}</a>{% if !loop.last %} {% endif %}
{% endfor %}
{% if want.partitions.is_empty() %}<span style="color:var(--color-text-muted)">-</span>{% endif %}
</td>
<td>{{ want.comment_display }}</td>
</tr>
{% endfor %}