From 5e85f82eda2b5ae767814569844c5b4d46bf3649 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Thu, 26 Feb 2026 22:09:23 -0500 Subject: [PATCH] chore(web-ui): remove dead filter components Remove 9 unused files that are not imported anywhere in the app. The Executions page uses PageHeader with FilterSelect dropdowns, not these legacy toggle-button filter components. Removed files: - ExecutionFilters.tsx, ExecutionsList.tsx, QuickFilters.tsx - SearchWithFilters.tsx, SuggestedFilters.tsx, FilterTag.tsx - hooks/useFilterState.ts, utils/filterUtils.ts, types/filters.ts Co-Authored-By: Claude Opus 4.6 --- .../src/components/ExecutionFilters.tsx | 292 ------------ .../client/src/components/ExecutionsList.tsx | 431 ------------------ .../web/client/src/components/FilterTag.tsx | 46 -- .../client/src/components/QuickFilters.tsx | 100 ---- .../src/components/SearchWithFilters.tsx | 241 ---------- .../src/components/SuggestedFilters.tsx | 81 ---- .../web/client/src/hooks/useFilterState.ts | 112 ----- control-plane/web/client/src/types/filters.ts | 214 --------- .../web/client/src/utils/filterUtils.ts | 311 ------------- 9 files changed, 1828 deletions(-) delete mode 100644 control-plane/web/client/src/components/ExecutionFilters.tsx delete mode 100644 control-plane/web/client/src/components/ExecutionsList.tsx delete mode 100644 control-plane/web/client/src/components/FilterTag.tsx delete mode 100644 control-plane/web/client/src/components/QuickFilters.tsx delete mode 100644 control-plane/web/client/src/components/SearchWithFilters.tsx delete mode 100644 control-plane/web/client/src/components/SuggestedFilters.tsx delete mode 100644 control-plane/web/client/src/hooks/useFilterState.ts delete mode 100644 control-plane/web/client/src/types/filters.ts delete mode 100644 control-plane/web/client/src/utils/filterUtils.ts diff --git a/control-plane/web/client/src/components/ExecutionFilters.tsx b/control-plane/web/client/src/components/ExecutionFilters.tsx deleted file mode 100644 index 9f2322f3..00000000 --- a/control-plane/web/client/src/components/ExecutionFilters.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; -import { Button } from './ui/button'; -import { Badge } from './ui/badge'; -import { ResponsiveGrid } from '@/components/layout/ResponsiveGrid'; -import { FilterSelect } from './ui/FilterSelect'; -import { TextInput } from './ui/TextInput'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; -import type { ExecutionFilters as ExecutionFiltersType } from '../types/executions'; - -interface ExecutionFiltersProps { - filters: Partial; - onFiltersChange: (filters: Partial) => void; - onSearch: (searchTerm: string) => void; - searchTerm: string; -} - -export function ExecutionFilters({ - filters, - onFiltersChange, - onSearch, - searchTerm -}: ExecutionFiltersProps) { - const [isOpen, setIsOpen] = useState(false); - const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm); - const [localFilters, setLocalFilters] = useState(filters); - - useEffect(() => { - setLocalSearchTerm(searchTerm); - }, [searchTerm]); - - useEffect(() => { - setLocalFilters(filters); - }, [filters]); - - const handleSearchSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onSearch(localSearchTerm); - }; - - const handleFilterChange = (key: keyof ExecutionFiltersType, value: any) => { - const newFilters = { ...localFilters, [key]: value }; - setLocalFilters(newFilters); - onFiltersChange(newFilters); - }; - - const clearFilters = () => { - const clearedFilters = { - page: 1, - page_size: filters.page_size || 20 - }; - setLocalFilters(clearedFilters); - setLocalSearchTerm(''); - onFiltersChange(clearedFilters); - onSearch(''); - }; - - const getActiveFiltersCount = () => { - let count = 0; - if (localFilters.agent_node_id) count++; - if (localFilters.workflow_id) count++; - if (localFilters.session_id) count++; - if (localFilters.actor_id) count++; - if (localFilters.status) count++; - if (localFilters.start_time) count++; - if (localFilters.end_time) count++; - if (localSearchTerm) count++; - return count; - }; - - const statusOptions = [ - { value: '', label: 'All Statuses' }, - { value: 'running', label: 'Running' }, - { value: 'completed', label: 'Completed' }, - { value: 'failed', label: 'Failed' }, - { value: 'pending', label: 'Pending' } - ]; - - const pageSizeOptions = [ - { value: 10, label: '10 per page' }, - { value: 20, label: '20 per page' }, - { value: 50, label: '50 per page' }, - { value: 100, label: '100 per page' } - ]; - - return ( - - - - -
- Filters & Search -
- {getActiveFiltersCount() > 0 && ( - - {getActiveFiltersCount()} active - - )} - - {isOpen ? '▲' : '▼'} - -
-
-
-
- - - - {/* Search */} -
- -
- setLocalSearchTerm(e.target.value)} - placeholder="Search by workflow name, execution ID, or error message..." - className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - - {localSearchTerm && ( - - )} -
-
- - {/* Filter Grid */} - - {/* Status Filter */} - handleFilterChange('status', value || undefined)} - options={statusOptions} - /> - - {/* Agent Node Filter */} - handleFilterChange('agent_node_id', e.target.value || undefined)} - placeholder="Filter by agent node ID..." - /> - - {/* Workflow Filter */} - handleFilterChange('workflow_id', e.target.value || undefined)} - placeholder="Filter by workflow ID..." - /> - - {/* Session Filter */} - handleFilterChange('session_id', e.target.value || undefined)} - placeholder="Filter by session ID..." - /> - - {/* Actor Filter */} - handleFilterChange('actor_id', e.target.value || undefined)} - placeholder="Filter by actor ID..." - /> - - {/* Page Size */} - handleFilterChange('page_size', parseInt(value, 10))} - options={pageSizeOptions.map(({ value, label }) => ({ value: String(value), label }))} - /> - - - {/* Time Range Filters */} - -
- - { - const date = new Date(localFilters.start_time); - return isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 16); - })() : ''} - onChange={(e) => handleFilterChange('start_time', e.target.value ? new Date(e.target.value).toISOString() : undefined)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const date = new Date(localFilters.end_time); - return isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 16); - })() : ''} - onChange={(e) => handleFilterChange('end_time', e.target.value ? new Date(e.target.value).toISOString() : undefined)} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - {/* Quick Time Filters */} -
- -
- - - - -
-
- - {/* Actions */} -
-
- {getActiveFiltersCount() > 0 && `${getActiveFiltersCount()} filter(s) active`} -
-
- - -
-
-
-
-
-
- ); -} diff --git a/control-plane/web/client/src/components/ExecutionsList.tsx b/control-plane/web/client/src/components/ExecutionsList.tsx deleted file mode 100644 index 39678d88..00000000 --- a/control-plane/web/client/src/components/ExecutionsList.tsx +++ /dev/null @@ -1,431 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'; -import { Badge, type BadgeProps } from './ui/badge'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { ResponsiveGrid } from '@/components/layout/ResponsiveGrid'; -import { Separator } from './ui/separator'; -import { Skeleton } from './ui/skeleton'; -import { - getExecutionsSummary, - getExecutionStats, - streamExecutionEvents, - searchExecutions -} from '../services/executionsApi'; -import type { - ExecutionSummary, - ExecutionStats, - ExecutionFilters, - ExecutionGrouping -} from '../types/executions'; -import { statusTone } from '@/lib/theme'; -import { cn } from '@/lib/utils'; - -type StatusVariant = NonNullable; - -const executionStatusVariantMap: Record = { - completed: "success", - success: "success", - failed: "failed", - error: "failed", - running: "running", - active: "running", - pending: "pending", - queued: "pending", -}; - -const getExecutionStatusVariant = (status: string | undefined | null): StatusVariant => { - if (!status) return "unknown"; - const normalized = status.toLowerCase(); - return executionStatusVariantMap[normalized] ?? "unknown"; -}; - -const formatStatusLabel = (status: string | undefined | null): string => { - if (!status) return "Unknown"; - return status - .toLowerCase() - .replace(/[_-]+/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()); -}; - -interface ExecutionsListProps { - initialFilters?: Partial; - showStats?: boolean; - showFilters?: boolean; - title?: string; - description?: string; -} - -export function ExecutionsList({ - initialFilters = {}, - showStats = true, - showFilters = true, - title = "Execution Monitor", - description = "Track and monitor all workflow executions across your agents" -}: ExecutionsListProps) { - const [executions, setExecutions] = useState([]); - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [filters, setFilters] = useState>({ - page: 1, - page_size: 20, - ...initialFilters - }); - const [pagination, setPagination] = useState({ - total_count: 0, - page: 1, - page_size: 20, - total_pages: 0, - has_next: false, - has_prev: false - }); - const [searchTerm, setSearchTerm] = useState(''); - const [grouping] = useState({ - group_by: 'none', - sort_by: 'time', - sort_order: 'desc' - }); - - const fetchExecutions = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const result = await getExecutionsSummary(filters, grouping); - - if ('executions' in result) { - // PaginatedExecutions - setExecutions(result.executions); - setPagination({ - total_count: result.total_count || result.total || 0, - page: result.page, - page_size: result.page_size, - total_pages: result.total_pages, - has_next: result.has_next || false, - has_prev: result.has_prev || false - }); - } else { - // GroupedExecutions - flatten for now, we'll handle grouping in a separate component - const flatExecutions = result.groups?.flatMap(group => group.executions) || []; - setExecutions(flatExecutions); - setPagination({ - total_count: result.total_count || 0, - page: result.page, - page_size: result.page_size, - total_pages: result.total_pages, - has_next: result.has_next || false, - has_prev: result.has_prev || false - }); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch executions'); - } finally { - setLoading(false); - } - }, [filters, grouping]); - - const fetchStats = useCallback(async () => { - if (!showStats) return; - - try { - const statsData = await getExecutionStats(filters); - setStats(statsData); - } catch (err) { - console.error('Failed to fetch execution stats:', err); - } - }, [filters, showStats]); - - useEffect(() => { - fetchExecutions(); - fetchStats(); - }, [fetchExecutions, fetchStats]); - - // Set up real-time updates with error handling - useEffect(() => { - let eventSource: EventSource | null = null; - - try { - eventSource = streamExecutionEvents(); - - eventSource.onmessage = (event) => { - try { - const executionEvent = JSON.parse(event.data); - - // Update executions list based on event type - setExecutions(prev => { - if (!executionEvent.execution) return prev; - - const executionId = executionEvent.execution.execution_id || executionEvent.execution.id?.toString(); - const existingIndex = prev.findIndex(e => (e.execution_id || e.id?.toString()) === executionId); - - if (existingIndex >= 0) { - // Update existing execution - const updated = [...prev]; - updated[existingIndex] = executionEvent.execution; - return updated; - } else if (executionEvent.type === 'execution_started') { - // Add new execution to the beginning - return [executionEvent.execution, ...prev]; - } - - return prev; - }); - - // Refresh stats - fetchStats(); - } catch (err) { - console.error('Failed to parse execution event:', err); - } - }; - - eventSource.onerror = (error) => { - console.error('Execution events stream error:', error); - }; - } catch (err) { - console.error('Failed to setup execution events stream:', err); - } - - return () => { - if (eventSource) { - eventSource.close(); - } - }; - }, [fetchStats]); - - const handleSearch = async (term: string) => { - setSearchTerm(term); - if (term.trim()) { - try { - setLoading(true); - const result = await searchExecutions(term, filters); - setExecutions(result.executions); - setPagination({ - total_count: result.total_count || result.total || 0, - page: result.page, - page_size: result.page_size, - total_pages: result.total_pages, - has_next: result.has_next || false, - has_prev: result.has_prev || false - }); - } catch (err) { - setError(err instanceof Error ? err.message : 'Search failed'); - } finally { - setLoading(false); - } - } else { - fetchExecutions(); - } - }; - - const handlePageChange = (newPage: number) => { - setFilters(prev => ({ ...prev, page: newPage })); - }; - - if (error) { - return ( - - -
-
Error Loading Executions
-
{error}
- -
-
-
- ); - } - - return ( -
- {/* Header */} -
-

{title}

-

{description}

-
- - {/* Stats Cards */} - {showStats && stats && ( - - - Execution Statistics - - - -
-
{stats.total_executions}
-
Total
-
-
-
- {stats.successful_executions} -
-
Successful
-
-
-
- {stats.failed_executions} -
-
Failed
-
-
-
- {stats.running_executions} -
-
Running
-
-
-
-
- )} - - {/* Search */} - {showFilters && ( - - - Search Executions - - -
- setSearchTerm(e.target.value)} - className="flex-1" - /> - - {searchTerm && ( - - )} -
-
-
- )} - - {/* Executions List */} - - -
-
- Recent Executions - - {pagination.total_count} total executions - {searchTerm && ` matching "${searchTerm}"`} - -
- - Page {pagination.page} of {pagination.total_pages} - -
-
- - {loading ? ( -
- {Array.from({ length: 5 }).map((_, i) => ( -
- - - - {i < 4 && } -
- ))} -
- ) : executions.length === 0 ? ( -
-
No executions found
-
- {searchTerm ? 'Try adjusting your search terms' : 'Executions will appear here as they run'} -
-
- ) : ( -
- {executions.map((execution, index) => { - const executionId = execution.execution_id || execution.id?.toString() || 'Unknown'; - const startedAt = execution.started_at || execution.created_at; - return ( -
- -
-
-
-

{executionId}

- - {formatStatusLabel(execution.status)} - -
-
- {execution.workflow_id && ( -
Workflow: {execution.workflow_id}
- )} - {execution.session_id && ( -
Session: {execution.session_id}
- )} - {execution.agent_node_id && ( -
Agent: {execution.agent_node_id}
- )} -
Started: {startedAt ? (() => { - const date = new Date(startedAt); - return !isNaN(date.getTime()) ? date.toLocaleString() : 'Invalid Date'; - })() : 'N/A'}
- {execution.completed_at && (() => { - const date = new Date(execution.completed_at); - return !isNaN(date.getTime()) ? ( -
Ended: {date.toLocaleString()}
- ) : ( -
Ended: Invalid Date
- ); - })()} - {execution.duration_ms && ( -
Duration: {execution.duration_ms}ms
- )} -
-
-
-
- {index < executions.length - 1 && } -
- ); - })} -
- )} - - {/* Pagination */} - {pagination.total_pages > 1 && ( -
-
- Showing {((pagination.page - 1) * pagination.page_size) + 1} to{' '} - {Math.min(pagination.page * pagination.page_size, pagination.total_count)} of{' '} - {pagination.total_count} executions -
-
- - -
-
- )} -
-
-
- ); -} diff --git a/control-plane/web/client/src/components/FilterTag.tsx b/control-plane/web/client/src/components/FilterTag.tsx deleted file mode 100644 index a79a3636..00000000 --- a/control-plane/web/client/src/components/FilterTag.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { X } from '@/components/ui/icon-bridge'; -import { cn } from '@/lib/utils'; -import { statusTone } from '@/lib/theme'; -import type { FilterTag as FilterTagType, FilterColor } from '../types/filters'; - -interface FilterTagProps { - tag: FilterTagType; - onRemove?: (tagId: string) => void; - className?: string; -} - -const colorVariants: Record = { - blue: [statusTone.info.bg, statusTone.info.fg, statusTone.info.border], - green: [statusTone.success.bg, statusTone.success.fg, statusTone.success.border], - orange: [statusTone.warning.bg, statusTone.warning.fg, statusTone.warning.border], - red: [statusTone.error.bg, statusTone.error.fg, statusTone.error.border], - gray: [statusTone.neutral.bg, statusTone.neutral.fg, statusTone.neutral.border], - purple: ["bg-bg-tertiary", "text-chart-2", "border border-border-tertiary"], - indigo: ["bg-bg-tertiary", "text-chart-3", "border border-border-tertiary"], - pink: ["bg-bg-tertiary", "text-chart-5", "border border-border-tertiary"], -}; - -export function FilterTag({ tag, onRemove, className }: FilterTagProps) { - const colorClass = colorVariants[tag.color] || colorVariants.gray; - - return ( -
- {tag.label} - {tag.removable && onRemove && ( - - )} -
- ); -} diff --git a/control-plane/web/client/src/components/QuickFilters.tsx b/control-plane/web/client/src/components/QuickFilters.tsx deleted file mode 100644 index aa34d315..00000000 --- a/control-plane/web/client/src/components/QuickFilters.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { TrashCan } from "@/components/ui/icon-bridge"; -import { cn } from "../lib/utils"; -import type { FilterTag as FilterTagType } from "../types/filters"; -import { createFilterTag } from "../utils/filterUtils"; -import { Button } from "./ui/button"; - -interface QuickFiltersProps { - tags: FilterTagType[]; - onTagsChange: (tags: FilterTagType[]) => void; - className?: string; -} - -const QUICK_FILTERS = [ - { type: "status", value: "running", label: "Running", color: "blue" }, - { type: "status", value: "succeeded", label: "Succeeded", color: "green" }, - { type: "status", value: "failed", label: "Failed", color: "red" }, - { type: "time", value: "last-24h", label: "Last 24h", color: "indigo" }, -] as const; - -export function QuickFilters({ - tags, - onTagsChange, - className, -}: QuickFiltersProps) { - const handleQuickFilterClick = ( - filterType: string, - filterValue: string, - label: string - ) => { - // Check if filter already exists - const existingIndex = tags.findIndex( - (tag) => tag.type === filterType && tag.value === filterValue - ); - - if (existingIndex >= 0) { - // Remove existing filter - const newTags = [...tags]; - newTags.splice(existingIndex, 1); - onTagsChange(newTags); - } else { - // Add new filter - const newTag = createFilterTag(filterType as any, filterValue, label); - onTagsChange([...tags, newTag]); - } - }; - - const handleClearAll = () => { - onTagsChange([]); - }; - - const isFilterActive = (filterType: string, filterValue: string) => { - return tags.some( - (tag) => tag.type === filterType && tag.value === filterValue - ); - }; - - return ( -
- {/* Quick filter buttons */} -
- {QUICK_FILTERS.map((filter) => { - const isActive = isFilterActive(filter.type, filter.value); - return ( - - ); - })} -
- - {/* Clear all button */} - {tags.length > 0 && ( - <> -
- - - )} -
- ); -} diff --git a/control-plane/web/client/src/components/SearchWithFilters.tsx b/control-plane/web/client/src/components/SearchWithFilters.tsx deleted file mode 100644 index 64b390a7..00000000 --- a/control-plane/web/client/src/components/SearchWithFilters.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Command } from 'cmdk'; -import { Search, ChevronDown } from "@/components/ui/icon-bridge"; -import { cn } from '../lib/utils'; -import { FilterTag } from './FilterTag'; -import type { FilterTag as FilterTagType, FilterSuggestion } from '../types/filters'; -import { FILTER_SUGGESTIONS } from '../types/filters'; -import { parseFilterInput, createFilterTag } from '../utils/filterUtils'; - -interface SearchWithFiltersProps { - tags: FilterTagType[]; - onTagsChange: (tags: FilterTagType[]) => void; - placeholder?: string; - className?: string; -} - -export function SearchWithFilters({ - tags, - onTagsChange, - placeholder = "Search executions or add filters like status:running, agent:support...", - className, -}: SearchWithFiltersProps) { - const [open, setOpen] = useState(false); - const [inputValue, setInputValue] = useState(''); - const [filteredSuggestions, setFilteredSuggestions] = useState([]); - const inputRef = useRef(null); - const containerRef = useRef(null); - - // Filter suggestions based on input - useEffect(() => { - if (!inputValue.trim()) { - setFilteredSuggestions(FILTER_SUGGESTIONS.slice(0, 12)); // Show top suggestions - return; - } - - const query = inputValue.toLowerCase(); - const filtered = FILTER_SUGGESTIONS.filter(suggestion => { - // Check if already applied - const isApplied = tags.some(tag => - tag.type === suggestion.type && tag.value === suggestion.value - ); - if (isApplied) return false; - - // Match against keywords, label, or description - return ( - suggestion.keywords.some(keyword => keyword.includes(query)) || - suggestion.label.toLowerCase().includes(query) || - suggestion.description?.toLowerCase().includes(query) || - suggestion.category.toLowerCase().includes(query) - ); - }).slice(0, 8); - - setFilteredSuggestions(filtered); - }, [inputValue, tags]); - - // Group suggestions by category - const groupedSuggestions = filteredSuggestions.reduce((acc, suggestion) => { - if (!acc[suggestion.category]) { - acc[suggestion.category] = []; - } - acc[suggestion.category].push(suggestion); - return acc; - }, {} as Record); - - const handleInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && inputValue.trim()) { - e.preventDefault(); - handleAddFromInput(); - } else if (e.key === 'Backspace' && !inputValue && tags.length > 0) { - // Remove last tag when backspacing on empty input - const newTags = [...tags]; - newTags.pop(); - onTagsChange(newTags); - } - }; - - const handleAddFromInput = () => { - if (!inputValue.trim()) return; - - const newTags = parseFilterInput(inputValue); - if (newTags.length > 0) { - onTagsChange([...tags, ...newTags]); - setInputValue(''); - setOpen(false); - } - }; - - const handleSuggestionSelect = (suggestion: FilterSuggestion) => { - const newTag = createFilterTag(suggestion.type, suggestion.value, suggestion.label); - onTagsChange([...tags, newTag]); - setInputValue(''); - setOpen(false); - inputRef.current?.focus(); - }; - - const handleRemoveTag = (tagId: string) => { - onTagsChange(tags.filter(tag => tag.id !== tagId)); - }; - - const handleInputFocus = () => { - setOpen(true); - }; - - const handleInputBlur = (e: React.FocusEvent) => { - // Don't close if clicking on a suggestion - if (containerRef.current?.contains(e.relatedTarget as Node)) { - return; - } - setTimeout(() => setOpen(false), 150); - }; - - return ( -
- - {/* Main search container */} -
- {/* Search icon */} -
- -
- - {/* Tags and input container */} -
- {/* Filter tags */} - {tags.map((tag) => ( - - ))} - - {/* Input field */} - -
- - {/* Command Hint & Dropdown indicator */} -
- {!inputValue && tags.length === 0 && ( - - K - - )} - -
-
- - {/* Suggestions dropdown */} - {open && ( - - {Object.keys(groupedSuggestions).length === 0 ? ( -
- {inputValue ? ( -
-
- No matching filters found -
-
- Press Enter to search for "{inputValue}" -
-
- ) : ( -
-
- Filter suggestions -
-
- Start typing to see available filters -
-
- )} -
- ) : ( -
- {Object.entries(groupedSuggestions).map(([category, suggestions], categoryIndex) => ( -
- {categoryIndex > 0 &&
} - - {/* Category header */} -
-

- {category} -

-
- - {/* Category items */} -
- {suggestions.map((suggestion) => ( - handleSuggestionSelect(suggestion)} - className="group flex cursor-pointer items-center justify-between rounded-lg px-3 py-3 text-sm transition-all duration-150 hover:bg-accent/80 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground" - > -
-
- - {suggestion.label} - -
- - {suggestion.type}:{suggestion.value} - -
-
- {suggestion.description && ( - - {suggestion.description} - - )} -
-
- ))} -
-
-
- ))} -
- )} - - )} - -
- ); -} diff --git a/control-plane/web/client/src/components/SuggestedFilters.tsx b/control-plane/web/client/src/components/SuggestedFilters.tsx deleted file mode 100644 index 898719c6..00000000 --- a/control-plane/web/client/src/components/SuggestedFilters.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { cn } from "../lib/utils"; -import type { - FilterSuggestion, - FilterTag as FilterTagType, -} from "../types/filters"; -import { FILTER_SUGGESTIONS } from "../types/filters"; -import { createFilterTag } from "../utils/filterUtils"; - -interface SuggestedFiltersProps { - tags: FilterTagType[]; - onTagsChange: (tags: FilterTagType[]) => void; - className?: string; -} - -// Curated list of most useful suggested filters -const SUGGESTED_FILTER_IDS = [ - "status-running", - "status-failed", - "time-last-24h", - "group-workflow", - "sort-time", - "status-completed", -]; - -export function SuggestedFilters({ - tags, - onTagsChange, - className, -}: SuggestedFiltersProps) { - // Get suggested filters that aren't already applied - const availableSuggestions = FILTER_SUGGESTIONS.filter((suggestion) => - SUGGESTED_FILTER_IDS.includes(suggestion.id) - ) - .filter((suggestion) => { - // Don't show if already applied - return !tags.some( - (tag) => tag.type === suggestion.type && tag.value === suggestion.value - ); - }) - .slice(0, 6); // Limit to 6 suggestions - - const handleSuggestionClick = (suggestion: FilterSuggestion) => { - const newTag = createFilterTag( - suggestion.type, - suggestion.value, - suggestion.label - ); - onTagsChange([...tags, newTag]); - }; - - if (availableSuggestions.length === 0) { - return null; - } - - return ( -
- - Quick filters: - - {availableSuggestions.map((suggestion) => ( - - ))} -
- ); -} diff --git a/control-plane/web/client/src/hooks/useFilterState.ts b/control-plane/web/client/src/hooks/useFilterState.ts deleted file mode 100644 index 0dd4f1e1..00000000 --- a/control-plane/web/client/src/hooks/useFilterState.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useState, useEffect, useCallback, useMemo } from 'react'; -import type { FilterTag } from '../types/filters'; -import type { ExecutionFilters, ExecutionGrouping } from '../types/executions'; -import { - convertTagsToApiFormat, - convertApiFormatToTags, - serializeFiltersToUrl, - deserializeFiltersFromUrl, -} from '../utils/filterUtils'; - -interface UseFilterStateOptions { - initialFilters?: Partial; - initialGrouping?: ExecutionGrouping; - syncWithUrl?: boolean; -} - -export interface UseFilterStateReturn { - tags: FilterTag[]; - filters: Partial; - grouping: ExecutionGrouping; - hasFilters: boolean; - updateTags: (tags: FilterTag[]) => void; - addTag: (tag: FilterTag) => void; - removeTag: (tagId: string) => void; - clearTags: () => void; -} - - -export function useFilterState({ - initialFilters = {}, - initialGrouping = { - group_by: 'none', - sort_by: 'time', - sort_order: 'desc', - }, - syncWithUrl = true, -}: UseFilterStateOptions = {}):UseFilterStateReturn { - // Initialize tags from URL or initial values - const [tags, setTags] = useState(() => { - if (syncWithUrl && typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search); - const urlTags = deserializeFiltersFromUrl(urlParams); - if (urlTags.length > 0) { - return urlTags; - } - } - return convertApiFormatToTags(initialFilters, initialGrouping); - }); - - // Convert tags to API format - memoized to prevent infinite re-renders - const { filters, grouping } = useMemo(() => { - return convertTagsToApiFormat(tags); - }, [tags]); - - // Update URL when tags change - useEffect(() => { - if (!syncWithUrl || typeof window === 'undefined') return; - - const urlString = serializeFiltersToUrl(tags); - const newUrl = urlString - ? `${window.location.pathname}?${urlString}` - : window.location.pathname; - - // Only update if URL actually changed - if (newUrl !== window.location.pathname + window.location.search) { - window.history.replaceState({}, '', newUrl); - } - }, [tags, syncWithUrl]); - - // Handle browser back/forward - useEffect(() => { - if (!syncWithUrl || typeof window === 'undefined') return; - - const handlePopState = () => { - const urlParams = new URLSearchParams(window.location.search); - const urlTags = deserializeFiltersFromUrl(urlParams); - setTags(urlTags); - }; - - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, [syncWithUrl]); - - const updateTags = useCallback((newTags: FilterTag[]) => { - setTags(newTags); - }, []); - - const addTag = useCallback((tag: FilterTag) => { - setTags(prev => [...prev, tag]); - }, []); - - const removeTag = useCallback((tagId: string) => { - setTags(prev => prev.filter(tag => tag.id !== tagId)); - }, []); - - const clearTags = useCallback(() => { - setTags([]); - }, []); - - const hasFilters = tags.length > 0; - - return { - tags, - filters, - grouping, - hasFilters, - updateTags, - addTag, - removeTag, - clearTags, - }; -} diff --git a/control-plane/web/client/src/types/filters.ts b/control-plane/web/client/src/types/filters.ts deleted file mode 100644 index c10930f5..00000000 --- a/control-plane/web/client/src/types/filters.ts +++ /dev/null @@ -1,214 +0,0 @@ -export interface FilterTag { - id: string; - type: FilterType; - value: string; - label: string; - color: FilterColor; - removable: boolean; -} - -export type FilterType = - | 'search' - | 'status' - | 'agent' - | 'workflow' - | 'session' - | 'actor' - | 'time' - | 'group-by' - | 'sort' - | 'order'; - -export type FilterColor = - | 'blue' - | 'green' - | 'purple' - | 'orange' - | 'red' - | 'gray' - | 'indigo' - | 'pink'; - -export interface FilterSuggestion { - id: string; - type: FilterType; - value: string; - label: string; - description?: string; - category: string; - keywords: string[]; -} - -export interface FilterState { - tags: FilterTag[]; - searchText: string; -} - -export const FILTER_COLORS: Record = { - search: 'gray', - status: 'blue', - agent: 'green', - workflow: 'purple', - session: 'orange', - actor: 'red', - time: 'indigo', - 'group-by': 'pink', - sort: 'gray', - order: 'gray', -}; - -export const FILTER_SUGGESTIONS: FilterSuggestion[] = [ - // Status filters - { - id: 'status-running', - type: 'status', - value: 'running', - label: 'Status: Running', - description: 'Show only running executions', - category: 'Status', - keywords: ['status', 'running', 'active', 'in-progress'], - }, - { - id: 'status-completed', - type: 'status', - value: 'completed', - label: 'Status: Completed', - description: 'Show only completed executions', - category: 'Status', - keywords: ['status', 'completed', 'finished', 'done', 'success'], - }, - { - id: 'status-failed', - type: 'status', - value: 'failed', - label: 'Status: Failed', - description: 'Show only failed executions', - category: 'Status', - keywords: ['status', 'failed', 'error', 'failed'], - }, - { - id: 'status-pending', - type: 'status', - value: 'pending', - label: 'Status: Pending', - description: 'Show only pending executions', - category: 'Status', - keywords: ['status', 'pending', 'waiting', 'queued'], - }, - - // Time filters - { - id: 'time-last-hour', - type: 'time', - value: 'last-hour', - label: 'Time: Last Hour', - description: 'Show executions from the last hour', - category: 'Time Range', - keywords: ['time', 'hour', 'recent', 'last'], - }, - { - id: 'time-last-24h', - type: 'time', - value: 'last-24h', - label: 'Time: Last 24 Hours', - description: 'Show executions from the last 24 hours', - category: 'Time Range', - keywords: ['time', '24h', 'day', 'today', 'recent'], - }, - { - id: 'time-last-week', - type: 'time', - value: 'last-week', - label: 'Time: Last Week', - description: 'Show executions from the last week', - category: 'Time Range', - keywords: ['time', 'week', 'last', '7 days'], - }, - - // Group by filters - { - id: 'group-workflow', - type: 'group-by', - value: 'workflow', - label: 'Group by: Workflow', - description: 'Group executions by workflow', - category: 'Grouping', - keywords: ['group', 'workflow', 'organize'], - }, - { - id: 'group-agent', - type: 'group-by', - value: 'agent', - label: 'Group by: Agent', - description: 'Group executions by agent', - category: 'Grouping', - keywords: ['group', 'agent', 'organize'], - }, - { - id: 'group-session', - type: 'group-by', - value: 'session', - label: 'Group by: Session', - description: 'Group executions by session', - category: 'Grouping', - keywords: ['group', 'session', 'organize'], - }, - { - id: 'group-status', - type: 'group-by', - value: 'status', - label: 'Group by: Status', - description: 'Group executions by status', - category: 'Grouping', - keywords: ['group', 'status', 'organize'], - }, - - // Sort filters - { - id: 'sort-time', - type: 'sort', - value: 'time', - label: 'Sort by: Time', - description: 'Sort executions by start time', - category: 'Sorting', - keywords: ['sort', 'time', 'date', 'chronological'], - }, - { - id: 'sort-duration', - type: 'sort', - value: 'duration', - label: 'Sort by: Duration', - description: 'Sort executions by duration', - category: 'Sorting', - keywords: ['sort', 'duration', 'time', 'length'], - }, - { - id: 'sort-status', - type: 'sort', - value: 'status', - label: 'Sort by: Status', - description: 'Sort executions by status', - category: 'Sorting', - keywords: ['sort', 'status', 'state'], - }, - - // Order filters - { - id: 'order-desc', - type: 'order', - value: 'desc', - label: 'Order: Descending', - description: 'Sort in descending order (newest first)', - category: 'Sorting', - keywords: ['order', 'desc', 'descending', 'newest', 'latest'], - }, - { - id: 'order-asc', - type: 'order', - value: 'asc', - label: 'Order: Ascending', - description: 'Sort in ascending order (oldest first)', - category: 'Sorting', - keywords: ['order', 'asc', 'ascending', 'oldest', 'earliest'], - }, -]; diff --git a/control-plane/web/client/src/utils/filterUtils.ts b/control-plane/web/client/src/utils/filterUtils.ts deleted file mode 100644 index ff7b4be6..00000000 --- a/control-plane/web/client/src/utils/filterUtils.ts +++ /dev/null @@ -1,311 +0,0 @@ -import type { ExecutionFilters, ExecutionGrouping } from "../types/executions"; -import type { FilterTag, FilterType } from "../types/filters"; -import { FILTER_COLORS } from "../types/filters"; - -export function generateFilterId(): string { - return Math.random().toString(36).substr(2, 9); -} - -export function createFilterTag( - type: FilterType, - value: string, - customLabel?: string -): FilterTag { - const label = customLabel || formatFilterLabel(type, value); - - return { - id: generateFilterId(), - type, - value, - label, - color: FILTER_COLORS[type], - removable: true, - }; -} - -export function formatFilterLabel(type: FilterType, value: string): string { - switch (type) { - case "search": - return value; - case "status": - return `Status: ${capitalizeFirst(value)}`; - case "agent": - return `Agent: ${value}`; - case "workflow": - return `Workflow: ${value}`; - case "session": - return `Session: ${value}`; - case "actor": - return `Actor: ${value}`; - case "time": - return `Time: ${formatTimeLabel(value)}`; - case "group-by": - return `Group by: ${capitalizeFirst(value)}`; - case "sort": - return `Sort: ${capitalizeFirst(value)}`; - case "order": - return value === "asc" ? "Ascending" : "Descending"; - default: - return `${capitalizeFirst(type)}: ${value}`; - } -} - -function formatTimeLabel(value: string): string { - switch (value) { - case "last-hour": - return "Last Hour"; - case "last-24h": - return "Last 24 Hours"; - case "last-week": - return "Last Week"; - default: - return capitalizeFirst(value); - } -} - -function capitalizeFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function convertTagsToApiFormat(tags: FilterTag[]): { - filters: Partial; - grouping: ExecutionGrouping; -} { - const filters: Partial = { - page: 1, - page_size: 20, - }; - - const grouping: ExecutionGrouping = { - group_by: "none", - sort_by: "time", - sort_order: "desc", - }; - - // Extract search text from search tags - const searchTags = tags.filter((tag) => tag.type === "search"); - if (searchTags.length > 0) { - filters.search = searchTags.map((tag) => tag.value).join(" "); - } - - tags.forEach((tag) => { - switch (tag.type) { - case "status": - filters.status = tag.value; - break; - case "agent": - filters.agent_node_id = tag.value; - break; - case "workflow": - filters.workflow_id = tag.value; - break; - case "session": - filters.session_id = tag.value; - break; - case "actor": - filters.actor_id = tag.value; - break; - case "time": - const timeRange = getTimeRange(tag.value); - if (timeRange.start_time) { - filters.start_time = timeRange.start_time; - } - if (timeRange.end_time) { - filters.end_time = timeRange.end_time; - } - break; - case "group-by": - grouping.group_by = tag.value as any; - break; - case "sort": - grouping.sort_by = tag.value as any; - break; - case "order": - grouping.sort_order = tag.value as any; - break; - } - }); - - return { filters, grouping }; -} - -function getTimeRange(timeValue: string): { - start_time?: string; - end_time?: string; -} { - const now = new Date(); - const nowISO = now.toISOString(); - - switch (timeValue) { - case "last-hour": - return { - start_time: new Date(now.getTime() - 3600000).toISOString(), - end_time: nowISO, - }; - case "last-24h": - return { - start_time: new Date(now.getTime() - 86400000).toISOString(), - end_time: nowISO, - }; - case "last-week": - return { - start_time: new Date(now.getTime() - 604800000).toISOString(), - end_time: nowISO, - }; - default: - return {}; - } -} - -export function convertApiFormatToTags( - filters: Partial, - grouping: ExecutionGrouping -): FilterTag[] { - const tags: FilterTag[] = []; - - // Add search tag - if (filters.search) { - tags.push(createFilterTag("search", filters.search)); - } - - // Add filter tags - if (filters.status) { - tags.push(createFilterTag("status", filters.status)); - } - - if (filters.agent_node_id) { - tags.push(createFilterTag("agent", filters.agent_node_id)); - } - - if (filters.workflow_id) { - tags.push(createFilterTag("workflow", filters.workflow_id)); - } - - if (filters.session_id) { - tags.push(createFilterTag("session", filters.session_id)); - } - - if (filters.actor_id) { - tags.push(createFilterTag("actor", filters.actor_id)); - } - - // Add time tag if time range is set - if (filters.start_time || filters.end_time) { - const timeLabel = getTimeLabelFromRange( - filters.start_time, - filters.end_time - ); - if (timeLabel) { - tags.push(createFilterTag("time", timeLabel)); - } - } - - // Add grouping tags - if (grouping.group_by && grouping.group_by !== "none") { - tags.push(createFilterTag("group-by", grouping.group_by)); - } - - if (grouping.sort_by && grouping.sort_by !== "time") { - tags.push(createFilterTag("sort", grouping.sort_by)); - } - - if (grouping.sort_order && grouping.sort_order !== "desc") { - tags.push(createFilterTag("order", grouping.sort_order)); - } - - return tags; -} - -function getTimeLabelFromRange( - startTime?: string, - endTime?: string -): string | null { - if (!startTime || !endTime) return null; - - const start = new Date(startTime); - const end = new Date(endTime); - const diffMs = end.getTime() - start.getTime(); - - // Check if it matches our predefined ranges (with some tolerance) - const tolerance = 60000; // 1 minute tolerance - - if (Math.abs(diffMs - 3600000) < tolerance) { - // 1 hour - return "last-hour"; - } else if (Math.abs(diffMs - 86400000) < tolerance) { - // 24 hours - return "last-24h"; - } else if (Math.abs(diffMs - 604800000) < tolerance) { - // 1 week - return "last-week"; - } - - return "custom"; -} - -export function parseFilterInput(input: string): FilterTag[] { - const tags: FilterTag[] = []; - const parts = input.split(/\s+/); - - for (const part of parts) { - if (part.includes(":")) { - const [type, value] = part.split(":", 2); - const filterType = type.toLowerCase() as FilterType; - - // Validate filter type - if (Object.keys(FILTER_COLORS).includes(filterType)) { - tags.push(createFilterTag(filterType, value)); - } else { - // If not a valid filter type, treat as search text - tags.push(createFilterTag("search", part)); - } - } else if (part.trim()) { - // Regular search text - tags.push(createFilterTag("search", part)); - } - } - - return tags; -} - -export function serializeFiltersToUrl(tags: FilterTag[]): string { - const params = new URLSearchParams(); - - tags.forEach((tag) => { - if (tag.type === "search") { - const existing = params.get("q") || ""; - params.set("q", existing ? `${existing} ${tag.value}` : tag.value); - } else { - params.append("filter", `${tag.type}:${tag.value}`); - } - }); - - return params.toString(); -} - -export function deserializeFiltersFromUrl( - urlParams: URLSearchParams -): FilterTag[] { - const tags: FilterTag[] = []; - - // Add search query - const query = urlParams.get("q"); - if (query) { - tags.push(createFilterTag("search", query)); - } - - // Add filter parameters - const filters = urlParams.getAll("filter"); - filters.forEach((filter) => { - if (filter.includes(":")) { - const [type, value] = filter.split(":", 2); - const filterType = type as FilterType; - - if (Object.keys(FILTER_COLORS).includes(filterType)) { - tags.push(createFilterTag(filterType, value)); - } - } - }); - - return tags; -}