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 }}
+ />
+
+ } onClick={handleRefresh} loading={loading}>
+ Refresh
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {filteredExecutions.length === 0 ? (
+
+ No executions found
+
+ ) : (
+
+
+
+
+ Function
+ Status
+ Duration
+ Memory
+ Started
+ Actions
+
+
+
+ {filteredExecutions.map((execution) => (
+
+
+
+ {getFunctionName(execution.function_id)}
+ {execution.id.slice(0, 8)}...
+
+
+
+
+ {execution.status}
+
+
+
+
+
+ {formatDuration(execution.duration)}
+
+
+
+
+ {/* */}
+ {formatMemory(execution.memory_used)}
+
+
+
+ {formatDate(execution.created_at)}
+
+
+
+ handleViewLogs(execution)}
+ title="View Logs"
+ >
+
+
+ {(execution.status === 'running' || execution.status === 'pending') && (
+ handleCancelExecution(execution.id)}
+ title="Cancel Execution"
+ >
+
+
+ )}
+
+
+
+ ))}
+
+
+
+ )}
+
+ {totalPages > 1 && (
+
+
+
+ )}
+
+
+
+ {/* Logs Modal */}
+ setLogsModalOpened(false)}
+ title={`Execution Logs - ${selectedExecution?.id.slice(0, 8)}...`}
+ size="xl"
+ >
+
+ {selectedExecution && (
+
+
+
+ Function: {getFunctionName(selectedExecution.function_id)}
+ Status: {selectedExecution.status}
+
+
+ Duration: {formatDuration(selectedExecution.duration)}
+ Memory: {formatMemory(selectedExecution.memory_used)}
+
+
+
+ {selectedExecution.input && (
+
+ Input:
+ {JSON.stringify(selectedExecution.input, null, 2)}
+
+ )}
+
+ {selectedExecution.output && (
+
+ Output:
+ {JSON.stringify(selectedExecution.output, null, 2)}
+
+ )}
+
+ {selectedExecution.error && (
+
+ Error:
+ {selectedExecution.error}
+
+ )}
+
+ )}
+
+
+ Container Logs:
+ {logsLoading ? (
+
+
+
+ ) : executionLogs.length === 0 ? (
+ No logs available
+ ) : (
+
+
+ {executionLogs.join('\n')}
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default ExecutionList;
\ No newline at end of file
diff --git a/faas/web/src/types.ts b/faas/web/src/types.ts
index 074f181..9f9a5e4 100644
--- a/faas/web/src/types.ts
+++ b/faas/web/src/types.ts
@@ -16,14 +16,18 @@ export interface FunctionDefinition {
export interface FunctionExecution {
id: string;
- functionId: string;
- input: string;
- output?: string;
+ function_id: string;
+ input?: any;
+ output?: any;
error?: string;
- status: 'pending' | 'running' | 'completed' | 'failed';
- startTime: string;
- endTime?: string;
- duration?: number;
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'timeout' | 'canceled';
+ duration: number; // Duration in nanoseconds
+ memory_used: number;
+ container_id?: string;
+ executor_id: string;
+ created_at: string;
+ started_at?: string;
+ completed_at?: string;
}
export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20';