Align runs and inbox native surfaces
This commit is contained in:
1 parent
b3d5bcdde5
commit
e1b7f52444
2 files changed
+634
-153
No files matched your search
@@ -2,61 +2,263 @@ use super::*;
|
||||
|
||||
impl AppRoot {
|
||||
pub(super) fn render_inbox(&self) -> impl IntoElement + '_ {
|
||||
let pending_approvals = self
|
||||
.snapshot
|
||||
.approvals
|
||||
.iter()
|
||||
.filter(|item| item.state == "pending")
|
||||
.collect::<Vec<_>>();
|
||||
let unread_messages = self
|
||||
.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| item.read_at.is_none())
|
||||
.collect::<Vec<_>>();
|
||||
let review_tasks = self
|
||||
.snapshot
|
||||
.tasks
|
||||
.iter()
|
||||
.filter(|task| task.status == "in_review")
|
||||
.collect::<Vec<_>>();
|
||||
let failed_runs = self
|
||||
.snapshot
|
||||
.runs
|
||||
.iter()
|
||||
.filter(|run| {
|
||||
matches!(
|
||||
run.status.as_str(),
|
||||
"failed" | "timed_out" | "cancelled" | "interrupted"
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let total_attention = pending_approvals.len()
|
||||
+ unread_messages.len()
|
||||
+ review_tasks.len()
|
||||
+ failed_runs.len();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.flex_col()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(div().text_lg().child("Unread messages"))
|
||||
.children(
|
||||
self.snapshot
|
||||
.agent_messages
|
||||
.iter()
|
||||
.filter(|item| item.read_at.is_none())
|
||||
.take(8)
|
||||
.map(|item| {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
div()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.child(format!("{} · {}", item.subject, item.body))
|
||||
}),
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child("Inbox"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(6.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(format!("{} items need attention", total_attention)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(div().text_lg().child("Pending approvals"))
|
||||
.children(
|
||||
self.snapshot
|
||||
.approvals
|
||||
.iter()
|
||||
.filter(|item| item.state == "pending")
|
||||
.take(8)
|
||||
.map(|item| {
|
||||
div()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.child(format!(
|
||||
"{} · {}",
|
||||
item.approval_type, item.payload_summary
|
||||
))
|
||||
.child(inbox_filters(
|
||||
pending_approvals.len(),
|
||||
review_tasks.len(),
|
||||
failed_runs.len(),
|
||||
unread_messages.len(),
|
||||
))
|
||||
.when(total_attention == 0, |container| {
|
||||
container.child(
|
||||
div()
|
||||
.min_h(px(260.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.w(px(420.0))
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child("All clear"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.text_base()
|
||||
.text_color(theme::text_muted())
|
||||
.child("No approvals, reviews, failed runs, or unread internal messages need board attention right now."),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(total_attention > 0, |container| {
|
||||
container
|
||||
.when(!pending_approvals.is_empty(), |content| {
|
||||
content.child(inbox_group(
|
||||
"Pending approvals",
|
||||
pending_approvals.into_iter().map(|item| {
|
||||
inbox_row(
|
||||
item.payload_summary.clone(),
|
||||
format!("{} · pending", item.approval_type.replace('_', " ")),
|
||||
theme::accent_orange_soft(),
|
||||
theme::accent_orange(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
))
|
||||
})
|
||||
.when(!review_tasks.is_empty(), |content| {
|
||||
content.child(inbox_group(
|
||||
"Reviews in progress",
|
||||
review_tasks.into_iter().map(|item| {
|
||||
inbox_row(
|
||||
item.title.clone(),
|
||||
format!("{} · awaiting review", item.status),
|
||||
theme::accent_blue_soft(),
|
||||
theme::accent_blue(),
|
||||
)
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(!failed_runs.is_empty(), |content| {
|
||||
content.child(inbox_group(
|
||||
"Failed runs",
|
||||
failed_runs.into_iter().map(|item| {
|
||||
inbox_row(
|
||||
inbox_run_title(&self.snapshot, item),
|
||||
item.error_message.clone().unwrap_or_else(|| item.status.clone()),
|
||||
theme::danger_bg(),
|
||||
theme::danger(),
|
||||
)
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(!unread_messages.is_empty(), |content| {
|
||||
content.child(inbox_group(
|
||||
"Unread messages",
|
||||
unread_messages.into_iter().map(|item| {
|
||||
inbox_row(
|
||||
item.subject.clone(),
|
||||
item.body.clone(),
|
||||
theme::accent_teal_soft(),
|
||||
theme::accent_teal(),
|
||||
)
|
||||
}),
|
||||
))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn inbox_run_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
|
||||
run.task_id
|
||||
.as_ref()
|
||||
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
|
||||
.map(|task| task.title.clone())
|
||||
.unwrap_or_else(|| {
|
||||
if run.summary.trim().is_empty() {
|
||||
format!("Run {}", run.id.get(..8).unwrap_or(run.id.as_str()))
|
||||
} else {
|
||||
run.summary.clone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn inbox_filters(
|
||||
approvals: usize,
|
||||
reviews: usize,
|
||||
failed_runs: usize,
|
||||
messages: usize,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(filter_chip(
|
||||
"All",
|
||||
approvals + reviews + failed_runs + messages,
|
||||
true,
|
||||
))
|
||||
.child(filter_chip("Approvals", approvals, false))
|
||||
.child(filter_chip("Reviews", reviews, false))
|
||||
.child(filter_chip("Failed", failed_runs, false))
|
||||
.child(filter_chip("Messages", messages, false))
|
||||
}
|
||||
|
||||
fn filter_chip(label: &'static str, count: usize, active: bool) -> impl IntoElement {
|
||||
div()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.rounded_full()
|
||||
.bg(if active {
|
||||
theme::primary_action()
|
||||
} else {
|
||||
theme::panel_muted()
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(if active {
|
||||
theme::primary_action_text()
|
||||
} else {
|
||||
theme::text_secondary()
|
||||
})
|
||||
.child(format!("{} {}", label, count))
|
||||
}
|
||||
|
||||
fn inbox_group(label: &'static str, rows: impl Iterator<Item = gpui::Div>) -> impl IntoElement {
|
||||
div()
|
||||
.gap_3()
|
||||
.child(div().text_xs().text_color(theme::text_muted()).child(label))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.children(rows),
|
||||
)
|
||||
}
|
||||
|
||||
fn inbox_row(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
tile_bg: gpui::Hsla,
|
||||
accent: gpui::Hsla,
|
||||
) -> gpui::Div {
|
||||
div()
|
||||
.px_5()
|
||||
.py_4()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_border())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.items_start()
|
||||
.child(div().w(px(14.0)).h(px(14.0)).rounded_full().bg(accent))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.text_base()
|
||||
.text_color(theme::text_primary())
|
||||
.child(title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(6.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.line_clamp(2)
|
||||
.child(subtitle),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(20.0)).h(px(20.0)).rounded_full().bg(tile_bg)),
|
||||
)
|
||||
}
|
||||
@@ -7,150 +7,429 @@ impl AppRoot {
|
||||
.selected_run_id
|
||||
.as_ref()
|
||||
.and_then(|id| self.snapshot.runs.iter().find(|item| &item.id == id));
|
||||
let selected_status = selected
|
||||
.map(|item| item.status.clone())
|
||||
.unwrap_or_else(|| "Select a run".to_string());
|
||||
let selected_summary = selected
|
||||
.map(|item| item.summary.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "No run selected.".to_string());
|
||||
let selected_model = selected
|
||||
.and_then(|item| item.model.clone())
|
||||
.unwrap_or_else(|| "model pending".to_string());
|
||||
let selected_agent = selected
|
||||
.and_then(|run| {
|
||||
self.snapshot
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.id == run.agent_id)
|
||||
.map(|agent| agent.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "Agent unavailable".to_string());
|
||||
let selected_workspace = selected
|
||||
.and_then(|run| {
|
||||
run.workspace_id.as_ref().and_then(|workspace_id| {
|
||||
self.snapshot
|
||||
.workspaces
|
||||
.iter()
|
||||
.find(|workspace| &workspace.id == workspace_id)
|
||||
.map(|workspace| workspace.name.clone())
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| "Workspace pending".to_string());
|
||||
let selected_error = selected
|
||||
.and_then(|item| item.error_message.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.gap_4()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.id("runs-list")
|
||||
.w(px(360.0))
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.w(px(420.0))
|
||||
.p_5()
|
||||
.gap_4()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_lg()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.overflow_scroll()
|
||||
.child(div().text_lg().child("Run queue"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child("Run queue"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(6.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(format!("{} runs", self.snapshot.runs.len())),
|
||||
),
|
||||
)
|
||||
.child(run_filter_shell()),
|
||||
)
|
||||
.child(run_search_shell("Search runs"))
|
||||
.children(self.snapshot.runs.iter().map(|item| {
|
||||
let active =
|
||||
self.state.selected_run_id.as_deref() == Some(item.id.as_str());
|
||||
let run_id = item.id.clone();
|
||||
let headline = if item.summary.trim().is_empty() {
|
||||
format!("{} · {}", item.status, item.id)
|
||||
let title = run_task_title(&self.snapshot, item);
|
||||
let subtitle = run_subtitle(&self.snapshot, item);
|
||||
let summary = if item.summary.trim().is_empty() {
|
||||
"No summary yet".to_string()
|
||||
} else {
|
||||
format!("{} · {}", item.status, item.summary)
|
||||
item.summary.clone()
|
||||
};
|
||||
div()
|
||||
.id(SharedString::from(format!("run-{}", item.id)))
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(if active {
|
||||
theme::selection_border()
|
||||
} else {
|
||||
theme::panel_border()
|
||||
})
|
||||
.bg(if active {
|
||||
theme::selection_fill()
|
||||
} else {
|
||||
theme::panel()
|
||||
})
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(theme::panel_hover()))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.state.selected_run_id = Some(run_id.clone());
|
||||
cx.notify();
|
||||
}))
|
||||
.child(headline)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.text_base()
|
||||
.text_color(theme::text_primary())
|
||||
.truncate()
|
||||
.child(title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(6.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(subtitle),
|
||||
),
|
||||
)
|
||||
.child(run_status_badge(item.status.as_str())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_secondary())
|
||||
.line_clamp(2)
|
||||
.child(summary),
|
||||
)
|
||||
.when_some(item.updated_at.get(..10), |container, date| {
|
||||
container.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.text_xs()
|
||||
.text_color(theme::text_subtle())
|
||||
.child(date.to_string()),
|
||||
)
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(div().text_lg().child("Run inspector"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(selected_status),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(self.metric_card("Agent", selected_agent, "Assigned operator"))
|
||||
.child(self.metric_card(
|
||||
"Model",
|
||||
selected_model,
|
||||
"Runtime-selected model",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(self.metric_card(
|
||||
"Workspace",
|
||||
selected_workspace,
|
||||
"Bound workspace",
|
||||
))
|
||||
.gap_5()
|
||||
.when_some(selected, |container, run| {
|
||||
container
|
||||
.child(
|
||||
self.metric_card(
|
||||
"Connector",
|
||||
selected
|
||||
.map(|item| item.connector_id.clone())
|
||||
.unwrap_or_else(|| "connector pending".to_string()),
|
||||
"Execution provider",
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_h(px(180.0))
|
||||
.p_3()
|
||||
.border_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.rounded_md()
|
||||
.bg(theme::panel_inset())
|
||||
.child(selected_summary),
|
||||
)
|
||||
.when(!selected_error.is_empty(), |container| {
|
||||
div()
|
||||
.p_5()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child(run_task_title(&self.snapshot, run)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(8.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(run.status.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(16.0))
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(info_tile(
|
||||
"AGENT",
|
||||
run_agent_name(&self.snapshot, run),
|
||||
))
|
||||
.child(info_tile(
|
||||
"MODEL",
|
||||
run.model.clone().unwrap_or_else(|| "Pending".to_string()),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(12.0))
|
||||
.flex()
|
||||
.gap_3()
|
||||
.child(info_tile(
|
||||
"WORKSPACE",
|
||||
run_workspace_name(&self.snapshot, run),
|
||||
))
|
||||
.child(info_tile(
|
||||
"CONNECTOR",
|
||||
run.connector_id.clone(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(18.0))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_inset_border())
|
||||
.bg(theme::panel_inset())
|
||||
.child(if run.summary.trim().is_empty() {
|
||||
"No run summary yet.".to_string()
|
||||
} else {
|
||||
run.summary.clone()
|
||||
}),
|
||||
)
|
||||
.when_some(run.error_message.clone(), |content, error| {
|
||||
if error.trim().is_empty() {
|
||||
content
|
||||
} else {
|
||||
content.child(
|
||||
div()
|
||||
.mt(px(16.0))
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::danger())
|
||||
.bg(theme::danger_bg())
|
||||
.child(error),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p_5()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.text_lg()
|
||||
.text_color(theme::text_primary())
|
||||
.child("Run timeline"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme::text_muted())
|
||||
.child(short_date(run.updated_at.as_str()).to_string()),
|
||||
),
|
||||
)
|
||||
.child(run_timeline(&self.snapshot, run)),
|
||||
)
|
||||
})
|
||||
.when(selected.is_none(), |container| {
|
||||
container.child(
|
||||
div()
|
||||
.p_3()
|
||||
.min_h(px(320.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::primary_action())
|
||||
.rounded_md()
|
||||
.bg(theme::danger_bg())
|
||||
.child(selected_error),
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.child(
|
||||
div()
|
||||
.w(px(420.0))
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.text_color(theme::text_primary())
|
||||
.child("No run selected"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt(px(10.0))
|
||||
.text_base()
|
||||
.text_color(theme::text_muted())
|
||||
.child("Choose a run from the queue to inspect its status and execution context."),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_task_title(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
|
||||
run.task_id
|
||||
.as_ref()
|
||||
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
|
||||
.map(|task| task.title.clone())
|
||||
.unwrap_or_else(|| {
|
||||
if run.summary.trim().is_empty() {
|
||||
format!("Run {}", short_id(run.id.as_str()))
|
||||
} else {
|
||||
run.summary.clone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn run_subtitle(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
|
||||
format!(
|
||||
"{} via {}{}",
|
||||
run_agent_name(snapshot, run),
|
||||
run.connector_id,
|
||||
run.workspace_id
|
||||
.as_ref()
|
||||
.map(|_| format!(" · {}", run_workspace_name(snapshot, run)))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
|
||||
fn run_agent_name(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
|
||||
snapshot
|
||||
.agents
|
||||
.iter()
|
||||
.find(|agent| agent.id == run.agent_id)
|
||||
.map(|agent| agent.name.clone())
|
||||
.unwrap_or_else(|| run.agent_id.clone())
|
||||
}
|
||||
|
||||
fn run_workspace_name(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> String {
|
||||
run.workspace_id
|
||||
.as_ref()
|
||||
.and_then(|workspace_id| {
|
||||
snapshot
|
||||
.workspaces
|
||||
.iter()
|
||||
.find(|workspace| &workspace.id == workspace_id)
|
||||
})
|
||||
.map(|workspace| workspace.name.clone())
|
||||
.unwrap_or_else(|| "Workspace pending".to_string())
|
||||
}
|
||||
|
||||
fn run_status_badge(status: &str) -> impl IntoElement {
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded_full()
|
||||
.bg(match status {
|
||||
"running" => theme::accent_teal_soft(),
|
||||
"failed" | "timed_out" | "cancelled" | "interrupted" => theme::danger_bg(),
|
||||
_ => theme::panel_muted(),
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(match status {
|
||||
"running" => theme::accent_teal(),
|
||||
"failed" | "timed_out" | "cancelled" | "interrupted" => theme::danger(),
|
||||
_ => theme::text_secondary(),
|
||||
})
|
||||
.child(status.to_uppercase())
|
||||
}
|
||||
|
||||
fn info_tile(label: &'static str, value: String) -> impl IntoElement {
|
||||
div()
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.rounded_lg()
|
||||
.bg(theme::panel_muted())
|
||||
.child(div().text_xs().text_color(theme::text_muted()).child(label))
|
||||
.child(
|
||||
div()
|
||||
.mt(px(8.0))
|
||||
.text_sm()
|
||||
.text_color(theme::text_primary())
|
||||
.child(value),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_timeline(snapshot: &domain::ProfileSnapshot, run: &domain::Run) -> impl IntoElement {
|
||||
div()
|
||||
.mt(px(16.0))
|
||||
.gap_3()
|
||||
.child(timeline_row(
|
||||
"Created",
|
||||
short_date(run.created_at.as_str()).to_string(),
|
||||
))
|
||||
.child(timeline_row(
|
||||
"Started",
|
||||
run.started_at
|
||||
.as_deref()
|
||||
.map(short_date)
|
||||
.unwrap_or("Pending")
|
||||
.to_string(),
|
||||
))
|
||||
.child(timeline_row(
|
||||
"Task",
|
||||
run.task_id
|
||||
.as_ref()
|
||||
.and_then(|task_id| snapshot.tasks.iter().find(|task| &task.id == task_id))
|
||||
.map(|task| task.status.clone())
|
||||
.unwrap_or_else(|| "Unlinked".to_string()),
|
||||
))
|
||||
.child(timeline_row(
|
||||
"Updated",
|
||||
short_date(run.updated_at.as_str()).to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn timeline_row(label: &'static str, value: String) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(theme::panel_border())
|
||||
.child(div().text_sm().text_color(theme::text_muted()).child(label))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(theme::text_primary())
|
||||
.child(value),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_filter_shell() -> impl IntoElement {
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.text_sm()
|
||||
.text_color(theme::text_secondary())
|
||||
.child("All")
|
||||
}
|
||||
|
||||
fn run_search_shell(label: &'static str) -> impl IntoElement {
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(theme::panel_border())
|
||||
.bg(theme::panel())
|
||||
.text_sm()
|
||||
.text_color(theme::text_subtle())
|
||||
.child(label)
|
||||
}
|
||||
|
||||
fn short_id(value: &str) -> &str {
|
||||
value.get(..8).unwrap_or(value)
|
||||
}
|
||||
|
||||
fn short_date(value: &str) -> &str {
|
||||
value.get(..10).unwrap_or(value)
|
||||
}
|
||||
Reference in new issue
Block a user