Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/desktop-alert-log-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sfmc-bds/desktop": minor
---

点击告警后打开日志控制台,并定位到对应行高亮。
5 changes: 4 additions & 1 deletion packages/desktop/src/renderer/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ function Workspace() {

/** 外壳:三栏布局、快捷键与全局对话框 */
function Shell() {
const { selected, model, page, setPage, appInfo } = useDesktop();
const { selected, model, page, setPage, appInfo, logFocus } = useDesktop();
const [layout, setLayout] = useState(readLayout);
const narrow = useMediaQuery(NARROW_QUERY);
/** 窄窗口下右侧栏以浮层打开(不持久化) */
Expand All @@ -163,6 +163,9 @@ function Shell() {
}
}, [model.handshake, page, setPage]);
useEffect(() => setRailOverlay(false), [narrow]);
useEffect(() => {
if (logFocus) setRailOverlay(false);
}, [logFocus]);

const railOpen = narrow ? railOverlay : layout.rail;
const toggleSidebar = useCallback(() => setLayout((value) => ({ ...value, sidebar: !value.sidebar })), []);
Expand Down
22 changes: 20 additions & 2 deletions packages/desktop/src/renderer/app/desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ interface DesktopValue {
model: Model;
page: PageKey;
setPage: (page: PageKey) => void;
/** 打开日志控制台并定位到这一条 */
logFocus: { entry: LogEntryWire; nonce: number } | null;
openLog: (entry: LogEntryWire) => void;
clearLogFocus: () => void;
busy: boolean;
/** 包裹异步操作:显示全局进度、捕获错误并提示 */
guarded: <T>(action: () => Promise<T>) => Promise<T | undefined>;
Expand Down Expand Up @@ -148,7 +152,17 @@ export function DesktopProvider({ children }: { children: ReactNode }) {
const [profiles, setProfiles] = useState<InstanceProfile[]>([]);
const [current, setCurrentState] = useState("");
const [models, setModels] = useState<Record<string, Model>>({});
const [page, setPage] = useState<PageKey>("overview");
const [page, setPageState] = useState<PageKey>("overview");
const [logFocus, setLogFocus] = useState<{ entry: LogEntryWire; nonce: number } | null>(null);
const setPage = useCallback((next: PageKey) => {
if (next !== "logs") setLogFocus(null);
setPageState(next);
}, []);
const openLog = useCallback((entry: LogEntryWire) => {
setLogFocus({ entry, nonce: Date.now() });
setPageState("logs");
}, []);
const clearLogFocus = useCallback(() => setLogFocus(null), []);
const [busyCount, setBusyCount] = useState(0);
const [flow, setFlow] = useState<Flow | null>(null);
const [profileDialog, setProfileDialog] = useState<ProfileDialogState>({ open: false, editing: null });
Expand All @@ -167,6 +181,7 @@ export function DesktopProvider({ children }: { children: ReactNode }) {
const model = models[current] ?? blank();
const setCurrent = useCallback((id: string) => {
setCurrentState(id);
setLogFocus(null);
if (id) localStorage.setItem(LAST_INSTANCE_KEY, id);
}, []);

Expand Down Expand Up @@ -526,6 +541,9 @@ export function DesktopProvider({ children }: { children: ReactNode }) {
model,
page,
setPage,
logFocus,
openLog,
clearLogFocus,
busy: busyCount > 0,
guarded,
patch,
Expand All @@ -549,7 +567,7 @@ export function DesktopProvider({ children }: { children: ReactNode }) {
appInfo,
desktopUpdate,
}),
[ready, profiles, current, setCurrent, selected, models, model, page, busyCount, guarded, patch, refresh, request, submit, editable, connect, disconnect, startAttach, createDeployment, applyAttachment, confirmHost, flow, profileDialog, saveProfile, removeProfile, appInfo, desktopUpdate]
[ready, profiles, current, setCurrent, selected, models, model, page, setPage, logFocus, openLog, clearLogFocus, busyCount, guarded, patch, refresh, request, submit, editable, connect, disconnect, startAttach, createDeployment, applyAttachment, confirmHost, flow, profileDialog, saveProfile, removeProfile, appInfo, desktopUpdate]
);
return <DesktopContext.Provider value={value}>{children}</DesktopContext.Provider>;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop/src/renderer/components/RightRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function RailSection({ title, action, onAction, children }: { title: string; act

/** 右侧动态栏 */
export function RightRail() {
const { model, selected, setPage, page } = useDesktop();
const { model, selected, setPage, page, openLog } = useDesktop();
const [now, setNow] = useState(Date.now());
useEffect(() => {
const timer = setInterval(() => setNow(Date.now()), 30_000);
Expand All @@ -42,7 +42,7 @@ export function RightRail() {
) : (
<>
<RailSection title="告警" action="控制台" onAction={() => setPage("logs")}>
<AlertFeed logs={model.logs} now={now} limit={4} onSelect={() => setPage("logs")} />
<AlertFeed logs={model.logs} now={now} limit={4} onSelect={openLog} />
</RailSection>
<RailSection title="任务动态" action="全部" onAction={() => setPage("tasks")}>
<TaskFeed tasks={model.tasks} now={now} limit={6} onSelect={() => setPage("tasks")} />
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/src/renderer/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function ConnectionDetails() {

/** 告警按钮:角标为"上次打开后新增"的警告/错误数,打开弹层即视为已读(按实例分别记录) */
function AlertsButton() {
const { model, current, setPage } = useDesktop();
const { model, current, openLog } = useDesktop();
const [seen, setSeen] = useState<Record<string, number>>({});
const [open, setOpen] = useState(false);
const seenAt = seen[current] ?? 0;
Expand Down Expand Up @@ -143,9 +143,9 @@ function AlertsButton() {
logs={model.logs}
now={Date.now()}
limit={8}
onSelect={() => {
onSelect={(log) => {
setOpen(false);
setPage("logs");
openLog(log);
}}
/>
</Popover>
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop/src/renderer/components/feeds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ export function serviceState(row: ServiceStatusRow): { tone: Tone; label: string
}

/** 告警列表:错误用红色色块、警告用琥珀色块,正文取日志首行 */
export function AlertFeed({ logs, now, limit = 6, onSelect, emptyText = "已加载日志中没有警告或错误" }: { logs: LogEntryWire[]; now: number; limit?: number; onSelect?: () => void; emptyText?: string }) {
export function AlertFeed({ logs, now, limit = 6, onSelect, emptyText = "已加载日志中没有警告或错误" }: { logs: LogEntryWire[]; now: number; limit?: number; onSelect?: (log: LogEntryWire) => void; emptyText?: string }) {
const alerts = groupAlerts(logs, limit);
if (!alerts.length) return <EmptyState compact icon="bell" title="暂无告警" description={emptyText} />;
return (
<ul className="feed">
{alerts.map(({ log, count, firstTime }, index) => (
<li key={`${log.time}-${index}`}>
<button type="button" className="feed-item" onClick={onSelect} disabled={!onSelect}>
<button type="button" className="feed-item" onClick={() => onSelect?.(log)} disabled={!onSelect}>
<IconTile icon={log.level === "error" ? "bug" : "warning"} tone={log.level === "error" ? "danger" : "warning"} />
<span className="feed-text">
<span className="feed-title truncate" title={log.text}>{log.text.split("\n")[0]}</span>
Expand Down
11 changes: 11 additions & 0 deletions packages/desktop/src/renderer/lib/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ export function groupAlerts(logs: LogEntryWire[], limit = 6): AlertGroup[] {
}
return [...groups.values()];
}

/** 定位告警对应的日志。优先用原对象,日志被替换后按时间、来源、级别和正文匹配最近一条。 */
export function findLogIndex(logs: LogEntryWire[], target: LogEntryWire): number {
const exact = logs.lastIndexOf(target);
if (exact >= 0) return exact;
for (let index = logs.length - 1; index >= 0; index--) {
const log = logs[index]!;
if (log.time === target.time && log.source === target.source && log.level === target.level && log.text === target.text) return index;
}
return -1;
}
53 changes: 42 additions & 11 deletions packages/desktop/src/renderer/pages/ConsolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Button, IconButton, SearchInput, Select } from "../components/controls.
import { toast } from "../components/feedback.js";
import { Icon } from "../components/icons.js";
import { Chip, Kbd } from "../components/ui.js";
import { findLogIndex } from "../lib/alerts.js";
import { hashHue, type Tone } from "../lib/format.js";

/** 单次渲染的最大日志行数 */
Expand Down Expand Up @@ -59,7 +60,7 @@ function toggle<T>(set: Set<T>, value: T): Set<T> {
}

export function ConsolePage() {
const { model, editable, request, guarded } = useDesktop();
const { model, editable, request, guarded, logFocus, clearLogFocus } = useDesktop();
const logs = model.logs;
const [query, setQuery] = useState("");
const [sources, setSources] = useState<Set<string>>(new Set());
Expand All @@ -74,6 +75,18 @@ export function ConsolePage() {
const [historyIndex, setHistoryIndex] = useState(-1);
const viewport = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
const focusRow = useRef<HTMLDivElement>(null);
const missingFocus = useRef(0);
const focusNonce = logFocus?.nonce ?? 0;
const [seenFocus, setSeenFocus] = useState(0);
if (logFocus && focusNonce !== seenFocus) {
setSeenFocus(focusNonce);
setQuery("");
setSources(new Set());
setLevels(new Set());
setClearedAt("");
setFollow(false);
}

const visibleLogs = useMemo(() => (clearedAt ? logs.filter((log) => log.time > clearedAt) : logs), [logs, clearedAt]);
const sourceCounts = useMemo(() => {
Expand All @@ -95,11 +108,25 @@ export function ConsolePage() {
(!needle || log.text.toLowerCase().includes(needle))
);
}, [visibleLogs, sources, levels, query]);
const rendered = filtered.slice(-RENDER_LIMIT);
const focusIndex = logFocus ? findLogIndex(filtered, logFocus.entry) : -1;
const renderStart = focusIndex >= 0
? Math.max(0, Math.min(Math.max(0, focusIndex - 80), Math.max(0, filtered.length - RENDER_LIMIT)))
: Math.max(0, filtered.length - RENDER_LIMIT);
const rendered = filtered.slice(renderStart, renderStart + RENDER_LIMIT);

useLayoutEffect(() => {
if (follow && viewport.current) viewport.current.scrollTop = viewport.current.scrollHeight;
}, [rendered.length, follow, logs]);
if (logFocus || !follow || !viewport.current) return;
viewport.current.scrollTop = viewport.current.scrollHeight;
}, [rendered.length, follow, logs, logFocus]);
useLayoutEffect(() => {
focusRow.current?.scrollIntoView({ block: "center", inline: "nearest" });
}, [logFocus, focusIndex]);
useEffect(() => {
if (!logFocus || missingFocus.current === logFocus.nonce) return;
if (findLogIndex(logs, logFocus.entry) >= 0) return;
missingFocus.current = logFocus.nonce;
toast.warning("这条日志已不在当前记录中");
}, [logFocus, logs]);

useEffect(() => {
const listener = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -180,13 +207,16 @@ export function ConsolePage() {
aria-live="off"
>
{rendered.length ? (
rendered.map((log, index) => (
<div key={`${log.time}-${index}`} className={`log-row level-${log.level}`}>
<span className="log-time">{clock(log.time)}</span>
<span className="log-src" style={{ color: `hsl(${hashHue(log.source)} 58% var(--log-src-l))` }}>{log.source}</span>
<span className="log-text">{highlight(log.text, query.trim())}</span>
</div>
))
rendered.map((log, index) => {
const focused = renderStart + index === focusIndex;
return (
<div key={`${log.time}-${renderStart + index}`} ref={focused ? focusRow : undefined} className={`log-row level-${log.level}${focused ? " log-focus" : ""}`}>
<span className="log-time">{clock(log.time)}</span>
<span className="log-src" style={{ color: `hsl(${hashHue(log.source)} 58% var(--log-src-l))` }}>{log.source}</span>
<span className="log-text">{highlight(log.text, query.trim())}</span>
</div>
);
})
) : (
<div className="console-empty">
<Icon name="terminal" size={20} />
Expand All @@ -200,6 +230,7 @@ export function ConsolePage() {
className="console-jump"
onClick={() => {
setFollow(true);
clearLogFocus();
if (viewport.current) viewport.current.scrollTop = viewport.current.scrollHeight;
}}
>
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop/src/renderer/pages/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function ServiceRow({ row }: { row: ServiceStatusRow }) {
}

export function OverviewPage() {
const { model, editable, submit, setPage, request } = useDesktop();
const { model, editable, submit, setPage, request, openLog } = useDesktop();
const [startupOpen, setStartupOpen] = useState(false);
const [now, setNow] = useState(Date.now());
const metrics = useRuntimeMetrics();
Expand Down Expand Up @@ -277,7 +277,7 @@ export function OverviewPage() {
</button>
}
>
<AlertFeed logs={model.logs} now={now} limit={5} onSelect={() => setPage("logs")} />
<AlertFeed logs={model.logs} now={now} limit={5} onSelect={openLog} />
</Surface>
<Surface
title="最近任务"
Expand Down
11 changes: 11 additions & 0 deletions packages/desktop/src/renderer/styles/pages.css
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@
contain-intrinsic-size: auto 22px;
}

.log-row.log-focus {
background: var(--c-warning-soft);
box-shadow: inset 3px 0 0 var(--c-warning);
content-visibility: visible;
}

.log-row.level-error.log-focus {
background: var(--c-danger-soft);
box-shadow: inset 3px 0 0 var(--c-danger);
}

.console-viewport:not(.wrap) .log-text {
white-space: pre;
}
Expand Down
Loading