feat(workbench): expand SCADA monitoring and investigation presentation

This commit is contained in:
2026-11-07 19:50:14 +08:00
parent 0801e161ec
commit ba71e2d529
133 changed files with 6754 additions and 1438 deletions
@@ -41,6 +41,13 @@ export type PresentationHistoryControls = {
entries: PresentationHistoryEntry[];
selectedId: number | null;
onSelect: (id: number | null) => void;
playback?: {
playing: boolean;
speed: number;
onToggle: () => void;
onSpeedChange: (speed: number) => void;
onSeek: (id: number) => void;
};
};
export const emptyPresentationView = (): PresentationView => ({
charts: [],
@@ -58,6 +65,8 @@ export class PresentationViewHistory {
current = emptyPresentationView();
entries: PresentationHistoryEntry[] = [];
selectedId: number | null = null;
playing = false;
playbackIds: number[] = [];
private sequence = 0;
get currentEntryId() {
@@ -91,30 +100,33 @@ export class PresentationViewHistory {
taskTitle: string | null,
metadata: { key?: string; taskId?: string; createdAt?: number } = {}
) {
const id = ++this.sequence;
this.entries = [
...this.entries,
{
id,
label,
source,
taskTitle,
createdAt: Date.now(),
...metadata,
status: "running",
snapshot: this.current
}
];
const existing = metadata.key ? this.entries.find((entry) => entry.key === metadata.key) : null;
const id = existing?.id ?? ++this.sequence;
const entry: PresentationHistoryEntry = {
id,
label,
source,
taskTitle,
createdAt: existing?.createdAt ?? Date.now(),
...metadata,
status: "running",
snapshot: this.current
};
this.entries = existing
? this.entries.map((previous) => previous.id === id ? entry : previous)
: [...this.entries, entry];
return id;
}
finish(
id: number,
status: Exclude<PresentationHistoryEntry["status"], "running">,
message?: string,
snapshot = this.current
snapshot = this.current,
expected?: PresentationHistoryEntry
) {
// Hydration can replace this row before an older cancelled request settles.
this.entries = this.entries.map((entry) =>
entry.id === id ? { ...entry, status, message, snapshot } : entry
entry.id === id && (!expected || entry === expected) ? { ...entry, status, message, snapshot } : entry
);
}
record(
@@ -128,12 +140,34 @@ export class PresentationViewHistory {
return id;
}
select(id: number | null) {
this.pausePlayback();
if (id !== null && !this.entries.some((entry) => entry.id === id && entry.status !== "running"))
return false;
this.selectedId = id;
return true;
}
startPlayback(entries: PresentationHistoryEntry[]) {
this.playbackIds = entries.filter((entry) => entry.status === "completed").map((entry) => entry.id);
if (!this.playbackIds.length) return false;
const index = this.playbackIds.indexOf(this.selectedId ?? -1);
if (index < 0 || index === this.playbackIds.length - 1) this.selectedId = this.playbackIds[0];
this.playing = true;
return true;
}
advancePlayback() {
if (!this.playing) return false;
const next = this.playbackIds[this.playbackIds.indexOf(this.selectedId ?? -1) + 1];
if (next === undefined) {
this.pausePlayback();
return false;
}
this.selectedId = next;
return true;
}
pausePlayback() { this.playing = false; }
reset() {
this.pausePlayback();
this.playbackIds = [];
this.current = emptyPresentationView();
this.entries = [];
this.selectedId = null;