This commit is contained in:
2025-08-31 01:06:02 -04:00
parent d8f1fb3753
commit 7a7ad1e44d
3 changed files with 320 additions and 278 deletions

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Group,
Text,
Badge,
Stack,
Table,
Button,
Stack,
Title,
Modal,
Select,
TextInput,
Pagination,
Container,
Alert,
Loader,
Code,
Group,
ActionIcon,
Modal,
Badge,
Card,
Text,
Loader,
Alert,
Code,
ScrollArea,
Flex,
} from '@mantine/core';
@ -183,138 +183,162 @@ const ExecutionList: React.FC = () => {
if (loading && executions.length === 0) {
return (
<Container size="xl">
<Flex justify="center" align="center" h={200}>
<Stack gap="lg">
<Group justify="space-between">
<Title order={2}>Function Executions</Title>
<Button
leftSection={<IconRefresh size={16} />}
onClick={handleRefresh}
loading={loading}
>
Refresh
</Button>
</Group>
<Stack align="center" justify="center" h={200}>
<Loader size="lg" />
</Flex>
</Container>
<Text>Loading executions...</Text>
</Stack>
</Stack>
);
}
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>
<Stack gap="lg">
<Group justify="space-between">
<Title order={2}>Function Executions</Title>
<Button
leftSection={<IconRefresh size={16} />}
onClick={handleRefresh}
loading={loading}
>
Refresh
</Button>
</Group>
{error && (
<Alert color="red" mb="md">
{error}
</Alert>
)}
<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>
{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>
)}
{error && (
<Alert color="red" title="Error">
{error}
</Alert>
)}
{totalPages > 1 && (
<Group justify="center" mt="md">
<Pagination
value={page}
onChange={setPage}
total={totalPages}
size="sm"
/>
</Group>
)}
{filteredExecutions.length === 0 ? (
<Card shadow="sm" radius="md" withBorder p="xl">
<Stack align="center" gap="md">
<IconClock size={48} color="gray" />
<div style={{ textAlign: 'center' }}>
<Text fw={500} mb="xs">
No executions found
</Text>
<Text size="sm" c="dimmed">
There are no function executions matching your current filters
</Text>
</div>
</Stack>
</Card>
</Stack>
) : (
<Card shadow="sm" radius="md" withBorder>
<Table>
<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 style={{ fontSize: '12px' }}>{execution.id.slice(0, 8)}...</Code>
</Stack>
</Table.Td>
<Table.Td>
<Badge color={getStatusColor(execution.status)} variant="light">
{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>
</Card>
)}
{totalPages > 1 && (
<Group justify="center">
<Pagination
value={page}
onChange={setPage}
total={totalPages}
size="sm"
/>
</Group>
)}
{/* Logs Modal */}
<Modal
@ -378,8 +402,8 @@ const ExecutionList: React.FC = () => {
</div>
</Stack>
</Modal>
</Container>
</Stack>
);
};
export default ExecutionList;
export default ExecutionList;

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react';
import {
Table,
Badge,
Button,
Group,
Text,
ActionIcon,
Menu,
Paper,
Stack,
Title,
Alert,
Group,
ActionIcon,
Badge,
Card,
Text,
Loader,
Center,
Alert,
Tooltip,
Menu,
} from '@mantine/core';
import {
IconPlayerPlay,
@ -114,40 +114,50 @@ export const FunctionList: React.FC<FunctionListProps> = ({
}
};
if (loading) {
if (loading && functions.length === 0) {
return (
<Center py={60}>
<Loader size="lg" />
</Center>
);
}
<Stack gap="lg">
<Group justify="space-between">
<Title order={2}>Functions</Title>
<Group>
<Button
leftSection={<IconRefresh size={16} />}
onClick={loadFunctions}
loading={loading}
>
Refresh
</Button>
<Button
leftSection={<IconPlus size={16} />}
onClick={onCreateFunction}
>
Create Function
</Button>
</Group>
</Group>
if (error) {
return (
<Alert icon={<IconExclamationCircle size={16} />} title="Error" color="red" mb="md">
{error}
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}>
Retry
</Button>
</Alert>
<Stack align="center" justify="center" h={200}>
<Loader size="lg" />
<Text>Loading functions...</Text>
</Stack>
</Stack>
);
}
return (
<Paper shadow="xs" p="md">
<Group justify="space-between" mb="md">
<Stack gap="lg">
<Group justify="space-between">
<Title order={2}>Functions</Title>
<Group>
<Button
leftSection={<IconRefresh size={14} />}
variant="light"
leftSection={<IconRefresh size={16} />}
onClick={loadFunctions}
loading={loading}
>
Refresh
</Button>
<Button
leftSection={<IconPlus size={14} />}
leftSection={<IconPlus size={16} />}
onClick={onCreateFunction}
>
Create Function
@ -155,121 +165,129 @@ export const FunctionList: React.FC<FunctionListProps> = ({
</Group>
</Group>
{error && (
<Alert color="red" title="Error">
{error}
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}>
Retry
</Button>
</Alert>
)}
{functions.length === 0 ? (
<Center py={40}>
<div style={{ textAlign: 'center' }}>
<Card shadow="sm" radius="md" withBorder p="xl">
<Stack align="center" gap="md">
<IconCode size={48} color="gray" />
<Text size="lg" mt="md" c="dimmed">
No functions found
</Text>
<Text size="sm" c="dimmed" mb="md">
Create your first serverless function to get started
</Text>
<div style={{ textAlign: 'center' }}>
<Text fw={500} mb="xs">
No functions found
</Text>
<Text size="sm" c="dimmed">
Create your first serverless function to get started
</Text>
</div>
<Button
leftSection={<IconPlus size={14} />}
leftSection={<IconPlus size={16} />}
onClick={onCreateFunction}
>
Create Function
</Button>
</div>
</Center>
</Stack>
</Card>
) : (
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Runtime</Table.Th>
<Table.Th>Image</Table.Th>
<Table.Th>Memory</Table.Th>
<Table.Th>Timeout</Table.Th>
<Table.Th>Owner</Table.Th>
<Table.Th>Created</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{functions.map((func) => (
<Table.Tr key={func.id}>
<Table.Td>
<Text fw={500}>{func.name}</Text>
<Text size="xs" c="dimmed">{func.handler}</Text>
</Table.Td>
<Table.Td>
<Badge color={getRuntimeColor(func.runtime)} variant="light">
{func.runtime}
</Badge>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
{func.image}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{func.memory} MB</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{func.timeout}</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{func.owner?.name || 'Unknown'}
{func.owner?.type && (
<Text size="xs" c="dimmed">({func.owner.type})</Text>
)}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{func.created_at ? new Date(func.created_at).toLocaleDateString() : 'N/A'}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<Tooltip label="Execute Function">
<ActionIcon
variant="light"
color="green"
size="sm"
onClick={() => onExecuteFunction(func)}
>
<IconPlayerPlay size={16} />
</ActionIcon>
</Tooltip>
<Menu position="bottom-end">
<Menu.Target>
<ActionIcon variant="light" size="sm">
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconSettings size={16} />}
onClick={() => onEditFunction(func)}
>
Edit
</Menu.Item>
<Menu.Item
leftSection={<IconRocket size={16} />}
onClick={() => handleDeploy(func)}
>
Deploy
</Menu.Item>
<Menu.Item
leftSection={<IconTrash size={16} />}
color="red"
onClick={() => handleDelete(func)}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Table.Td>
<Card shadow="sm" radius="md" withBorder>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Runtime</Table.Th>
<Table.Th>Image</Table.Th>
<Table.Th>Memory</Table.Th>
<Table.Th>Timeout</Table.Th>
<Table.Th>Owner</Table.Th>
<Table.Th>Created</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{functions.map((func) => (
<Table.Tr key={func.id}>
<Table.Td>
<Text fw={500}>{func.name}</Text>
<Text size="xs" c="dimmed">{func.description || ''}</Text>
</Table.Td>
<Table.Td>
<Badge color={getRuntimeColor(func.runtime)} variant="light">
{func.runtime}
</Badge>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">
{func.image || ''}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{func.memoryLimit || 'N/A'} MB</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{func.timeout || 'N/A'}</Text>
</Table.Td>
<Table.Td>
<Text size="sm">N/A</Text>
</Table.Td>
<Table.Td>
<Text size="sm">
{func.createdAt ? new Date(func.createdAt).toLocaleDateString() : 'N/A'}
</Text>
</Table.Td>
<Table.Td>
<Group gap="xs">
<Tooltip label="Execute Function">
<ActionIcon
variant="subtle"
color="green"
size="sm"
onClick={() => onExecuteFunction(func)}
>
<IconPlayerPlay size={16} />
</ActionIcon>
</Tooltip>
<Menu position="bottom-end">
<Menu.Target>
<ActionIcon variant="subtle" size="sm">
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconSettings size={16} />}
onClick={() => onEditFunction(func)}
>
Edit
</Menu.Item>
<Menu.Item
leftSection={<IconRocket size={16} />}
onClick={() => handleDeploy(func)}
>
Deploy
</Menu.Item>
<Menu.Item
leftSection={<IconTrash size={16} />}
color="red"
onClick={() => handleDelete(func)}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>
)}
</Paper>
</Stack>
);
};

View File

@ -75,16 +75,16 @@ services:
- ./migrations:/app/migrations:ro,Z
restart: unless-stopped
frontend:
build:
context: ./kms-frontend
dockerfile: Dockerfile
container_name: kms-frontend
ports:
- "3000:80"
networks:
- kms-network
restart: unless-stopped
# frontend:
# build:
# context: ./kms-frontend
# dockerfile: Dockerfile
# container_name: kms-frontend
# ports:
# - "3000:80"
# networks:
# - kms-network
# restart: unless-stopped
volumes:
postgres_data: