-
This commit is contained in:
@ -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,7 +402,7 @@ const ExecutionList: React.FC = () => {
|
||||
</div>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Container>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user