Files
endorphin-tessera-gui/src/shell/developer_tools/network.rs
T

144 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 绘制已观察到的 HTTP 活动、请求列表与详情。
//! 筛选、选择和滚动是工具视图状态;展示一条请求不会重发该请求。
//!
use super::*;
/// 将本进程 HTTP 活动投影为可筛选请求列表和独立滚动详情,并生成选择/分隔条命中区域。
// developer_tools::build 选择 Network 时调用,共享日志已在父入口克隆,过滤和文字测量不持活动日志锁。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
records: &[Exchange],
viewport: Rect,
) {
// 工具页以 viewport 横向原点和宽度布局;底部状态栏高度留给父入口统一绘制。
let x = viewport.x;
let w = viewport.width;
let bottom = viewport.height - 22.0;
p.fill(Rect::new(x, 32.0, w, 30.0), BAR);
// 水位后存在记录才允许 Clear,Action::ClearNetwork 更新工具水位并清除选择,不删除 Console 共用日志。
p.enabled_button(
Rect::new(x + 3.0, 32.0, 62.0, 29.0),
"Clear",
Action::ClearNetwork,
records.iter().any(|r| r.id > state.network_after),
);
// 过滤框注册 Filter 焦点,按键编辑在 DeveloperToolsState 中处理;此处只展示当前文本与焦点。
let filter_area = Rect::new(x + 72.0, 35.0, (w - 85.0).max(100.0), 24.0);
p.ui.text_field(
filter_area,
Action::Focus(Focus::Filter),
state.focus == Some(Focus::Filter),
);
p.label(
filter_area.x + 7.0,
filter_area.y + 3.0,
&format!(
"Filter: {}{}",
state.filter,
// 竖线是焦点提示字符,不是 Console 那种按字节光标定位,也不修改过滤字符串本身。
if state.focus == Some(Focus::Filter) {
"│"
} else {
""
}
),
MUTED,
false,
);
p.label(
x + 10.0,
69.0,
"METHOD STATUS TIME PATH",
MUTED,
true,
);
// 上下区分隔位置取自工具状态比例,拖动只改变显示分配,不改变记录本身。
let divider = 92.0 + (bottom - 92.0) * state.network_fraction;
// 对方法、路径和状态的组合字符串做大小写不敏感子串匹配;未响应记录可用 pending 搜索。
let filter = state.filter.to_lowercase();
// 保留原记录顺序及引用,只建立本次展示候选,不复制/更改每条 Exchange 的响应状态。
let visible = records
.iter()
// 清屏按 ID 水位隐藏旧记录,不删除共享 ActivityLog,Console 仍可独立展示同一条请求。
.filter(|r| r.id > state.network_after)
// 方法、路径与状态合并后统一小写匹配,origin 和正文不在当前过滤范围内。
.filter(|r| {
format!(
"{} {} {}",
r.method,
r.path,
r.status.map_or("pending".into(), |s| s.to_string())
)
.to_lowercase()
.contains(&filter)
})
.collect::<Vec<_>>();
let mut lines = Vec::new();
// 逐匹配记录生成一行,pending 状态用省略号;耗时直接取日志快照,不在绘制时重新计时。
for r in &visible {
// 先创建带状态颜色的行,再附加稳定 Request 动作和选中标记,筛选改变行号也不会改变目标身份。
let mut l = Line::new(
format!(
"{:<7} {:<6} {:>5} ms {}",
r.method,
r.status.map_or("…".into(), |s| s.to_string()),
r.elapsed_ms,
r.path
),
if r.status.is_some_and(|s| s >= 400) {
ERROR
} else {
TEXT
},
);
// 请求 ID 是选择身份,不依赖筛选后行号;点击行仅生成 Request 动作,不重新发送 HTTP。
l.selected = state.selected_request == Some(r.id);
l.action = Some(Action::Request(r.id));
lines.push(l);
}
// 空列表可能来自无记录、清屏或过滤无匹配,统一展示本进程 API 的捕获范围提示。
if lines.is_empty() {
lines.push(Line::new(
"No matching requests. Listening to Tessera's local API.",
MUTED,
));
}
// 列表使用 Requests Region 保存独立滚动,false 表示不因为新增请求自动跟随到底部。
p.lines(
Rect::new(x + 1.0, 92.0, w - 2.0, divider - 92.0),
Region::Requests,
&lines,
state,
false,
);
// 只登记横向分隔条,父绘制入口最后覆盖绘制并注册 Split::Network 命中目标。
p.split(Rect::new(x + 1.0, divider, w - 2.0, 1.0), Split::Network);
// 详情从完整保留记录中查找,因此筛选隐藏某行后可继续查看已选详情;记录淘汰后才回提示。
let selected = records
.iter()
.find(|r| Some(r.id) == state.selected_request);
// 详情另建行列表,不依赖上面的 visible;筛选无匹配并不自动取消已选请求。
let mut details = Vec::new();
// 仍保留的已选记录交给 request_details;ID 已淘汰或从未选择则显示捕获范围说明。
if let Some(r) = selected {
request_details(&mut details, r);
} else {
multiline(
&mut details,
"Select a request to inspect headers and bodies.\n\nCapture scope: 127.0.0.1:4737 in this Tessera process.\n80 most recent requests; body previews up to 32 KiB.\nOnly Content-Type and Content-Length request headers are recorded.\nExternal browser / OS traffic is not captured.",
MUTED,
);
}
// 请求列表与详情使用不同 Region,各自维护滚动量,避免切换详情影响列表定位。
p.lines(
Rect::new(x + 1.0, divider + 5.0, w - 2.0, bottom - divider - 5.0),
Region::Details,
&details,
state,
false,
);
}