From d8f1fb3753a289efb1edc61a6e7bf2e725886371 Mon Sep 17 00:00:00 2001 From: Ryan Copley Date: Sun, 31 Aug 2025 00:56:09 -0400 Subject: [PATCH] progress --- faas/web/src/App.tsx | 3 +- faas/web/src/components/ExecutionList.tsx | 385 ++++++++++++++++++++++ faas/web/src/types.ts | 18 +- 3 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 faas/web/src/components/ExecutionList.tsx diff --git a/faas/web/src/App.tsx b/faas/web/src/App.tsx index d48a331..735f952 100644 --- a/faas/web/src/App.tsx +++ b/faas/web/src/App.tsx @@ -9,6 +9,7 @@ import { import { FunctionList } from './components/FunctionList'; import { FunctionForm } from './components/FunctionForm'; import { ExecutionModal } from './components/ExecutionModal'; +import ExecutionList from './components/ExecutionList'; import { FunctionDefinition } from './types'; const App: React.FC = () => { @@ -109,7 +110,7 @@ const App: React.FC = () => { /> ); case 'executions': - return
Executions view coming soon...
; + return ; default: return ( { + const [executions, setExecutions] = useState([]); + const [functions, setFunctions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedFunction, setSelectedFunction] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [selectedExecution, setSelectedExecution] = useState(null); + const [executionLogs, setExecutionLogs] = useState([]); + const [logsModalOpened, setLogsModalOpened] = useState(false); + const [logsLoading, setLogsLoading] = useState(false); + + const limit = 20; + + const loadExecutions = async () => { + try { + setLoading(true); + setError(null); + + const offset = (page - 1) * limit; + const functionId = selectedFunction || undefined; + + const response = await executionApi.list(functionId, limit, offset); + setExecutions(response.data.executions || []); + + // Calculate total pages (rough estimate) + const hasMore = response.data.executions?.length === limit; + setTotalPages(hasMore ? page + 1 : page); + + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to load executions'); + console.error('Error loading executions:', err); + } finally { + setLoading(false); + } + }; + + const loadFunctions = async () => { + try { + const response = await functionApi.list(); + setFunctions(response.data.functions || []); + } catch (err) { + console.error('Error loading functions:', err); + } + }; + + useEffect(() => { + loadFunctions(); + }, []); + + useEffect(() => { + loadExecutions(); + }, [page, selectedFunction]); + + const handleRefresh = () => { + loadExecutions(); + }; + + const handleViewLogs = async (execution: FunctionExecution) => { + setSelectedExecution(execution); + setLogsModalOpened(true); + setLogsLoading(true); + + try { + const response = await executionApi.getLogs(execution.id); + setExecutionLogs(response.data.logs || []); + } catch (err: any) { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to load logs', + color: 'red', + }); + setExecutionLogs([]); + } finally { + setLogsLoading(false); + } + }; + + const handleCancelExecution = async (executionId: string) => { + try { + await executionApi.cancel(executionId); + notifications.show({ + title: 'Success', + message: 'Execution cancelled successfully', + color: 'green', + }); + loadExecutions(); + } catch (err: any) { + notifications.show({ + title: 'Error', + message: err.response?.data?.error || 'Failed to cancel execution', + color: 'red', + }); + } + }; + + const getStatusColor = (status: FunctionExecution['status']) => { + switch (status) { + case 'completed': + return 'green'; + case 'failed': + return 'red'; + case 'running': + return 'blue'; + case 'pending': + return 'yellow'; + case 'timeout': + return 'orange'; + case 'canceled': + return 'gray'; + default: + return 'gray'; + } + }; + + const formatDuration = (nanoseconds: number) => { + if (!nanoseconds) return 'N/A'; + const milliseconds = nanoseconds / 1000000; + if (milliseconds < 1000) { + return `${milliseconds.toFixed(0)}ms`; + } + return `${(milliseconds / 1000).toFixed(2)}s`; + }; + + const formatMemory = (bytes: number) => { + if (!bytes) return 'N/A'; + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(0)}KB`; + } + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString(); + }; + + const getFunctionName = (functionId: string) => { + const func = functions.find(f => f.id === functionId); + return func?.name || 'Unknown Function'; + }; + + const filteredExecutions = executions.filter(execution => { + if (!searchTerm) return true; + const functionName = getFunctionName(execution.function_id); + return functionName.toLowerCase().includes(searchTerm.toLowerCase()) || + execution.id.toLowerCase().includes(searchTerm.toLowerCase()) || + execution.status.toLowerCase().includes(searchTerm.toLowerCase()); + }); + + if (loading && executions.length === 0) { + return ( + + + + + + ); + } + + return ( + + + + + + setSearchTerm(event.currentTarget.value)} + leftSection={} + style={{ width: 300 }} + /> +