This commit is contained in:
2025-08-31 00:56:09 -04:00
parent 279bbd3dcc
commit d8f1fb3753
3 changed files with 398 additions and 8 deletions

View File

@ -9,6 +9,7 @@ import {
import { FunctionList } from './components/FunctionList'; import { FunctionList } from './components/FunctionList';
import { FunctionForm } from './components/FunctionForm'; import { FunctionForm } from './components/FunctionForm';
import { ExecutionModal } from './components/ExecutionModal'; import { ExecutionModal } from './components/ExecutionModal';
import ExecutionList from './components/ExecutionList';
import { FunctionDefinition } from './types'; import { FunctionDefinition } from './types';
const App: React.FC = () => { const App: React.FC = () => {
@ -109,7 +110,7 @@ const App: React.FC = () => {
/> />
); );
case 'executions': case 'executions':
return <div>Executions view coming soon...</div>; return <ExecutionList />;
default: default:
return ( return (
<FunctionList <FunctionList

View File

@ -0,0 +1,385 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Group,
Text,
Badge,
Stack,
Table,
Button,
Select,
TextInput,
Pagination,
Container,
Alert,
Loader,
Code,
ActionIcon,
Modal,
ScrollArea,
Flex,
} from '@mantine/core';
import {
IconRefresh,
IconEye,
IconX,
IconSearch,
IconClock,
} from '@tabler/icons-react';
import { executionApi, functionApi } from '../services/apiService';
import { FunctionExecution, FunctionDefinition } from '../types';
import { notifications } from '@mantine/notifications';
const ExecutionList: React.FC = () => {
const [executions, setExecutions] = useState<FunctionExecution[]>([]);
const [functions, setFunctions] = useState<FunctionDefinition[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedFunction, setSelectedFunction] = useState<string>('');
const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedExecution, setSelectedExecution] = useState<FunctionExecution | null>(null);
const [executionLogs, setExecutionLogs] = useState<string[]>([]);
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 (
<Container size="xl">
<Flex justify="center" align="center" h={200}>
<Loader size="lg" />
</Flex>
</Container>
);
}
return (
<Container size="xl">
<Stack gap="md">
<Card>
<Group justify="space-between" mb="md">
<Group>
<TextInput
placeholder="Search executions..."
value={searchTerm}
onChange={(event) => setSearchTerm(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
style={{ width: 300 }}
/>
<Select
placeholder="All Functions"
data={functions.map(f => ({ value: f.id, label: f.name }))}
value={selectedFunction}
onChange={(value) => {
setSelectedFunction(value || '');
setPage(1);
}}
clearable
style={{ width: 200 }}
/>
</Group>
<Button leftSection={<IconRefresh size={16} />} onClick={handleRefresh} loading={loading}>
Refresh
</Button>
</Group>
{error && (
<Alert color="red" mb="md">
{error}
</Alert>
)}
{filteredExecutions.length === 0 ? (
<Text c="dimmed" ta="center" py="xl">
No executions found
</Text>
) : (
<ScrollArea>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Function</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Duration</Table.Th>
<Table.Th>Memory</Table.Th>
<Table.Th>Started</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filteredExecutions.map((execution) => (
<Table.Tr key={execution.id}>
<Table.Td>
<Stack gap={2}>
<Text fw={500}>{getFunctionName(execution.function_id)}</Text>
<Code size="xs">{execution.id.slice(0, 8)}...</Code>
</Stack>
</Table.Td>
<Table.Td>
<Badge color={getStatusColor(execution.status)} variant="filled">
{execution.status}
</Badge>
</Table.Td>
<Table.Td>
<Group gap="xs">
<IconClock size={14} />
<Text size="sm">{formatDuration(execution.duration)}</Text>
</Group>
</Table.Td>
<Table.Td>
<Group gap="xs">
{/* <IconMemory size={14} /> */}
<Text size="sm">{formatMemory(execution.memory_used)}</Text>
</Group>
</Table.Td>
<Table.Td>
<Text size="sm">{formatDate(execution.created_at)}</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon
size="sm"
variant="subtle"
onClick={() => handleViewLogs(execution)}
title="View Logs"
>
<IconEye size={16} />
</ActionIcon>
{(execution.status === 'running' || execution.status === 'pending') && (
<ActionIcon
size="sm"
variant="subtle"
color="red"
onClick={() => handleCancelExecution(execution.id)}
title="Cancel Execution"
>
<IconX size={16} />
</ActionIcon>
)}
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea>
)}
{totalPages > 1 && (
<Group justify="center" mt="md">
<Pagination
value={page}
onChange={setPage}
total={totalPages}
size="sm"
/>
</Group>
)}
</Card>
</Stack>
{/* Logs Modal */}
<Modal
opened={logsModalOpened}
onClose={() => setLogsModalOpened(false)}
title={`Execution Logs - ${selectedExecution?.id.slice(0, 8)}...`}
size="xl"
>
<Stack gap="md">
{selectedExecution && (
<Card>
<Group justify="space-between">
<Stack gap="xs">
<Text size="sm"><strong>Function:</strong> {getFunctionName(selectedExecution.function_id)}</Text>
<Text size="sm"><strong>Status:</strong> <Badge color={getStatusColor(selectedExecution.status)}>{selectedExecution.status}</Badge></Text>
</Stack>
<Stack gap="xs" align="flex-end">
<Text size="sm"><strong>Duration:</strong> {formatDuration(selectedExecution.duration)}</Text>
<Text size="sm"><strong>Memory:</strong> {formatMemory(selectedExecution.memory_used)}</Text>
</Stack>
</Group>
{selectedExecution.input && (
<div>
<Text size="sm" fw={500} mt="md" mb="xs">Input:</Text>
<Code block>{JSON.stringify(selectedExecution.input, null, 2)}</Code>
</div>
)}
{selectedExecution.output && (
<div>
<Text size="sm" fw={500} mt="md" mb="xs">Output:</Text>
<Code block>{JSON.stringify(selectedExecution.output, null, 2)}</Code>
</div>
)}
{selectedExecution.error && (
<div>
<Text size="sm" fw={500} mt="md" mb="xs">Error:</Text>
<Code block c="red">{selectedExecution.error}</Code>
</div>
)}
</Card>
)}
<div>
<Text size="sm" fw={500} mb="xs">Container Logs:</Text>
{logsLoading ? (
<Flex justify="center" p="md">
<Loader size="sm" />
</Flex>
) : executionLogs.length === 0 ? (
<Text c="dimmed" ta="center" p="md">No logs available</Text>
) : (
<ScrollArea h={300}>
<Code block>
{executionLogs.join('\n')}
</Code>
</ScrollArea>
)}
</div>
</Stack>
</Modal>
</Container>
);
};
export default ExecutionList;

View File

@ -16,14 +16,18 @@ export interface FunctionDefinition {
export interface FunctionExecution { export interface FunctionExecution {
id: string; id: string;
functionId: string; function_id: string;
input: string; input?: any;
output?: string; output?: any;
error?: string; error?: string;
status: 'pending' | 'running' | 'completed' | 'failed'; status: 'pending' | 'running' | 'completed' | 'failed' | 'timeout' | 'canceled';
startTime: string; duration: number; // Duration in nanoseconds
endTime?: string; memory_used: number;
duration?: number; container_id?: string;
executor_id: string;
created_at: string;
started_at?: string;
completed_at?: string;
} }
export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20'; export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20';