Skip to Content

Assurance Case Editor

The assurance case editor is the core interactive component of the TEA Platform. It allows users to visually create and edit assurance cases as node-based diagrams. This page documents how the editor is implemented.

Technology

The editor is built with ReactFlow , a highly customisable library for building node-based editors and diagrams in React.

ReactFlow Documentation

For general ReactFlow concepts and API reference, see the official ReactFlow documentation .

Architecture Overview

┌─────────────────────────────────────────────────────────┐ │ FlowEditor Component │ │ (Client Component) │ ├─────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ ReactFlow │ │ Toolbar │ │ Node Inspector │ │ │ │ Canvas │ │ │ │ Panel │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ Custom Node Types │ │ ┌────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Goal │ │ Strategy │ │ Evidence │ │ Context │ │ │ │ Node │ │ Node │ │ Node │ │ Node │ │ │ └────────┘ └──────────┘ └────────────┘ └────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ State Management │ │ (React state + Server Actions) │ └─────────────────────────────────────────────────────────┘

Key Files

FilePurpose
components/cases/flow-editor/flow-editor.tsxMain editor component
components/cases/flow-editor/nodes/Custom node components
components/cases/flow-editor/edges/Custom edge components
components/cases/flow-editor/toolbar/Editor toolbar
components/cases/flow-editor/panels/Side panels (inspector, etc.)
actions/elements.tsServer Actions for element CRUD

Custom Node Types

Each element type has a corresponding custom node component. These are registered with ReactFlow via the nodeTypes prop.

Node Type Registration

const nodeTypes = { goal: GoalNode, strategy: StrategyNode, propertyClaim: PropertyClaimNode, evidence: EvidenceNode, context: ContextNode, assumption: AssumptionNode, justification: JustificationNode, module: ModuleNode, } <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} // ... />

Node Component Structure

Each custom node follows a similar pattern:

interface NodeData { id: string elementType: ElementType name: string description: string // ... other element fields } function GoalNode({ data, selected }: NodeProps<NodeData>) { return ( <div className={cn( 'goal-node', selected && 'selected' )}> <Handle type="target" position={Position.Top} /> <div className="node-header"> <span className="node-label">{data.name}</span> </div> <div className="node-content"> {data.description} </div> <Handle type="source" position={Position.Bottom} /> </div> ) }

Node Styling

Nodes are styled to follow GSN (Goal Structuring Notation) conventions:

Element TypeShapeColour
GoalRectangleBlue
StrategyParallelogramGreen
Property ClaimRectangleLight Blue
EvidenceCircle/RoundedOrange
ContextRounded RectangleGrey
AssumptionEllipseYellow
JustificationEllipsePurple

Data Flow

Loading the Editor

  1. Server Component fetches case data with elements
  2. Elements are transformed to ReactFlow nodes/edges
  3. FlowEditor receives initial state as props
// app/(main)/cases/[id]/page.tsx async function CasePage({ params }) { const caseData = await getCaseWithElements(params.id) const { nodes, edges } = transformToFlow(caseData.elements) return <FlowEditor initialNodes={nodes} initialEdges={edges} /> }

Element-to-Node Transformation

Elements from the database are transformed to ReactFlow nodes:

function elementToNode(element: AssuranceElement): Node { return { id: element.id, type: elementTypeToNodeType(element.elementType), position: element.position ?? { x: 0, y: 0 }, data: { id: element.id, elementType: element.elementType, name: element.name, description: element.description, // ... other fields } } }

Edge Generation

Edges are generated from the parent-child relationships:

function generateEdges(elements: AssuranceElement[]): Edge[] { return elements .filter(el => el.parentId) .map(el => ({ id: `${el.parentId}-${el.id}`, source: el.parentId, target: el.id, type: 'smoothstep', })) }

Interactivity

Adding Elements

When a user adds a new element:

  1. User selects element type from toolbar
  2. User clicks on canvas or existing node
  3. Client creates optimistic node
  4. Server Action creates element in database
  5. On success, node ID is updated; on failure, node is removed
async function handleAddElement(type: ElementType, parentId?: string) { // Optimistic update const tempId = `temp-${Date.now()}` addNode({ id: tempId, type, data: { ... } }) // Server Action const result = await createElement({ type, parentId, caseId }) if (result.success) { updateNodeId(tempId, result.data.id) } else { removeNode(tempId) toast.error(result.error) } }

Editing Elements

Element editing is handled through the inspector panel:

  1. User selects a node
  2. Inspector panel shows element details
  3. User edits fields
  4. Changes are debounced and saved via Server Action

Deleting Elements

Deletion cascades to child elements:

  1. User selects node and presses Delete or uses context menu
  2. Confirmation dialog for elements with children
  3. Server Action deletes element and children
  4. Nodes are removed from canvas

State Management

The editor uses a combination of:

  • Local React state for UI state (selection, viewport)
  • ReactFlow state for nodes and edges
  • Server Actions for persistence

Concurrency Control

To prevent conflicts in collaborative editing:

  1. Cases have a lockUuid field
  2. When editing, user acquires a lock
  3. Lock is released on save or timeout
  4. Other users see “Case is being edited” message

Keyboard Shortcuts

ShortcutAction
Delete / BackspaceDelete selected elements
Ctrl/Cmd + CCopy selected elements
Ctrl/Cmd + VPaste elements
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
Ctrl/Cmd + ASelect all
EscapeDeselect all

Further Reading