progress
This commit is contained in:
@ -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 <div>Executions view coming soon...</div>;
|
||||
return <ExecutionList />;
|
||||
default:
|
||||
return (
|
||||
<FunctionList
|
||||
|
||||
385
faas/web/src/components/ExecutionList.tsx
Normal file
385
faas/web/src/components/ExecutionList.tsx
Normal 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;
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user