refactoring: graph state
This commit is contained in:
@@ -48,6 +48,20 @@ This doc summarizes recent improvements and suggested next steps for readability
|
||||
- **config/renderingLogic**: uses context nodes/edges directly (no casts); `setVarInContext(src: NodeLike)`.
|
||||
- **useRenderingNodeState**: casts to `NodeLike[]` / `EdgeLike[]` when calling `buildSourceSignatures` (types from renderingSignatures).
|
||||
|
||||
### 9. Graph state, connection state, and node display state
|
||||
|
||||
- **lib/graph/state.ts**: Central state contracts and flow doc.
|
||||
- **GraphState** – in-memory nodes + edges (useGraphStateWithHistory, persisted by useCanvasGraph).
|
||||
- **ConnectionPathState** – type describing the slice that drives edge status and path animation (trigger/updating/paused/error sets).
|
||||
- **NodeDisplayStatus** – `'initial' | 'loading' | 'success' | 'error'` for node UI; **getNodeDisplayStatus({ loading, error, hasContent })**.
|
||||
- **StoredGraphState** – shape for save/load (version + nodes + edges).
|
||||
- **FlowContext**: Module doc splits value into (1) Graph state, (2) Connection path state, (3) UI state. Same flat props, clearer sections.
|
||||
- **useGraphStateWithHistory**: JSDoc explains history (past/future), setNodes vs setNodesSilent, setStateImmediate.
|
||||
- **projectGraphStorage**: Uses **StoredGraphState** from state.ts; re-exports type. Doc references state flow.
|
||||
- **useRenderingNodeState**: Returns **displayStatus** (for NodeStatusIndicator/empty/error UI) and **lifecycle** (updating/error/paused for useSyncConnectionStatus). Hook calls useSyncConnectionStatus(id, state.lifecycle). Type includes RenderingNodeLifecycle.
|
||||
- **RenderingNode**: Uses **state.displayStatus** for NodeStatusIndicator instead of computing status locally.
|
||||
- **nodeLifecycle** and **connectionStatus**: Docs reference state.ts for overall state flow.
|
||||
|
||||
## Design patterns in use
|
||||
|
||||
| Pattern | Where |
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Per-project graph persistence (localStorage).
|
||||
* Used by CanvasPage to load/save and by ProjectsTablePage for export.
|
||||
* Saves and loads StoredGraphState (version + nodes + edges). Used by useCanvasGraph and export.
|
||||
*/
|
||||
|
||||
import type { StoredGraphState } from '@/lib/graph/state'
|
||||
|
||||
export type { StoredGraphState }
|
||||
export const PROJECT_FILE_EXT = '.zui.json'
|
||||
export const PROJECT_VERSION = 1
|
||||
|
||||
@@ -12,22 +15,20 @@ export function getGraphStorageKey(projectId: string): string {
|
||||
return `${GRAPH_KEY_PREFIX}${projectId}`
|
||||
}
|
||||
|
||||
export type GraphState = { version: number; nodes: unknown[]; edges: unknown[] }
|
||||
|
||||
export function loadGraphFromStorage(projectId: string): GraphState | null {
|
||||
export function loadGraphFromStorage(projectId: string): StoredGraphState | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(getGraphStorageKey(projectId))
|
||||
if (!raw) return null
|
||||
const data = JSON.parse(raw) as unknown
|
||||
if (!data || typeof data !== 'object' || !Array.isArray((data as GraphState).nodes) || !Array.isArray((data as GraphState).edges))
|
||||
if (!data || typeof data !== 'object' || !Array.isArray((data as StoredGraphState).nodes) || !Array.isArray((data as StoredGraphState).edges))
|
||||
return null
|
||||
return data as GraphState
|
||||
return data as StoredGraphState
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function saveGraphToStorage(projectId: string, state: GraphState): void {
|
||||
export function saveGraphToStorage(projectId: string, state: StoredGraphState): void {
|
||||
localStorage.setItem(getGraphStorageKey(projectId), JSON.stringify(state))
|
||||
}
|
||||
|
||||
|
||||
@@ -62,14 +62,6 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
? { width, height }
|
||||
: undefined
|
||||
|
||||
const status = state.loading
|
||||
? 'loading'
|
||||
: state.error
|
||||
? 'error'
|
||||
: state.renderedContent
|
||||
? 'success'
|
||||
: 'initial'
|
||||
|
||||
const { theme } = useTheme()
|
||||
const [rawEditorHeight, rawEditorContainerRef] = useResizeHeight(180, [viewMode])
|
||||
|
||||
@@ -138,7 +130,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeStatusIndicator status={status} variant="overlay" width={dimensions?.width} height={dimensions?.height}>
|
||||
<NodeStatusIndicator status={state.displayStatus} variant="overlay" width={dimensions?.width} height={dimensions?.height}>
|
||||
<BaseNode
|
||||
className="min-w-96 min-h-[320px]"
|
||||
dimensions={dimensions}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
stripTemplateSyntax,
|
||||
} from '@/lib/graph/rendering'
|
||||
import { buildSourceSignatures, type NodeLike, type EdgeLike } from '@/lib/graph/renderingSignatures'
|
||||
import { getNodeDisplayStatus, type NodeDisplayStatus } from '@/lib/graph/state'
|
||||
import type { ConfigTypeId, SourceRenderingLogicContext } from '@/lib/graph/rendering'
|
||||
|
||||
export type RenderingNodeData = {
|
||||
@@ -38,7 +39,19 @@ const DEFAULT_VIEWPORT_WIDTH = 1200
|
||||
const DEFAULT_VIEWPORT_HEIGHT = 800
|
||||
const RENDER_DEBOUNCE_MS = 250
|
||||
|
||||
/** Lifecycle state to pass to useSyncConnectionStatus so edge status (updating/paused/error) stays in sync. */
|
||||
export type RenderingNodeLifecycle = {
|
||||
updating: boolean
|
||||
error: boolean
|
||||
paused: boolean
|
||||
}
|
||||
|
||||
export type RenderingNodeState = {
|
||||
/** For NodeStatusIndicator and empty/error UI. Derived from loading, error, renderedContent. */
|
||||
displayStatus: NodeDisplayStatus
|
||||
/** Pass to useSyncConnectionStatus so edges show correct status (blue/yellow/red). */
|
||||
lifecycle: RenderingNodeLifecycle
|
||||
|
||||
// Connection & run control
|
||||
incomingIds: string[]
|
||||
effectiveUpdateMode: 'auto' | 'manual'
|
||||
@@ -183,7 +196,11 @@ export function useRenderingNodeState(
|
||||
incomingIds.length > 0 &&
|
||||
sourceSignature !== lastRunSourceSignature
|
||||
|
||||
useSyncConnectionStatus(id, { updating: loading, error: error != null, paused: hasPendingInputs })
|
||||
const lifecycle = useMemo<RenderingNodeLifecycle>(
|
||||
() => ({ updating: loading, error: error != null, paused: hasPendingInputs }),
|
||||
[loading, error, hasPendingInputs]
|
||||
)
|
||||
useSyncConnectionStatus(id, lifecycle)
|
||||
|
||||
useEffect(() => {
|
||||
if (incomingIds.length === 0) {
|
||||
@@ -472,7 +489,14 @@ export function useRenderingNodeState(
|
||||
const sourceData = useMemo(() => (srcNode?.data as Record<string, unknown>) ?? {}, [srcNode?.data])
|
||||
const sourceNodeType = (srcNode?.type as string) ?? null
|
||||
|
||||
const displayStatus = useMemo<NodeDisplayStatus>(
|
||||
() => getNodeDisplayStatus({ loading, error, hasContent: !!renderedContent }),
|
||||
[loading, error, renderedContent]
|
||||
)
|
||||
|
||||
return {
|
||||
displayStatus,
|
||||
lifecycle,
|
||||
incomingIds,
|
||||
effectiveUpdateMode,
|
||||
runTrigger,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||
|
||||
/**
|
||||
* In-memory graph state. History is a past/future stack of GraphState snapshots.
|
||||
* setNodes/setEdges push current state to past; setNodesSilent/setEdgesSilent update without history.
|
||||
* setStateImmediate replaces state and clears history (e.g. load example, import).
|
||||
*/
|
||||
export type GraphState = { nodes: AppNode[]; edges: AppEdge[] }
|
||||
|
||||
function cloneState(state: GraphState): GraphState {
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* Connection status: visual state of an edge (color/class).
|
||||
* Priority when multiple apply: error > paused > updating > default.
|
||||
*
|
||||
* Status is derived from node lifecycle state: nodes report updating / paused / error
|
||||
* via useSyncConnectionStatus() in nodeLifecycle.ts, which updates FlowContext sets.
|
||||
* Edges read those sets here to pick the single status per edge.
|
||||
* State flow: nodes report lifecycle via useSyncConnectionStatus() (nodeLifecycle.ts) → FlowContext
|
||||
* holds the sets → edges pass those sets into getConnectionStatus() here. See lib/graph/state.ts.
|
||||
*/
|
||||
|
||||
export type ConnectionStatus = 'default' | 'updating' | 'paused' | 'error'
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/**
|
||||
* FlowContext provides graph state, connection path state, and UI state to nodes and edges.
|
||||
*
|
||||
* ## 1. Graph state (nodes, edges)
|
||||
* - nodes, setNodes, edges, setEdges – owned by useCanvasGraph (history + persistence).
|
||||
*
|
||||
* ## 2. Connection path state (edge visuals and path animation)
|
||||
* - Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus() in nodeLifecycle.
|
||||
* - Context holds: connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds, connectionPathNodeIds,
|
||||
* connectionPathPausedSegmentNodeIds, connectionPathActiveSegmentNodeIds, connectionPathPausedNodeIds,
|
||||
* connectionPathErrorNodeIds, plus add/remove/start/end callbacks.
|
||||
* - Edges use these in getConnectionStatus() for color (default / updating / paused / error). See connectionStatus.ts.
|
||||
*
|
||||
* ## 3. UI state
|
||||
* - renamingNodeId, fullscreenNodeId, connectionFrom (drag-from handle), flowActionsRef, isValidConnection.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Connection } from '@xyflow/react'
|
||||
import type { AppNode, AppEdge } from './nodeTypes'
|
||||
@@ -13,48 +30,36 @@ export type FlowActions = {
|
||||
}
|
||||
|
||||
export type FlowContextValue = {
|
||||
// ---- Graph state ----
|
||||
nodes: AppNode[]
|
||||
setNodes: (updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => void
|
||||
edges: AppEdge[]
|
||||
setEdges: (updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => void
|
||||
|
||||
// ---- UI state ----
|
||||
renamingNodeId: string | null
|
||||
setRenamingNodeId: (id: string | null) => void
|
||||
/** Set when user starts dragging from an output handle; cleared on connect end. Used to highlight valid targets. */
|
||||
connectionFrom: ConnectionFrom
|
||||
setConnectionFrom: (v: ConnectionFrom) => void
|
||||
isValidConnection: (connection: Connection) => boolean
|
||||
/** Set by FlowKeyboardShortcuts so Node menubar can trigger paste / fit view. */
|
||||
flowActionsRef: React.MutableRefObject<FlowActions | null>
|
||||
/** When set, graph centers on this node and a fullscreen dialog shows the node. Cleared on close. */
|
||||
fullscreenNodeId: string | null
|
||||
setFullscreenNodeId: (id: string | null) => void
|
||||
/** Node ids currently updating (e.g. render loading, agent running). Only edges on those paths show the ant trail. */
|
||||
|
||||
// ---- Connection path state (edge status and path animation; see state.ts ConnectionPathState) ----
|
||||
connectionPathUpdatingNodeIds: string[]
|
||||
/** Node ids that triggered the current update (e.g. variable/config that changed). Path is restricted to downstream(trigger) ∩ upstream(updating). */
|
||||
connectionPathTriggerNodeIds: string[]
|
||||
/** Call when this node's output changed and may trigger downstream updates (e.g. variable value, config content). */
|
||||
addConnectionPathTrigger: (nodeId: string) => void
|
||||
/** All node ids on the path of an update. Edges with both endpoints in this set animate. */
|
||||
connectionPathNodeIds: Set<string>
|
||||
/** Path nodes in the "paused" segment (from trigger up to Archon on hold). Those edges are yellow. */
|
||||
connectionPathPausedSegmentNodeIds: Set<string>
|
||||
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
||||
connectionPathActiveSegmentNodeIds: Set<string>
|
||||
/** Nodes that are on hold (e.g. Renderer in manual mode waiting for Run). */
|
||||
connectionPathPausedNodeIds: string[]
|
||||
/** Add this node as paused (on hold); remove when user continues. */
|
||||
addConnectionPathPausedNode: (nodeId: string) => void
|
||||
/** Remove this node from paused. */
|
||||
removeConnectionPathPausedNode: (nodeId: string) => void
|
||||
/** Node ids that have an error (e.g. Render node). Incoming edges show error status (red). */
|
||||
connectionPathErrorNodeIds: string[]
|
||||
/** Call when this node has an error; remove when error is cleared. */
|
||||
addConnectionPathError: (nodeId: string) => void
|
||||
/** Call when this node's error is cleared. */
|
||||
removeConnectionPathError: (nodeId: string) => void
|
||||
/** Call when a path update starts for this node. Animation runs at least CONNECTION_PATH_UPDATE_MIN_MS. */
|
||||
startConnectionPathUpdate: (nodeId: string) => void
|
||||
/** Call when a path update ends for this node. If min duration not reached, animation continues until then. */
|
||||
endConnectionPathUpdate: (nodeId: string) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,21 @@
|
||||
* Node lifecycle: contract that nodes implement so the graph can show the right
|
||||
* connection status (edge colors) and path animation.
|
||||
*
|
||||
* ## State flow
|
||||
*
|
||||
* Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus(id, state).
|
||||
* This hook updates FlowContext sets; edges read them via getConnectionStatus() in connectionStatus.ts.
|
||||
* See lib/graph/state.ts for the overall state flow (graph state, connection path state).
|
||||
*
|
||||
* ## Lifecycle phases (conceptual)
|
||||
*
|
||||
* - **Idle** – Node is not on an active path; edges to/from it use default style.
|
||||
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
||||
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
||||
* path is computed from triggers + updating nodes.
|
||||
* - **Updating** – Node is doing async work (e.g. renderer loading, agent run triggered by renderer).
|
||||
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
||||
* edges on the path show "updating" (blue).
|
||||
* - **Paused** – Node is on hold (e.g. renderer in manual mode waiting for Run after inputs changed).
|
||||
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
||||
* segment show "paused" (yellow).
|
||||
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
||||
* `error: false` when cleared. Incoming edges to this node show "error" (red).
|
||||
* - **Idle** – Node is not on an active path; edges use default style.
|
||||
* - **Trigger** – Node's output changed; path is computed from triggers + updating nodes.
|
||||
* - **Updating** – Node is doing async work. Report `updating: true` → false. Path edges show blue.
|
||||
* - **Paused** – Node is on hold (e.g. manual mode waiting for Run). Report `paused: true` → false. Segment shows yellow.
|
||||
* - **Error** – Node has an error. Report `error: true` → false. Incoming edges show red.
|
||||
*
|
||||
* ## Connection status integration
|
||||
*
|
||||
* Edge status is derived in getConnectionStatus() from the sets that this lifecycle
|
||||
* feeds: connectionPathUpdatingNodeIds, connectionPathPausedNodeIds,
|
||||
* connectionPathErrorNodeIds (plus path/paused segment from graphPath). Priority:
|
||||
* error > paused > updating > default.
|
||||
*
|
||||
* Nodes that can be updating, paused, or in error should call useSyncConnectionStatus()
|
||||
* with their current state so edges update correctly.
|
||||
* Priority for edge status: error > paused > updating > default.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useRef } from 'react'
|
||||
|
||||
87
frontend/src/lib/graph/state.ts
Normal file
87
frontend/src/lib/graph/state.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Central state contracts for the graph: in-memory graph, connection path (edge visuals),
|
||||
* and node display status. Use these types to keep state flow predictable and readable.
|
||||
*
|
||||
* ## State flow (high level)
|
||||
*
|
||||
* 1. **Graph state** (nodes, edges) – Owned by useGraphStateWithHistory, persisted by useCanvasGraph.
|
||||
* Updates via setNodes/setEdges (with undo) or setNodesSilent/setEdgesSilent (no history).
|
||||
*
|
||||
* 2. **Connection path state** – Owned by useCanvasConnectionPath, provided via FlowContext.
|
||||
* Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus; the hook
|
||||
* updates context sets. Edges read those sets and getConnectionStatus() to decide color (default / updating / paused / error).
|
||||
*
|
||||
* 3. **Node display state** – Each node type derives its own UI state (e.g. loading, success, error, initial).
|
||||
* Rendering node uses displayStatus from useRenderingNodeState; other nodes can use getNodeDisplayStatus().
|
||||
*/
|
||||
|
||||
import type { AppNode, AppEdge } from './nodeTypes'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graph state (in-memory)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** In-memory graph: nodes and edges. Updated via setNodes/setEdges; history in useGraphStateWithHistory. */
|
||||
export type GraphState = {
|
||||
nodes: AppNode[]
|
||||
edges: AppEdge[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection path state (edge visuals and path animation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Slice of FlowContext that drives edge status and path animation.
|
||||
* Nodes report updating/error/paused via useSyncConnectionStatus; this state is derived from those reports.
|
||||
* Edges use pathNodeIds, pausedSegmentNodeIds, activeSegmentNodeIds, errorTargetNodeIds in getConnectionStatus().
|
||||
*/
|
||||
export type ConnectionPathState = {
|
||||
/** Node ids currently doing async work (e.g. render loading). Path edges show "updating" (blue). */
|
||||
connectionPathUpdatingNodeIds: string[]
|
||||
/** Node ids that triggered the update (e.g. config changed). Path = downstream(trigger) ∩ upstream(updating). */
|
||||
connectionPathTriggerNodeIds: string[]
|
||||
/** All node ids on the path. Edges with both endpoints here animate. */
|
||||
connectionPathNodeIds: Set<string>
|
||||
/** Path nodes in the paused segment (yellow edges). */
|
||||
connectionPathPausedSegmentNodeIds: Set<string>
|
||||
/** Path nodes in the active segment (blue edges). */
|
||||
connectionPathActiveSegmentNodeIds: Set<string>
|
||||
/** Node ids on hold (e.g. render in manual mode). */
|
||||
connectionPathPausedNodeIds: string[]
|
||||
/** Node ids with error (incoming edges show red). */
|
||||
connectionPathErrorNodeIds: string[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node display status (for UI: status indicator, empty state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Status for node UI: what to show in NodeStatusIndicator and empty/error states. */
|
||||
export type NodeDisplayStatus = 'initial' | 'loading' | 'success' | 'error'
|
||||
|
||||
/**
|
||||
* Derive display status from node state. Use in nodes to drive NodeStatusIndicator and empty/error UI.
|
||||
* Priority: loading > error > hasContent (success) > initial.
|
||||
*/
|
||||
export function getNodeDisplayStatus(state: {
|
||||
loading: boolean
|
||||
error: unknown
|
||||
hasContent: boolean
|
||||
}): NodeDisplayStatus {
|
||||
if (state.loading) return 'loading'
|
||||
if (state.error != null) return 'error'
|
||||
if (state.hasContent) return 'success'
|
||||
return 'initial'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stored graph state (persistence)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Shape of graph state when saved/loaded (e.g. localStorage). version for future migrations. */
|
||||
export type StoredGraphState = {
|
||||
version: number
|
||||
nodes: unknown[]
|
||||
edges: unknown[]
|
||||
}
|
||||
Reference in New Issue
Block a user