Files

17 KiB
Raw Permalink Blame History

VIEW-004: Node Census

View Type: 📋 Sidebar Panel (opens alongside canvas)

Overview

A searchable inventory of every node in a component (or the entire project), grouped by type with automatic duplicate detection and conflict warnings. The "find anything" tool for complex canvases.

Estimate: 2-3 days
Priority: HIGH
Complexity: Low
Dependencies: VIEW-000 (Foundation)


The Problem

In a complex canvas:

  • You can't find that Variable node you know exists somewhere
  • You accidentally create duplicate nodes with the same name
  • Two Variables with the same name cause subtle bugs
  • You don't know how many REST calls or Functions the component has
  • Cleaning up unused nodes requires manual hunting

The Solution

A comprehensive node inventory that:

  1. Lists all nodes grouped by type/category
  2. Detects duplicates (same name + same type)
  3. Warns about potential conflicts
  4. Enables quick search by name or type
  5. Click to jump to any node

User Stories

  1. As a developer searching, I want to find a node by name without scrolling around the canvas.

  2. As a developer cleaning up, I want to see all nodes grouped by type so I can identify unused or redundant ones.

  3. As a developer debugging, I want to know if I have duplicate Variables that might be causing conflicts.

  4. As a developer auditing, I want to see a count of each node type to understand component complexity.


UI Design

Main View

┌─────────────────────────────────────────────────────────────────┐
│ Node Census                                  [Scope: Component ▼]│
├─────────────────────────────────────────────────────────────────┤
│ 🔍 Search nodes...                                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ ⚠️ POTENTIAL ISSUES (3)                              [Collapse] │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ⚠️ "currentUser" Object appears 2 times                     │ │
│ │    ├─ Presales Page (near top)              [→ Jump]        │ │
│ │    └─ Presales Page (near bottom)           [→ Jump]        │ │
│ │    May cause: Value conflicts, unexpected overwrites        │ │
│ │                                                             │ │
│ │ ⚠️ "activeConversation" Variable appears 3 times            │ │
│ │    ├─ Presales Page                         [→ Jump]        │ │
│ │    ├─ Presales Page (duplicate!)            [→ Jump]        │ │
│ │    └─ Chat Component                        [→ Jump]        │ │
│ │    May cause: Race conditions, stale data                   │ │
│ │                                                             │ │
│ │ ⚠️ "response.output.trigger_" Expression appears 4 times    │ │
│ │    Consider: Consolidate or rename for clarity              │ │
│ └─────────────────────────────────────────────────────────────┘ │
│                                                                 │
│ 📊 BY CATEGORY                                                  │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ▼ 💾 Data (18 nodes)                                        │ │
│ │   ├─ Variable (8)                                           │ │
│ │   │   ├─ activeConversation ×2 ⚠️           [→ Jump]        │ │
│ │   │   ├─ presales                           [→ Jump]        │ │
│ │   │   ├─ messageText                        [→ Jump]        │ │
│ │   │   ├─ errorMessage                       [→ Jump]        │ │
│ │   │   └─ ... 3 more                                         │ │
│ │   ├─ Object (6)                                             │ │
│ │   │   ├─ currentUser ×2 ⚠️                  [→ Jump]        │ │
│ │   │   ├─ userData                           [→ Jump]        │ │
│ │   │   └─ ... 3 more                                         │ │
│ │   └─ Array (4)                                              │ │
│ │       ├─ firstResponderMessages             [→ Jump]        │ │
│ │       ├─ currentConversationMes...          [→ Jump]        │ │
│ │       └─ ... 2 more                                         │ │
│ │                                                             │ │
│ │ ▶ ⚡ Logic (22 nodes)                                        │ │
│ │ ▶ 📦 Visual (45 nodes)                                       │ │
│ │ ▶ 🌐 API (8 nodes)                                           │ │
│ │ ▶ 📡 Events (6 nodes)                                        │ │
│ │ ▶ 🔧 Other (9 nodes)                                         │ │
│ └─────────────────────────────────────────────────────────────┘ │
│                                                                 │
│ ────────────────────────────────────────────────────────────── │
│ Total: 108 nodes | 3 potential issues                          │
└─────────────────────────────────────────────────────────────────┘

Search Results View

┌─────────────────────────────────────────────────────────────────┐
│ 🔍 currentUser                                          [Clear] │
├─────────────────────────────────────────────────────────────────┤
│ Found 4 matches for "currentUser"                               │
│                                                                 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 💾 "currentUser" Object                                     │ │
│ │    Component: Presales Page                                 │ │
│ │    Connections: 5 outputs connected                         │ │
│ │    [→ Jump to Node]                                         │ │
│ │                                                             │ │
│ │ 💾 "currentUser" Object                                     │ │
│ │    Component: AuthFlow                                      │ │
│ │    Connections: 3 outputs connected                         │ │
│ │    [→ Jump to Node]                                         │ │
│ │                                                             │ │
│ │ ⚡ Expression containing "currentUser"                       │ │
│ │    Component: Presales Page                                 │ │
│ │    Expression: currentUser.name + " - " + ...               │ │
│ │    [→ Jump to Node]                                         │ │
│ │                                                             │ │
│ │ 🔧 Function referencing "currentUser"                       │ │
│ │    Component: App Shell                                     │ │
│ │    [→ Jump to Node]                                         │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Scope Selector

┌────────────────────────────┐
│ Scope:                     │
│ ○ Current Component        │  ← Show nodes in current component only
│ ○ Component + Children     │  ← Include subcomponents  
│ ● Entire Project           │  ← Search all components
└────────────────────────────┘

Interactions

  • Search - Filter by name, type, or content
  • Click category - Expand/collapse
  • Click node - Select (highlight in list)
  • [→ Jump] - Navigate to node in canvas
  • Scope dropdown - Change search scope
  • Click issue - Expand to show all instances

Technical Design

Data Model

interface NodeCensus {
  scope: 'component' | 'componentWithChildren' | 'project';
  component?: ComponentModel;  // If scoped to component
  
  totalNodes: number;
  
  byCategory: {
    category: NodeCategory;
    count: number;
    types: {
      typeName: string;
      displayName: string;
      count: number;
      nodes: CensusNode[];
    }[];
  }[];
  
  duplicates: DuplicateGroup[];
  warnings: CensusWarning[];
}

interface CensusNode {
  id: string;
  type: string;
  label: string;  // User-assigned label or generated name
  displayName: string;
  componentName: string;
  componentPath: string;
  
  // Connection info
  inputCount: number;
  outputCount: number;
  connectedInputs: number;
  connectedOutputs: number;
  
  // For search
  searchableContent: string;  // Includes parameters, expressions, etc.
  
  // Position hint for "near top/bottom" descriptions
  positionHint: 'top' | 'middle' | 'bottom';
}

interface DuplicateGroup {
  name: string;
  type: string;
  instances: CensusNode[];
  severity: 'info' | 'warning' | 'error';
  reason: string;
  suggestion: string;
}

interface CensusWarning {
  type: 'duplicate' | 'orphan' | 'complexity' | 'naming';
  message: string;
  nodes: CensusNode[];
  severity: 'info' | 'warning' | 'error';
}

Building the Census

function buildNodeCensus(
  project: ProjectModel,
  scope: CensusScope,
  currentComponent?: ComponentModel
): NodeCensus {
  const nodes: CensusNode[] = [];
  
  // Collect nodes based on scope
  if (scope === 'component' && currentComponent) {
    collectNodesFromComponent(currentComponent, nodes);
  } else if (scope === 'componentWithChildren' && currentComponent) {
    collectNodesRecursive(currentComponent, nodes);
  } else {
    project.forEachComponent(comp => collectNodesFromComponent(comp, nodes));
  }
  
  // Categorize
  const byCategory = categorizeNodes(nodes);
  
  // Detect duplicates
  const duplicates = findDuplicates(nodes);
  
  // Generate warnings
  const warnings = generateWarnings(nodes, duplicates);
  
  return {
    scope,
    component: currentComponent,
    totalNodes: nodes.length,
    byCategory,
    duplicates,
    warnings
  };
}

function collectNodesFromComponent(
  component: ComponentModel,
  nodes: CensusNode[]
): void {
  component.graph.forEachNode(node => {
    nodes.push({
      id: node.id,
      type: node.type.name,
      label: node.label || getDefaultLabel(node),
      displayName: node.type.displayName || node.type.name,
      componentName: component.name,
      componentPath: component.fullName,
      inputCount: node.getPorts('input').length,
      outputCount: node.getPorts('output').length,
      connectedInputs: countConnectedInputs(component, node),
      connectedOutputs: countConnectedOutputs(component, node),
      searchableContent: buildSearchableContent(node),
      positionHint: calculatePositionHint(node)
    });
  });
}

Search Implementation

function searchNodes(
  census: NodeCensus,
  query: string
): CensusNode[] {
  const lowerQuery = query.toLowerCase();
  
  return census.byCategory
    .flatMap(cat => cat.types.flatMap(t => t.nodes))
    .filter(node => 
      node.label.toLowerCase().includes(lowerQuery) ||
      node.type.toLowerCase().includes(lowerQuery) ||
      node.displayName.toLowerCase().includes(lowerQuery) ||
      node.searchableContent.toLowerCase().includes(lowerQuery)
    )
    .sort((a, b) => {
      // Exact matches first
      const aExact = a.label.toLowerCase() === lowerQuery;
      const bExact = b.label.toLowerCase() === lowerQuery;
      if (aExact && !bExact) return -1;
      if (bExact && !aExact) return 1;
      
      // Then by relevance (starts with)
      const aStarts = a.label.toLowerCase().startsWith(lowerQuery);
      const bStarts = b.label.toLowerCase().startsWith(lowerQuery);
      if (aStarts && !bStarts) return -1;
      if (bStarts && !aStarts) return 1;
      
      return a.label.localeCompare(b.label);
    });
}

Implementation Phases

Phase 1: Data Collection (0.5-1 day)

  1. Implement buildNodeCensus() function
  2. Implement scope handling (component, recursive, project)
  3. Build searchable content from node parameters
  4. Calculate position hints

Verification:

  • All nodes collected correctly
  • Scopes work as expected
  • Searchable content includes expressions, URLs, etc.

Phase 2: Categorization & Duplicates (0.5 day)

  1. Implement node categorization
  2. Implement duplicate detection
  3. Generate warnings with appropriate severity
  4. Create suggestions for fixing issues

Verification:

  • Categories correct for all node types
  • Duplicates detected reliably
  • Warnings helpful

Phase 3: Basic UI (1 day)

  1. Create NodeCensusView component
  2. Implement collapsible category tree
  3. Show duplicate warnings section
  4. Add scope selector
  5. Display node counts

Verification:

  • Tree renders correctly
  • Collapse/expand works
  • Warnings display prominently

Phase 4: Search & Navigation (0.5-1 day)

  1. Implement search input with filtering
  2. Add keyboard navigation
  3. Implement "Jump to Node" navigation
  4. Add search result highlighting

Verification:

  • Search filters correctly
  • Results update live
  • Jump to node works across components

Phase 5: Polish (0.5 day)

  1. Add loading states
  2. Improve typography and icons
  3. Add empty states
  4. Performance optimization for large projects

Verification:

  • UI polished
  • Large projects handled well
  • Responsive

Files to Create

packages/noodl-editor/src/editor/src/views/AnalysisPanel/
└── NodeCensusView/
    ├── index.ts
    ├── NodeCensusView.tsx
    ├── NodeCensusView.module.scss
    ├── CategoryTree.tsx
    ├── NodeListItem.tsx
    ├── DuplicateWarnings.tsx
    ├── SearchInput.tsx
    ├── ScopeSelector.tsx
    └── useNodeCensus.ts

Success Criteria

  • All nodes in scope appear in census
  • Categories correctly assigned
  • Duplicates detected and warned
  • Search finds nodes by name, type, and content
  • Jump to node works reliably
  • Scope switching works
  • Renders fast (< 500ms) for 200+ nodes

Future Enhancements

  • Bulk actions - Select multiple nodes, delete orphans
  • Export - Export node list as CSV
  • Comparison - Compare census between two components
  • History - Track node count over time
  • Orphan detection - Find nodes with no connections

Risks & Mitigations

Risk Mitigation
Large projects slow to census Cache results, incremental updates
False positive duplicates Allow user to dismiss warnings
Categorization misses node types Maintain mapping, default to "Other"

Dependencies

  • VIEW-000 Foundation (for categorization utilities)

Blocks

  • None (independent view)