fix: close monitor workbench final review gaps
This commit is contained in:
@@ -15,7 +15,8 @@ button { color:inherit; }
|
||||
.health-dot[data-state="degraded"] { background:#d18a24; }
|
||||
.context-bar { position:relative; gap:8px; padding:3px 8px; overflow:hidden; }
|
||||
.desktop-scope { display:flex; gap:8px; }
|
||||
.compact-scope-button, .compact-scope-panel { display:none; }
|
||||
.desktop-time { display:flex; gap:8px; }
|
||||
.compact-scope-button, .compact-scope-panel, .compact-time-button, .compact-time-panel { display:none; }
|
||||
.context-bar label { display:flex; align-items:center; gap:4px; color:var(--muted); font-size:11px; }
|
||||
.context-bar input, .context-bar select { width:132px; height:28px; border:1px solid var(--line); background:#fff; padding:0 7px; }
|
||||
.result-count { margin-left:auto; white-space:nowrap; color:var(--muted); font-size:12px; }
|
||||
@@ -66,9 +67,11 @@ pre { max-width:100%; overflow:auto; border:1px solid var(--line); background:#f
|
||||
.workspace.left-collapsed, .workspace.left-collapsed.detail-open { grid-template-columns:36px minmax(0,1fr); }
|
||||
.detail-pane { position:absolute; z-index:5; top:0; right:0; bottom:0; width:min(420px, calc(100vw - 40px)); box-shadow:-10px 0 24px rgba(23,32,31,.14); }
|
||||
.desktop-scope { display:none; }
|
||||
.desktop-time { display:none; }
|
||||
.compact-scope-button { display:block; height:28px; border:1px solid var(--line); background:#fff; white-space:nowrap; }
|
||||
.compact-scope-panel { position:fixed; z-index:8; top:74px; left:145px; display:grid; width:260px; gap:8px; border:1px solid var(--line); background:#fff; box-shadow:0 8px 22px rgba(23,32,31,.16); padding:10px; }
|
||||
.context-bar>label:nth-of-type(3), .context-bar>label:nth-of-type(4) { display:none; }
|
||||
.compact-time-button { display:block; height:28px; border:1px solid var(--line); background:#fff; white-space:nowrap; }
|
||||
.compact-time-panel { position:fixed; z-index:8; top:74px; left:300px; display:grid; width:300px; gap:8px; border:1px solid var(--line); background:#fff; box-shadow:0 8px 22px rgba(23,32,31,.16); padding:10px; }
|
||||
.context-bar input, .context-bar select { width:118px; }
|
||||
.run-header { min-height:64px; padding:7px 10px; }
|
||||
.main-scroll { height:calc(100% - 64px); }
|
||||
|
||||
@@ -3,20 +3,20 @@ import { loadMonitorRun, loadMonitorWorkspace } from "./state/monitor-client.js"
|
||||
import { monitorRouteHref, readMonitorRoute, withMonitorRoute } from "./state/monitor-route.js";
|
||||
|
||||
createApp({ setup() {
|
||||
const route = ref(readMonitorRoute()); const workspace = ref(null); const detail = ref(null); const loading = ref(true); const error = ref(""); const leftCollapsed = ref(false); const scopeOpen = ref(false); const detailOpen = ref(route.value.kind === "detail");
|
||||
const route = ref(readMonitorRoute()); const workspace = ref(null); const detail = ref(null); const loading = ref(true); const error = ref(""); const leftCollapsed = ref(false); const scopeOpen = ref(false); const timeOpen = ref(false); const detailOpen = ref(route.value.kind === "detail");
|
||||
const runs = computed(() => workspace.value?.runs?.items || []); const activeRun = computed(() => detail.value?.run || runs.value.find((item) => item.runId === route.value.runId) || null); const selectedDetail = computed(() => findSelectedDetail(detail.value, route.value));
|
||||
const activeRows = computed(() => route.value.tab === "findings" ? (detail.value?.findings || []).map(findingRow) : route.value.tab === "diagnostics" ? Object.entries(detail.value?.diagnostics || {}).map(diagnosticRow) : (detail.value?.timeline || []).map(timelineRow));
|
||||
async function restore() { const next = readMonitorRoute(); route.value = next; if (next.kind === "invalid") { error.value = `无效深链:${next.reason}`; loading.value = false; return; } loading.value = true; error.value = ""; try { workspace.value = await loadMonitorWorkspace(next); if (!next.runId && workspace.value.selection.defaultRunId) { const canonical = withMonitorRoute(next, { kind:"run", runId:workspace.value.selection.defaultRunId }); history.replaceState(null, "", monitorRouteHref(canonical)); route.value = canonical; } detail.value = route.value.runId ? await loadMonitorRun(route.value) : null; detailOpen.value = route.value.kind === "detail"; } catch (cause) { error.value = cause.message || "monitor_request_failed"; } finally { loading.value = false; document.querySelector("#monitor-workbench-root")?.setAttribute("data-monitor-ready", error.value ? "false" : "true"); } }
|
||||
function navigate(patch) { const next = withMonitorRoute(route.value, patch); history.pushState(null, "", monitorRouteHref(next)); void restore(); }
|
||||
const selectRun = (run) => navigate({ kind:"run", runId:run.runId, detailType:"", detailId:"" }); const selectTab = (tab) => navigate({ tab, kind:"run", detailType:"", detailId:"" }); const closeDetail = () => navigate({ kind:"run", detailType:"", detailId:"" });
|
||||
function selectRow(row) { if (route.value.tab !== "diagnostics") navigate({ kind:"detail", detailType:route.value.tab === "findings" ? "finding" : "event", detailId:row.id }); }
|
||||
function selectRow(row) { navigate({ kind:"detail", detailType:route.value.tab === "findings" ? "finding" : route.value.tab === "diagnostics" ? "diagnostic" : "event", detailId:row.id }); }
|
||||
const selectArtifact = (row) => navigate({ kind:"detail", detailType:"artifact", detailId:row.id }); const updateFilter = (key, value) => navigate({ kind:"workspace", runId:"", detailType:"", detailId:"", filters:{ [key]:value } });
|
||||
onMounted(() => { window.addEventListener("popstate", restore); void restore(); });
|
||||
return { route, workspace, detail, loading, error, leftCollapsed, scopeOpen, detailOpen, runs, activeRun, activeRows, selectedDetail, restore, selectRun, selectTab, selectRow, selectArtifact, closeDetail, updateFilter, formatDate, semanticDetail };
|
||||
return { route, workspace, detail, loading, error, leftCollapsed, scopeOpen, timeOpen, detailOpen, runs, activeRun, activeRows, selectedDetail, restore, selectRun, selectTab, selectRow, selectArtifact, closeDetail, updateFilter, formatDate, semanticDetail };
|
||||
}, template:`
|
||||
<div class="monitor-workbench" data-monitor-workbench>
|
||||
<header class="app-bar"><strong>Monitor</strong><span class="health-dot" :data-state="workspace?.health?.state"></span><span>{{ workspace?.health?.state || 'loading' }}</span><span class="app-stat">Cadence {{ workspace?.cadence?.label || workspace?.cadence?.state || 'unavailable' }}</span><span class="app-stat">Fresh {{ workspace?.freshness?.ageSeconds != null ? workspace.freshness.ageSeconds + 's' : workspace?.freshness?.state || 'unavailable' }}</span><span class="app-stat">Last success {{ formatDate(workspace?.summary?.latestSuccessAt) }}</span><span class="app-stat">Last failure {{ formatDate(workspace?.summary?.latestFailureAt) }}</span><button class="icon-button" @click="restore">刷新</button></header>
|
||||
<nav class="context-bar"><label>Sentinel<input :value="route.filters?.sentinelId" @change="updateFilter('sentinelId',$event.target.value)"></label><div class="desktop-scope"><label>Node<input :value="route.filters?.node" @change="updateFilter('node',$event.target.value)"></label><label>Lane<input :value="route.filters?.lane" @change="updateFilter('lane',$event.target.value)"></label></div><button class="compact-scope-button" @click="scopeOpen=!scopeOpen">范围 {{ route.filters?.node || '—' }}/{{ route.filters?.lane || '—' }}</button><div v-if="scopeOpen" class="compact-scope-panel"><label>Node<input :value="route.filters?.node" @change="updateFilter('node',$event.target.value)"></label><label>Lane<input :value="route.filters?.lane" @change="updateFilter('lane',$event.target.value)"></label></div><label>Status<select :value="route.filters?.status" @change="updateFilter('status',$event.target.value)"><option value="">全部</option><option value="succeeded">成功</option><option value="failed">失败</option><option value="blocked">阻塞</option><option value="running">运行中</option><option value="unknown">未知</option></select></label><label>From<input type="datetime-local" :value="route.filters?.from" @change="updateFilter('from',$event.target.value)"></label><label>To<input type="datetime-local" :value="route.filters?.to" @change="updateFilter('to',$event.target.value)"></label><span class="result-count">{{ workspace?.summary?.totalRuns ?? 0 }} runs · {{ workspace?.summary?.findingCount ?? 0 }} findings</span></nav>
|
||||
<nav class="context-bar"><label>Sentinel<input :value="route.filters?.sentinelId" @change="updateFilter('sentinelId',$event.target.value)"></label><div class="desktop-scope"><label>Node<input :value="route.filters?.node" @change="updateFilter('node',$event.target.value)"></label><label>Lane<input :value="route.filters?.lane" @change="updateFilter('lane',$event.target.value)"></label></div><button class="compact-scope-button" @click="scopeOpen=!scopeOpen">范围 {{ route.filters?.node || '—' }}/{{ route.filters?.lane || '—' }}</button><div v-if="scopeOpen" class="compact-scope-panel"><label>Node<input :value="route.filters?.node" @change="updateFilter('node',$event.target.value)"></label><label>Lane<input :value="route.filters?.lane" @change="updateFilter('lane',$event.target.value)"></label></div><label>Status<select :value="route.filters?.status" @change="updateFilter('status',$event.target.value)"><option value="">全部</option><option value="succeeded">成功</option><option value="failed">失败</option><option value="blocked">阻塞</option><option value="running">运行中</option><option value="unknown">未知</option></select></label><div class="desktop-time"><label>From<input type="datetime-local" :value="route.filters?.from" @change="updateFilter('from',$event.target.value)"></label><label>To<input type="datetime-local" :value="route.filters?.to" @change="updateFilter('to',$event.target.value)"></label></div><button class="compact-time-button" @click="timeOpen=!timeOpen">时间 {{ route.filters?.from || route.filters?.to ? '已筛选' : '全部' }}</button><div v-if="timeOpen" class="compact-time-panel"><label>From<input type="datetime-local" :value="route.filters?.from" @change="updateFilter('from',$event.target.value)"></label><label>To<input type="datetime-local" :value="route.filters?.to" @change="updateFilter('to',$event.target.value)"></label></div><span class="result-count">{{ workspace?.summary?.totalRuns ?? 0 }} runs · {{ workspace?.summary?.findingCount ?? 0 }} findings</span></nav>
|
||||
<main class="workspace" :class="{'left-collapsed':leftCollapsed,'detail-open':detailOpen}"><aside class="run-pane" data-pane="runs"><button class="pane-toggle" @click="leftCollapsed=!leftCollapsed">{{leftCollapsed?'›':'‹'}}</button><div class="pane-title">Runs</div><div class="run-list"><button v-for="run in runs" :key="run.runId" class="run-row" :class="{active:run.runId===route.runId}" @click="selectRun(run)"><time>{{formatDate(run.updatedAt)}}</time><strong>{{run.runId}}</strong><span>{{run.status}}</span><small>{{run.sentinelId}} · {{run.node}}/{{run.lane}} · {{run.findingCount}} findings</small></button></div></aside>
|
||||
<section class="main-pane" data-pane="main"><div v-if="loading" class="state-layer">正在加载 central Monitor view…</div><div v-else-if="error" class="state-layer error"><strong>加载失败</strong><code>{{error}}</code><button @click="restore">重试</button></div><div v-else-if="!activeRun" class="state-layer">当前范围无 run</div><template v-else><header class="run-header"><div><strong>{{activeRun.runId}}</strong><span>{{activeRun.status}} · {{formatDate(activeRun.updatedAt)}}</span></div><nav><button v-for="tab in ['timeline','findings','diagnostics']" :class="{active:route.tab===tab}" @click="selectTab(tab)">{{tab}}</button></nav></header><div class="main-scroll"><button v-for="row in activeRows" :key="row.id" class="semantic-row" @click="selectRow(row)"><time>{{row.time}}</time><strong>{{row.type}}</strong><span class="severity">{{row.severity}}</span><span class="summary">{{row.summary}}</span></button><section v-if="detail?.artifacts?.length" class="artifact-section"><h2>Artifacts</h2><button v-for="artifact in detail.artifacts" :key="artifact.id" class="semantic-row" @click="selectArtifact(artifact)"><strong>{{artifact.kind||'artifact'}}</strong><span>{{artifact.relativePath||artifact.id}}</span><span>{{artifact.reachable?'reachable':'unreachable'}}</span></button></section></div></template></section>
|
||||
<aside v-if="detailOpen" class="detail-pane" data-pane="detail"><header><strong>Detail</strong><button @click="closeDetail">关闭</button></header><div class="detail-scroll"><div v-if="selectedDetail"><h2>{{semanticDetail(selectedDetail).title}}</h2><dl><template v-for="item in semanticDetail(selectedDetail).fields" :key="item[0]"><dt>{{item[0]}}</dt><dd>{{item[1]}}</dd></template></dl><details><summary>原始诊断数据</summary><pre>{{JSON.stringify(selectedDetail,null,2)}}</pre></details></div><div v-else class="state-layer">对象不存在或不在当前范围</div></div></aside></main>
|
||||
@@ -27,6 +27,6 @@ function record(value) { return value && typeof value === "object" ? value : {};
|
||||
function timelineRow(row) { const value=record(row.event); return { id:row.id, time:formatDate(value.at||value.updatedAt||value.timestamp), type:String(value.type||value.phase||"event"), severity:String(value.severity||value.level||"—"), summary:String(value.summary||value.message||value.phase||row.id) }; }
|
||||
function findingRow(row) { const value=record(row.finding); return { id:row.id, time:formatDate(value.updatedAt||value.at), type:String(value.code||value.type||"finding"), severity:String(value.severity||value.level||"unknown"), summary:String(value.summary||value.title||value.rootCause||row.id) }; }
|
||||
function diagnosticRow([id,value]) { const item=record(value); return { id, time:formatDate(item.updatedAt), type:id, severity:String(item.state||"—"), summary:String(item.summary||item.message||item.state||"available") }; }
|
||||
function findSelectedDetail(detail,route) { if(!detail||route.kind!=="detail") return null; const rows=route.detailType==="finding"?detail.findings:route.detailType==="event"?detail.timeline:detail.artifacts; return rows?.find((item)=>item.id===route.detailId)||null; }
|
||||
function semanticDetail(item) { const value=record(item.finding||item.event||item); return { title:String(value.title||value.summary||value.kind||item.id), fields:[["ID",item.id],["时间",formatDate(value.updatedAt||value.at||value.timestamp)],["类型",value.type||value.code||value.kind||"—"],["Severity",value.severity||value.level||"—"],["摘要",value.summary||value.message||value.rootCause||"—"],["路径",value.relativePath||item.relativePath||"—"],["可达",item.reachable==null?"—":item.reachable?"是":"否"]] }; }
|
||||
function findSelectedDetail(detail,route) { if(!detail||route.kind!=="detail") return null; const rows=route.detailType==="finding"?detail.findings:route.detailType==="event"?detail.timeline:route.detailType==="diagnostic"?Object.entries(detail.diagnostics||{}).map(([id,value])=>({id,value})):detail.artifacts; return rows?.find((item)=>item.id===route.detailId)||null; }
|
||||
function semanticDetail(item) { const value=record(item.finding||item.event||item.value||item); return { title:String(value.title||value.summary||value.kind||item.id), fields:[["ID",item.id],["时间",formatDate(value.updatedAt||value.at||value.timestamp)],["类型",value.type||value.code||value.kind||item.id||"—"],["Severity",value.severity||value.level||value.state||"—"],["摘要",value.summary||value.message||value.rootCause||value.state||"—"],["路径",value.relativePath||item.relativePath||"—"],["可达",item.reachable==null?"—":item.reachable?"是":"否"]] }; }
|
||||
function formatDate(value) { return value ? new Date(value).toLocaleString("zh-CN",{hour12:false}) : "—"; }
|
||||
|
||||
@@ -20,7 +20,7 @@ export function readMonitorRoute(input = window.location.href) {
|
||||
route.kind = "run";
|
||||
route.runId = decodeURIComponent(segments[2]);
|
||||
if (segments.length === 3) return route;
|
||||
const detailTypes = { findings: "finding", events: "event", artifacts: "artifact" };
|
||||
const detailTypes = { findings: "finding", events: "event", artifacts: "artifact", diagnostics: "diagnostic" };
|
||||
if (!detailTypes[segments[3]] || !segments[4] || segments.length !== 5) return invalid("unsupported_path");
|
||||
route.kind = "detail";
|
||||
route.detailType = detailTypes[segments[3]];
|
||||
@@ -43,4 +43,4 @@ export function withMonitorRoute(route, patch) { return { ...route, ...patch, fi
|
||||
function invalid(reason) { return { kind: "invalid", reason }; }
|
||||
function value(url, key) { return url.searchParams.get(key) || ""; }
|
||||
function validDate(value) { return !value || Number.isFinite(Date.parse(value)); }
|
||||
function detailPath(route) { const segments = { finding: "findings", event: "events", artifact: "artifacts" }; return route.detailType && route.detailId ? `/${segments[route.detailType]}/${encodeURIComponent(route.detailId)}` : ""; }
|
||||
function detailPath(route) { const segments = { finding: "findings", event: "events", artifact: "artifacts", diagnostic: "diagnostics" }; return route.detailType && route.detailId ? `/${segments[route.detailType]}/${encodeURIComponent(route.detailId)}` : ""; }
|
||||
|
||||
Reference in New Issue
Block a user