-
This commit is contained in:
323
faas/web/src/components/ExecutionModal.tsx
Normal file
323
faas/web/src/components/ExecutionModal.tsx
Normal file
@ -0,0 +1,323 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
Switch,
|
||||
Alert,
|
||||
Badge,
|
||||
Divider,
|
||||
Paper,
|
||||
JsonInput,
|
||||
Loader,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { IconPlay, IconPlayerStop, IconRefresh, IconCopy } from '@tabler/icons-react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { functionApi, executionApi } from '../services/api';
|
||||
import { FunctionDefinition, ExecuteFunctionResponse, FunctionExecution } from '../types';
|
||||
|
||||
interface ExecutionModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
function: FunctionDefinition | null;
|
||||
}
|
||||
|
||||
export const ExecutionModal: React.FC<ExecutionModalProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
function: func,
|
||||
}) => {
|
||||
const [input, setInput] = useState('{}');
|
||||
const [async, setAsync] = useState(false);
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [result, setResult] = useState<ExecuteFunctionResponse | null>(null);
|
||||
const [execution, setExecution] = useState<FunctionExecution | null>(null);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [loadingLogs, setLoadingLogs] = useState(false);
|
||||
|
||||
if (!func) return null;
|
||||
|
||||
const handleExecute = async () => {
|
||||
try {
|
||||
setExecuting(true);
|
||||
setResult(null);
|
||||
setExecution(null);
|
||||
setLogs([]);
|
||||
|
||||
let parsedInput;
|
||||
try {
|
||||
parsedInput = input.trim() ? JSON.parse(input) : undefined;
|
||||
} catch (e) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Invalid JSON input',
|
||||
color: 'red',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await functionApi.execute(func.id, {
|
||||
input: parsedInput,
|
||||
async,
|
||||
});
|
||||
|
||||
setResult(response.data);
|
||||
|
||||
if (async) {
|
||||
// Poll for execution status
|
||||
pollExecution(response.data.execution_id);
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: `Function ${async ? 'invoked' : 'executed'} successfully`,
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to execute function',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const pollExecution = async (executionId: string) => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await executionApi.getById(executionId);
|
||||
setExecution(response.data);
|
||||
|
||||
if (response.data.status === 'running' || response.data.status === 'pending') {
|
||||
setTimeout(poll, 2000); // Poll every 2 seconds
|
||||
} else {
|
||||
// Execution completed, get logs
|
||||
loadLogs(executionId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling execution:', error);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
const loadLogs = async (executionId: string) => {
|
||||
try {
|
||||
setLoadingLogs(true);
|
||||
const response = await executionApi.getLogs(executionId);
|
||||
setLogs(response.data.logs || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading logs:', error);
|
||||
} finally {
|
||||
setLoadingLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (result && async) {
|
||||
try {
|
||||
await executionApi.cancel(result.execution_id);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Execution canceled',
|
||||
color: 'orange',
|
||||
});
|
||||
// Refresh execution status
|
||||
if (result.execution_id) {
|
||||
const response = await executionApi.getById(result.execution_id);
|
||||
setExecution(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to cancel execution',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'green';
|
||||
case 'failed': return 'red';
|
||||
case 'running': return 'blue';
|
||||
case 'pending': return 'yellow';
|
||||
case 'canceled': return 'orange';
|
||||
case 'timeout': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
notifications.show({
|
||||
title: 'Copied',
|
||||
message: 'Copied to clipboard',
|
||||
color: 'green',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={`Execute Function: ${func.name}`}
|
||||
size="xl"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Function Info */}
|
||||
<Paper withBorder p="sm" bg="gray.0">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text size="sm" fw={500}>{func.name}</Text>
|
||||
<Text size="xs" c="dimmed">{func.runtime} • {func.memory}MB • {func.timeout}</Text>
|
||||
</div>
|
||||
<Badge variant="light">
|
||||
{func.runtime}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Input */}
|
||||
<JsonInput
|
||||
label="Function Input (JSON)"
|
||||
placeholder='{"key": "value"}'
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
minRows={4}
|
||||
maxRows={8}
|
||||
validationError="Invalid JSON"
|
||||
/>
|
||||
|
||||
{/* Execution Options */}
|
||||
<Group justify="space-between">
|
||||
<Switch
|
||||
label="Asynchronous execution"
|
||||
description="Execute in background"
|
||||
checked={async}
|
||||
onChange={(event) => setAsync(event.currentTarget.checked)}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<IconPlay size={16} />}
|
||||
onClick={handleExecute}
|
||||
loading={executing}
|
||||
disabled={executing}
|
||||
>
|
||||
{async ? 'Invoke' : 'Execute'}
|
||||
</Button>
|
||||
{result && async && execution?.status === 'running' && (
|
||||
<Button
|
||||
leftSection={<IconPlayerStop size={16} />}
|
||||
color="orange"
|
||||
variant="light"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<>
|
||||
<Divider label="Execution Result" labelPosition="center" />
|
||||
|
||||
<Paper withBorder p="md">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Text fw={500}>Execution #{result.execution_id.slice(0, 8)}...</Text>
|
||||
<Group gap="xs">
|
||||
<Badge color={getStatusColor(execution?.status || result.status)}>
|
||||
{execution?.status || result.status}
|
||||
</Badge>
|
||||
{result.duration && (
|
||||
<Badge variant="light">
|
||||
{result.duration}ms
|
||||
</Badge>
|
||||
)}
|
||||
{result.memory_used && (
|
||||
<Badge variant="light">
|
||||
{result.memory_used}MB
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Output */}
|
||||
{(result.output || execution?.output) && (
|
||||
<div>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text size="sm" fw={500}>Output:</Text>
|
||||
<Tooltip label="Copy output">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(JSON.stringify(result.output || execution?.output, null, 2))}
|
||||
>
|
||||
<IconCopy size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Paper bg="gray.1" p="sm">
|
||||
<Text size="sm" component="pre" style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(result.output || execution?.output, null, 2)}
|
||||
</Text>
|
||||
</Paper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{(result.error || execution?.error) && (
|
||||
<Alert color="red" mt="sm">
|
||||
<Text size="sm">{result.error || execution?.error}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Logs */}
|
||||
{async && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text size="sm" fw={500}>Logs:</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
leftSection={<IconRefresh size={12} />}
|
||||
onClick={() => result.execution_id && loadLogs(result.execution_id)}
|
||||
loading={loadingLogs}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
<Paper bg="gray.9" p="sm" mah={200} style={{ overflow: 'auto' }}>
|
||||
{loadingLogs ? (
|
||||
<Group justify="center">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : logs.length > 0 ? (
|
||||
<Text size="xs" c="white" component="pre">
|
||||
{logs.join('\n')}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="xs" c="gray.5">No logs available</Text>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
299
faas/web/src/components/FunctionForm.tsx
Normal file
299
faas/web/src/components/FunctionForm.tsx
Normal file
@ -0,0 +1,299 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
TextInput,
|
||||
Select,
|
||||
NumberInput,
|
||||
Textarea,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Paper,
|
||||
Divider,
|
||||
JsonInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { functionApi } from '../services/api';
|
||||
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
|
||||
|
||||
interface FunctionFormProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
editFunction?: FunctionDefinition;
|
||||
}
|
||||
|
||||
|
||||
export const FunctionForm: React.FC<FunctionFormProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
onSuccess,
|
||||
editFunction,
|
||||
}) => {
|
||||
const isEditing = !!editFunction;
|
||||
const [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available runtimes from backend
|
||||
const fetchRuntimes = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8083/api/runtimes');
|
||||
const data = await response.json();
|
||||
setRuntimeOptions(data.runtimes || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch runtimes:', error);
|
||||
// Fallback to default options
|
||||
setRuntimeOptions([
|
||||
{ value: 'nodejs18', label: 'Node.js 18.x' },
|
||||
{ value: 'python3.9', label: 'Python 3.9' },
|
||||
{ value: 'go1.20', label: 'Go 1.20' },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
if (opened) {
|
||||
fetchRuntimes();
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: editFunction?.name || '',
|
||||
app_id: editFunction?.app_id || 'default',
|
||||
runtime: editFunction?.runtime || 'nodejs18' as RuntimeType,
|
||||
image: editFunction?.image || '',
|
||||
handler: editFunction?.handler || 'index.handler',
|
||||
code: editFunction?.code || '',
|
||||
environment: editFunction?.environment ? JSON.stringify(editFunction.environment, null, 2) : '{}',
|
||||
timeout: editFunction?.timeout || '30s',
|
||||
memory: editFunction?.memory || 128,
|
||||
owner: {
|
||||
type: editFunction?.owner?.type || 'team' as const,
|
||||
name: editFunction?.owner?.name || 'FaaS Team',
|
||||
owner: editFunction?.owner?.owner || 'admin@example.com',
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
name: (value) => value.length < 1 ? 'Name is required' : null,
|
||||
app_id: (value) => value.length < 1 ? 'App ID is required' : null,
|
||||
runtime: (value) => !value ? 'Runtime is required' : null,
|
||||
image: (value) => value.length < 1 ? 'Image is required' : null,
|
||||
handler: (value) => value.length < 1 ? 'Handler is required' : null,
|
||||
timeout: (value) => !value.match(/^\d+[smh]$/) ? 'Timeout must be in format like 30s, 5m, 1h' : null,
|
||||
memory: (value) => value < 64 || value > 3008 ? 'Memory must be between 64 and 3008 MB' : null,
|
||||
},
|
||||
});
|
||||
|
||||
const handleRuntimeChange = (runtime: string | null) => {
|
||||
// if (runtime && DEFAULT_IMAGES[runtime]) {
|
||||
// form.setFieldValue('image', DEFAULT_IMAGES[runtime]);
|
||||
// }
|
||||
form.setFieldValue('runtime', runtime as RuntimeType);
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
try {
|
||||
// Parse environment variables JSON
|
||||
let parsedEnvironment;
|
||||
try {
|
||||
parsedEnvironment = values.environment ? JSON.parse(values.environment) : undefined;
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Invalid JSON in environment variables',
|
||||
color: 'red',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isEditing && editFunction) {
|
||||
const updateData: UpdateFunctionRequest = {
|
||||
name: values.name,
|
||||
runtime: values.runtime,
|
||||
image: values.image,
|
||||
handler: values.handler,
|
||||
code: values.code || undefined,
|
||||
environment: parsedEnvironment,
|
||||
timeout: values.timeout,
|
||||
memory: values.memory,
|
||||
owner: values.owner,
|
||||
};
|
||||
await functionApi.update(editFunction.id, updateData);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Function updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
const createData: CreateFunctionRequest = {
|
||||
name: values.name,
|
||||
app_id: values.app_id,
|
||||
runtime: values.runtime,
|
||||
image: values.image,
|
||||
handler: values.handler,
|
||||
code: values.code || undefined,
|
||||
environment: parsedEnvironment,
|
||||
timeout: values.timeout,
|
||||
memory: values.memory,
|
||||
owner: values.owner,
|
||||
};
|
||||
await functionApi.create(createData);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Function created successfully',
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
onSuccess();
|
||||
onClose();
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error('Error saving function:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: `Failed to ${isEditing ? 'update' : 'create'} function`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={isEditing ? 'Edit Function' : 'Create Function'}
|
||||
size="lg"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="Function Name"
|
||||
placeholder="my-function"
|
||||
required
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="App ID"
|
||||
placeholder="my-app"
|
||||
required
|
||||
disabled={isEditing}
|
||||
{...form.getInputProps('app_id')}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<Select
|
||||
label="Runtime"
|
||||
placeholder="Select runtime"
|
||||
required
|
||||
data={runtimeOptions}
|
||||
{...form.getInputProps('runtime')}
|
||||
onChange={handleRuntimeChange}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Memory (MB)"
|
||||
placeholder="128"
|
||||
required
|
||||
min={64}
|
||||
max={3008}
|
||||
{...form.getInputProps('memory')}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="Timeout"
|
||||
placeholder="30s"
|
||||
required
|
||||
{...form.getInputProps('timeout')}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<TextInput
|
||||
label="Handler"
|
||||
description="The entry point for your function (e.g., 'index.handler' means handler function in index file)"
|
||||
placeholder="index.handler"
|
||||
required
|
||||
{...form.getInputProps('handler')}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Function Code"
|
||||
rows={16}
|
||||
resize={"vertical"}
|
||||
placeholder={`// Node.js example:
|
||||
exports.handler = async (event, context) => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({ message: 'Hello World!' })
|
||||
};
|
||||
};
|
||||
|
||||
// Python example:
|
||||
def handler(event, context):
|
||||
return {
|
||||
'statusCode': 200,
|
||||
'body': json.dumps({'message': 'Hello World!'})
|
||||
}`}
|
||||
minRows={16}
|
||||
{...form.getInputProps('code')}
|
||||
/>
|
||||
|
||||
<JsonInput
|
||||
label="Environment Variables"
|
||||
description="JSON object with key-value pairs that will be available in your function runtime"
|
||||
placeholder={`{
|
||||
"NODE_ENV": "production",
|
||||
"API_URL": "https://api.example.com",
|
||||
"DATABASE_HOST": "db.example.com",
|
||||
"LOG_LEVEL": "info"
|
||||
}`}
|
||||
validationError="Invalid JSON - please check your syntax"
|
||||
formatOnBlur
|
||||
autosize
|
||||
minRows={4}
|
||||
{...form.getInputProps('environment')}
|
||||
/>
|
||||
|
||||
<Paper withBorder p="md" bg="gray.0">
|
||||
<Text size="sm" fw={500} mb="xs">Owner Information</Text>
|
||||
<Group grow>
|
||||
<Select
|
||||
label="Owner Type"
|
||||
data={[
|
||||
{ value: 'individual', label: 'Individual' },
|
||||
{ value: 'team', label: 'Team' },
|
||||
]}
|
||||
{...form.getInputProps('owner.type')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Owner Name"
|
||||
placeholder="Team Name"
|
||||
{...form.getInputProps('owner.name')}
|
||||
/>
|
||||
</Group>
|
||||
<TextInput
|
||||
label="Owner Email"
|
||||
placeholder="owner@example.com"
|
||||
mt="xs"
|
||||
{...form.getInputProps('owner.owner')}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button variant="light" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{isEditing ? 'Update' : 'Create'} Function
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
271
faas/web/src/components/FunctionList.tsx
Normal file
271
faas/web/src/components/FunctionList.tsx
Normal file
@ -0,0 +1,271 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Badge,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
Paper,
|
||||
Title,
|
||||
Alert,
|
||||
Loader,
|
||||
Center,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconPlay,
|
||||
IconSettings,
|
||||
IconTrash,
|
||||
IconRocket,
|
||||
IconCode,
|
||||
IconDots,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconExclamationCircle,
|
||||
} from '@tabler/icons-react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { functionApi } from '../services/api';
|
||||
import { FunctionDefinition } from '../types';
|
||||
|
||||
interface FunctionListProps {
|
||||
onCreateFunction: () => void;
|
||||
onEditFunction: (func: FunctionDefinition) => void;
|
||||
onExecuteFunction: (func: FunctionDefinition) => void;
|
||||
}
|
||||
|
||||
export const FunctionList: React.FC<FunctionListProps> = ({
|
||||
onCreateFunction,
|
||||
onEditFunction,
|
||||
onExecuteFunction,
|
||||
}) => {
|
||||
const [functions, setFunctions] = useState<FunctionDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadFunctions = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await functionApi.list();
|
||||
setFunctions(response.data.functions || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load functions:', err);
|
||||
setError('Failed to load functions');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadFunctions();
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (func: FunctionDefinition) => {
|
||||
if (!confirm(`Are you sure you want to delete function "${func.name}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await functionApi.delete(func.id);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: `Function "${func.name}" deleted successfully`,
|
||||
color: 'green',
|
||||
});
|
||||
loadFunctions();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete function:', err);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: `Failed to delete function "${func.name}"`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeploy = async (func: FunctionDefinition) => {
|
||||
try {
|
||||
await functionApi.deploy(func.id);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: `Function "${func.name}" deployed successfully`,
|
||||
color: 'green',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to deploy function:', err);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: `Failed to deploy function "${func.name}"`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getRuntimeColor = (runtime: string) => {
|
||||
switch (runtime) {
|
||||
case 'nodejs18': return 'green';
|
||||
case 'python3.9': return 'blue';
|
||||
case 'go1.20': return 'cyan';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Center py={60}>
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper shadow="xs" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2}>Functions</Title>
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<IconRefresh size={14} />}
|
||||
variant="light"
|
||||
onClick={loadFunctions}
|
||||
loading={loading}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={onCreateFunction}
|
||||
>
|
||||
Create Function
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{functions.length === 0 ? (
|
||||
<Center py={40}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<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>
|
||||
<Button
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={onCreateFunction}
|
||||
>
|
||||
Create Function
|
||||
</Button>
|
||||
</div>
|
||||
</Center>
|
||||
) : (
|
||||
<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}
|
||||
<Text size="xs" c="dimmed">({func.owner.type})</Text>
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">
|
||||
{new Date(func.created_at).toLocaleDateString()}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Execute Function">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => onExecuteFunction(func)}
|
||||
>
|
||||
<IconPlay 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.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user