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

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Table, Table,
Badge,
Button, Button,
Group, Stack,
Text,
ActionIcon,
Menu,
Paper,
Title, Title,
Alert, Group,
ActionIcon,
Badge,
Card,
Text,
Loader, Loader,
Center, Alert,
Tooltip, Tooltip,
Menu,
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconPlayerPlay, IconPlayerPlay,
@ -114,40 +114,50 @@ export const FunctionList: React.FC<FunctionListProps> = ({
} }
}; };
if (loading) { if (loading && functions.length === 0) {
return ( return (
<Center py={60}> <Stack gap="lg">
<Loader size="lg" /> <Group justify="space-between">
</Center> <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) { <Stack align="center" justify="center" h={200}>
return ( <Loader size="lg" />
<Alert icon={<IconExclamationCircle size={16} />} title="Error" color="red" mb="md"> <Text>Loading functions...</Text>
{error} </Stack>
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}> </Stack>
Retry
</Button>
</Alert>
); );
} }
return ( return (
<Paper shadow="xs" p="md"> <Stack gap="lg">
<Group justify="space-between" mb="md"> <Group justify="space-between">
<Title order={2}>Functions</Title> <Title order={2}>Functions</Title>
<Group> <Group>
<Button <Button
leftSection={<IconRefresh size={14} />} leftSection={<IconRefresh size={16} />}
variant="light"
onClick={loadFunctions} onClick={loadFunctions}
loading={loading} loading={loading}
> >
Refresh Refresh
</Button> </Button>
<Button <Button
leftSection={<IconPlus size={14} />} leftSection={<IconPlus size={16} />}
onClick={onCreateFunction} onClick={onCreateFunction}
> >
Create Function Create Function
@ -155,121 +165,129 @@ export const FunctionList: React.FC<FunctionListProps> = ({
</Group> </Group>
</Group> </Group>
{error && (
<Alert color="red" title="Error">
{error}
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}>
Retry
</Button>
</Alert>
)}
{functions.length === 0 ? ( {functions.length === 0 ? (
<Center py={40}> <Card shadow="sm" radius="md" withBorder p="xl">
<div style={{ textAlign: 'center' }}> <Stack align="center" gap="md">
<IconCode size={48} color="gray" /> <IconCode size={48} color="gray" />
<Text size="lg" mt="md" c="dimmed"> <div style={{ textAlign: 'center' }}>
No functions found <Text fw={500} mb="xs">
</Text> No functions found
<Text size="sm" c="dimmed" mb="md"> </Text>
Create your first serverless function to get started <Text size="sm" c="dimmed">
</Text> Create your first serverless function to get started
</Text>
</div>
<Button <Button
leftSection={<IconPlus size={14} />} leftSection={<IconPlus size={16} />}
onClick={onCreateFunction} onClick={onCreateFunction}
> >
Create Function Create Function
</Button> </Button>
</div> </Stack>
</Center> </Card>
) : ( ) : (
<Table striped> <Card shadow="sm" radius="md" withBorder>
<Table.Thead> <Table>
<Table.Tr> <Table.Thead>
<Table.Th>Name</Table.Th> <Table.Tr>
<Table.Th>Runtime</Table.Th> <Table.Th>Name</Table.Th>
<Table.Th>Image</Table.Th> <Table.Th>Runtime</Table.Th>
<Table.Th>Memory</Table.Th> <Table.Th>Image</Table.Th>
<Table.Th>Timeout</Table.Th> <Table.Th>Memory</Table.Th>
<Table.Th>Owner</Table.Th> <Table.Th>Timeout</Table.Th>
<Table.Th>Created</Table.Th> <Table.Th>Owner</Table.Th>
<Table.Th>Actions</Table.Th> <Table.Th>Created</Table.Th>
</Table.Tr> <Table.Th>Actions</Table.Th>
</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>
</Table.Tr> </Table.Tr>
))} </Table.Thead>
</Table.Tbody> <Table.Tbody>
</Table> {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 - ./migrations:/app/migrations:ro,Z
restart: unless-stopped restart: unless-stopped
frontend: # frontend:
build: # build:
context: ./kms-frontend # context: ./kms-frontend
dockerfile: Dockerfile # dockerfile: Dockerfile
container_name: kms-frontend # container_name: kms-frontend
ports: # ports:
- "3000:80" # - "3000:80"
networks: # networks:
- kms-network # - kms-network
restart: unless-stopped # restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data: