Documents
UI_Components_&_Loading_States
UI_Components_&_Loading_States
Type
External
Status
Published
Created
Feb 27, 2026
Updated
Mar 15, 2026
Updated by
Dosu Bot

UI Components & Loading States#

Overview#

Implement reusable UI components and loading state patterns for consistent user experience across the application. This ticket creates the foundational UI building blocks used by other tickets.

Scope#

Included:

  • ViewComponents for all new UI patterns
  • Stimulus controllers for interactivity (tabs, toasts)
  • Skeleton loader components for loading states
  • Toast notification system
  • Component templates (ERB files)
  • Component tests

Excluded:

  • Integration with specific pages (handled by feature tickets)
  • Existing components (StatusPill, ProgressBar already exist)

Acceptance Criteria#

ViewComponents#

AgentStatusCardComponent:

  • Component created: app/components/agent_status_card_component.rb
  • Template created: app/components/agent_status_card_component.html.erb
  • Displays: status badge, agent name, hash rate, error indicator
  • Uses existing StatusPillComponent for status badge
  • Error indicator shows count if > 0 errors in last 24h
  • Card links to agent detail page
  • Component delegates to Turbo Stream broadcast partials for real-time updates
  • Individual card sections update without full-page refresh
  • State, last seen, hash rate, and errors broadcast independently
  • Broadcast partials are minimal and don't reference current_user
  • Stable DOM IDs enable targeted replacement
  • error_count_last_24h method removed from component class, moved to partial logic
  • Fragment cache key uses cache: agent (not cache: true) to incorporate cache_key_with_version with agent's updated_at timestamp, preventing stale cached cards when agent state or errors change

Agent Index Partials:

Four targeted partials handle independent Turbo Stream broadcasts for agent index cards:

  • _index_state.html.erb - Displays agent state badge with color coding (online/offline/error). Uses StatusPillComponent and has a stable DOM ID using dom_id(agent, :index_state) pattern.
  • _index_last_seen.html.erb - Shows relative time since last heartbeat ("X minutes ago" or "Not seen yet"). DOM ID: dom_id(agent, :index_last_seen).
  • _index_hash_rate.html.erb - Displays current hash rate or "N/A" fallback. DOM ID: dom_id(agent, :index_hash_rate).
  • _index_errors.html.erb - Shows error count badge for last 24 hours (excluding info severity). Entire <div> has DOM ID: dom_id(agent, :index_errors). Text turns red when error count > 0.

Each partial:

  • Has a stable DOM ID for targeted replacement
  • Is broadcast via broadcast_replace_later_to callback (runs in background job)
  • Runs WITHOUT access to current_user or session (background job context)
  • Contains only the minimum HTML needed for that specific UI element

Broadcast Callbacks:

Agent model callbacks trigger targeted broadcasts:

after_update_commit :broadcast_index_state, if: -> { saved_change_to_state? }
after_update_commit :broadcast_index_last_seen, if: -> { saved_change_to_last_seen_at? }

AgentError model callback triggers Agent#broadcast_index_errors:

after_create_commit -> { agent.broadcast_index_errors }

The Agent#broadcast_index_errors method follows the same pattern as broadcast_index_state and broadcast_index_last_seen: it uses broadcast_replace_later_to with a stable DOM ID (dom_id(agent, :index_errors)) to update the error count display on agent index cards in real-time. Keeping the broadcast contract on the Agent model maintains consistency across all agent index card updates.

HashcatStatus updates trigger hash rate broadcasts via update_agent_metrics method (uses update_columns to bypass callbacks, then manually broadcasts).

AgentDetailTabsComponent:

  • Component created: app/components/agent_detail_tabs_component.rb
  • Template created: app/components/agent_detail_tabs_component.html.erb
  • Renders slots: overview_tab, errors_tab, configuration_tab, capabilities_tab
  • Integrates with Stimulus tabs controller
  • Preserves tab structure for Turbo Stream broadcasts

CampaignProgressComponent:

  • Component created: app/components/campaign_progress_component.rb
  • Template created: app/components/campaign_progress_component.html.erb
  • Displays: progress bar, percentage text, ETA text
  • Uses existing ProgressBarComponent or extends it
  • Handles missing ETA gracefully ("Calculating...")

ErrorModalComponent:

  • Component created: app/components/error_modal_component.rb
  • Template created: app/components/error_modal_component.html.erb
  • Displays: error message, severity badge, timestamp
  • Uses Bootstrap modal component
  • Severity badges: fatal/error (danger), warning (warning), info (info)

SystemHealthCardComponent:

  • Component created: app/components/system_health_card_component.rb
  • Template created: app/components/system_health_card_component.html.erb
  • Displays: service name, status badge, latency, error message
  • Status variants: healthy (success), unhealthy (danger), checking (secondary)
  • Icons: check-circle (healthy), x-circle (unhealthy)

TaskActionsComponent:

  • Component created: app/components/task_actions_component.rb
  • Template created: app/components/task_actions_component.html.erb
  • Conditional buttons: cancel, retry, reassign, view logs, download results
  • Buttons shown based on task state and user permissions
  • Uses plain Bootstrap button classes (btn btn-primary, btn btn-danger, btn-sm, etc.)

SkeletonLoaderComponent:

  • Component created: app/components/skeleton_loader_component.rb
  • Template created: app/components/skeleton_loader_component.html.erb
  • Types supported: , ,
  • Configurable count (default: 5)
  • Uses CSS animations for shimmer effect
  • Matches layout of actual content

ToastNotificationComponent:

  • Component created: app/components/toast_notification_component.rb
  • Template created: app/components/toast_notification_component.html.erb
  • Variants: success, danger, warning, info
  • Integrates with Stimulus toast controller
  • Auto-dismiss after 5 seconds
  • Positioned in top-right corner

Stimulus Controllers#

tabs_controller.js:

  • Controller created: app/javascript/controllers/tabs_controller.js
  • Targets: tab, panel
  • Action: switch(event) - switches active tab
  • Shows first tab by default on connect
  • Updates active tab styling
  • Hides/shows panels with d-none class

toast_controller.js:

  • Controller created: app/javascript/controllers/toast_controller.js
  • Values: autohide (boolean, default: true), delay (number, default: 5000)
  • Initializes Bootstrap Toast on connect
  • Auto-dismiss after delay
  • Removes element from DOM after hidden
  • Imports Bootstrap Toast component

Shared Partials#

  • Toast partial created: app/views/shared/_toast.html.erb
  • Toast container added to layout: file/views/layouts/application.html.erb
  • Container positioned for toast display (top-right)

Testing#

  • Component tests for all ViewComponents
  • Stimulus controller tests for tabs_controller
  • Stimulus controller tests for toast_controller
  • Visual regression tests (optional, if using tools)
  • Verify components work in air-gapped environment (no external assets)

Technical References#

  • Core Flows: spec:50650885-e043-4e99-960b-672342fc4139/c565e255-83e7-4d16-a4ec-d45011fa5cad (Flow 6: Loading & Feedback Patterns)
  • Tech Plan: spec:50650885-e043-4e99-960b-672342fc4139/f3c30678-d7af-45ab-a95b-0d0714906b9e (Component Architecture, Stimulus Controllers)
  • Turbo Stream Broadcasts: doc.md (section "Turbo Stream Broadcasts")
  • Agent Monitoring: ticket:50650885-e043-4e99-960b-672342fc4139/[Agent Monitoring & Real-Time Updates]

Dependencies#

Requires:

  • None (foundational ticket)

Blocks:

  • ticket:50650885-e043-4e99-960b-672342fc4139/[Agent Monitoring & Real-Time Updates] - Provides AgentStatusCardComponent, tabs controller
  • ticket:50650885-e043-4e99-960b-672342fc4139/[Campaign Progress & ETA Display] - Provides CampaignProgressComponent, ErrorModalComponent
  • ticket:50650885-e043-4e99-960b-672342fc4139/[Task Management Actions] - Provides TaskActionsComponent
  • ticket:50650885-e043-4e99-960b-672342fc4139/[System Health Monitoring] - Provides SystemHealthCardComponent

Implementation Notes#

  • Follow existing ViewComponent patterns in file/components/
  • Use dry-initializer option syntax (already used in existing components)
  • Inherit from ApplicationViewComponent
  • Use plain Bootstrap HTML and utility classes for layout/structure
  • ViewComponents should render Bootstrap HTML directly (no abstraction layer)
  • Use Bootstrap utility classes: btn btn-primary, d-flex, gap-2, card, list-group, etc.
  • Test components in isolation before integrating
  • Ensure all assets (CSS, JS) are bundled for air-gapped deployment
  • Use Bootstrap 5 components (already in stack)

Component Development Guidelines:

The Railsboot component abstraction layer has been removed (PR #706). All ViewComponents now use plain ERB templates with Bootstrap utility classes directly.

  • Button patterns: <button class="btn btn-primary btn-sm">, <a href="..." class="btn btn-secondary">
  • Layout utilities: d-flex, justify-content-between, align-items-center, gap-2, gap-3
  • Card structure: <div class="card"><div class="card-body">...</div></div>
  • List groups: <ul class="list-group"><li class="list-group-item">...</li></ul>
  • Reference implementations: See agent_status_card_component.html.erb, agent_detail_tabs_component.html.erb for examples of plain Bootstrap HTML patterns

For detailed component patterns and theming guidelines, reference doc.md sections on "Catppuccin Macchiato Theme" and "Layout Grid". For frontend gotchas (navbar buttons, z-index utilities, Railsboot removal), reference doc.md section "Frontend & Accessibility".

Turbo Stream Broadcast Pattern:

The AgentStatusCardComponent demonstrates a reusable pattern for real-time component updates:

  1. Extract Minimal Partials - Create small partials (_index_state.html.erb, _index_hash_rate.html.erb, _index_errors.html.erb) that wrap a single UI element with a stable DOM ID using dom_id(record, :suffix).

  2. Model Callbacks - Use conditional after_update_commit callbacks (e.g., if: -> { saved_change_to_state? }) to broadcast only when specific attributes change, reducing unnecessary updates. The Agent#broadcast_index_errors method is called from the AgentError model's after_create_commit callback to keep the broadcast contract on the Agent model.

  3. Background Job Context - Broadcast partials run in background jobs WITHOUT access to current_user or session. All data must come from the model or explicit locals.

  4. Targeted Replacement - Use broadcast_replace_later_to with the stable DOM ID as the target to update individual sections without full-page refresh.

  5. Fragment Cache Invalidation - When using fragment caching on index cards, use cache: agent (not cache: true) so that cache_key_with_version incorporates the agent's updated_at timestamp. This ensures cached cards are invalidated when agent state or errors change, preventing stale content from being served.

This pattern extends to other real-time components. See the Attack model's broadcast_index_state implementation for a similar approach. For constraints and gotchas, reference doc.md section "Turbo Stream Broadcasts".

Estimated Effort#

2-3 days (8 components + 2 Stimulus controllers + templates + tests)