Files
pikasTech-unidesk/scripts/assets/web-probe-sentinel-monitor-workbench/monitor-workbench.js
T
2026-07-13 04:02:18 +00:00

33 lines
11 KiB
JavaScript
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.
import { createApp, computed, onMounted, ref } from "../web-probe-sentinel-monitor-web/vendor/vue.esm-browser.prod.js";
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 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) { 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, 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><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>
<footer class="status-bar"><span>central Monitor API</span><span>{{workspace?.freshness?.reason||workspace?.freshness?.updatedAt||'runtime metadata unavailable'}}</span></footer>
</div>` }).mount("#monitor-workbench-root");
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: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}) : "—"; }