Compare commits
2 commits
5c720ebc62
...
ce8bb92cdb
| Author | SHA1 | Date | |
|---|---|---|---|
| ce8bb92cdb | |||
| d744d2a63f |
10 changed files with 216 additions and 14 deletions
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
104
databuild/web/templates/wants/create.html
Normal file
104
databuild/web/templates/wants/create.html
Normal 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() %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue