progress
This commit is contained in:
@ -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
|
||||||
|
|||||||
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 {
|
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';
|
||||||
|
|||||||
Reference in New Issue
Block a user