This commit is contained in:
2025-08-30 21:17:23 -04:00
parent f72c05bfd8
commit 2778cbc512
46 changed files with 11717 additions and 0 deletions

31
faas/web/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

60
faas/web/nginx.conf Normal file
View File

@ -0,0 +1,60 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# CORS headers for Module Federation
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
add_header Access-Control-Max-Age 1728000;
add_header Content-Type "text/plain; charset=utf-8";
add_header Content-Length 0;
return 204;
}
# Main location
location / {
try_files $uri $uri/ /index.html;
}
# Handle .js files with correct MIME type
location ~* \.js$ {
add_header Content-Type application/javascript;
try_files $uri =404;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

5981
faas/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
faas/web/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "faas-web",
"version": "1.0.0",
"private": true,
"dependencies": {
"@mantine/core": "^7.0.0",
"@mantine/hooks": "^7.0.0",
"@mantine/notifications": "^7.0.0",
"@mantine/form": "^7.0.0",
"@mantine/code-highlight": "^7.0.0",
"@tabler/icons-react": "^2.40.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-react": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"babel-loader": "^9.1.0",
"css-loader": "^6.8.0",
"html-webpack-plugin": "^5.5.0",
"style-loader": "^3.3.0",
"typescript": "^5.1.0",
"webpack": "^5.88.0",
"webpack-cli": "^5.1.0",
"webpack-dev-server": "^4.15.0"
},
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production",
"dev": "npm start"
}
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FaaS - Function as a Service</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

97
faas/web/src/App.tsx Normal file
View File

@ -0,0 +1,97 @@
import React, { useState } from 'react';
import { MantineProvider, AppShell, Title, Group, Badge, Text } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import { IconFunction } from '@tabler/icons-react';
import { FunctionList } from './components/FunctionList';
import { FunctionForm } from './components/FunctionForm';
import { ExecutionModal } from './components/ExecutionModal';
import { FunctionDefinition } from './types';
// Default Mantine theme
const theme = {
colorScheme: 'light',
};
const App: React.FC = () => {
const [functionFormOpened, setFunctionFormOpened] = useState(false);
const [executionModalOpened, setExecutionModalOpened] = useState(false);
const [editingFunction, setEditingFunction] = useState<FunctionDefinition | null>(null);
const [executingFunction, setExecutingFunction] = useState<FunctionDefinition | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const handleCreateFunction = () => {
setEditingFunction(null);
setFunctionFormOpened(true);
};
const handleEditFunction = (func: FunctionDefinition) => {
setEditingFunction(func);
setFunctionFormOpened(true);
};
const handleExecuteFunction = (func: FunctionDefinition) => {
setExecutingFunction(func);
setExecutionModalOpened(true);
};
const handleFormSuccess = () => {
setRefreshKey(prev => prev + 1);
};
const handleFormClose = () => {
setFunctionFormOpened(false);
setEditingFunction(null);
};
const handleExecutionClose = () => {
setExecutionModalOpened(false);
setExecutingFunction(null);
};
return (
<MantineProvider theme={theme}>
<Notifications />
<AppShell
header={{ height: 60 }}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<IconFunction size={24} />
<Title order={3}>Function as a Service</Title>
<Badge variant="light" color="blue">FaaS</Badge>
</Group>
<Text size="sm" c="dimmed">
Serverless Functions Platform
</Text>
</Group>
</AppShell.Header>
<AppShell.Main>
<FunctionList
key={refreshKey}
onCreateFunction={handleCreateFunction}
onEditFunction={handleEditFunction}
onExecuteFunction={handleExecuteFunction}
/>
<FunctionForm
opened={functionFormOpened}
onClose={handleFormClose}
onSuccess={handleFormSuccess}
editFunction={editingFunction}
/>
<ExecutionModal
opened={executionModalOpened}
onClose={handleExecutionClose}
function={executingFunction}
/>
</AppShell.Main>
</AppShell>
</MantineProvider>
);
};
export default App;

View 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>
);
};

View 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>
);
};

View 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>
);
};

9
faas/web/src/index.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(<App />);
}

View File

@ -0,0 +1,88 @@
import axios from 'axios';
import {
FunctionDefinition,
FunctionExecution,
CreateFunctionRequest,
UpdateFunctionRequest,
ExecuteFunctionRequest,
ExecuteFunctionResponse,
RuntimeInfo,
} from '../types';
const API_BASE_URL = process.env.NODE_ENV === 'production'
? '/api/faas/api'
: 'http://localhost:8083/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'X-User-Email': 'admin@example.com', // Mock auth header
},
});
// Add response interceptor for error handling
api.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
export const functionApi = {
// Function management
list: (appId?: string, limit = 50, offset = 0) =>
api.get<{ functions: FunctionDefinition[] }>('/functions', {
params: { app_id: appId, limit, offset },
}),
create: (data: CreateFunctionRequest) =>
api.post<FunctionDefinition>('/functions', data),
getById: (id: string) =>
api.get<FunctionDefinition>(`/functions/${id}`),
update: (id: string, data: UpdateFunctionRequest) =>
api.put<FunctionDefinition>(`/functions/${id}`, data),
delete: (id: string) =>
api.delete(`/functions/${id}`),
deploy: (id: string, force = false) =>
api.post(`/functions/${id}/deploy`, { force }),
// Function execution
execute: (id: string, data: Omit<ExecuteFunctionRequest, 'function_id'>) =>
api.post<ExecuteFunctionResponse>(`/functions/${id}/execute`, data),
invoke: (id: string, data?: { input?: any }) =>
api.post<ExecuteFunctionResponse>(`/functions/${id}/invoke`, data),
};
export const executionApi = {
// Execution management
list: (functionId?: string, limit = 50, offset = 0) =>
api.get<{ executions: FunctionExecution[] }>('/executions', {
params: { function_id: functionId, limit, offset },
}),
getById: (id: string) =>
api.get<FunctionExecution>(`/executions/${id}`),
cancel: (id: string) =>
api.delete(`/executions/${id}`),
getLogs: (id: string) =>
api.get<{ logs: string[] }>(`/executions/${id}/logs`),
getRunning: () =>
api.get<{ executions: FunctionExecution[]; count: number }>('/executions/running'),
};
export const healthApi = {
health: () => api.get('/health'),
ready: () => api.get('/ready'),
};
export default api;

View File

@ -0,0 +1,91 @@
export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20' | 'custom';
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'timeout' | 'canceled';
export type OwnerType = 'individual' | 'team';
export interface Owner {
type: OwnerType;
name: string;
owner: string;
}
export interface FunctionDefinition {
id: string;
name: string;
app_id: string;
runtime: RuntimeType;
image: string;
handler: string;
code?: string;
environment?: Record<string, string>;
timeout: string;
memory: number;
owner: Owner;
created_at: string;
updated_at: string;
}
export interface FunctionExecution {
id: string;
function_id: string;
status: ExecutionStatus;
input?: any;
output?: any;
error?: string;
duration?: number;
memory_used?: number;
container_id?: string;
executor_id: string;
created_at: string;
started_at?: string;
completed_at?: string;
}
export interface CreateFunctionRequest {
name: string;
app_id: string;
runtime: RuntimeType;
image: string;
handler: string;
code?: string;
environment?: Record<string, string>;
timeout: string;
memory: number;
owner: Owner;
}
export interface UpdateFunctionRequest {
name?: string;
runtime?: RuntimeType;
image?: string;
handler?: string;
code?: string;
environment?: Record<string, string>;
timeout?: string;
memory?: number;
owner?: Owner;
}
export interface ExecuteFunctionRequest {
function_id: string;
input?: any;
async?: boolean;
}
export interface ExecuteFunctionResponse {
execution_id: string;
status: ExecutionStatus;
output?: any;
error?: string;
duration?: number;
memory_used?: number;
}
export interface RuntimeInfo {
type: RuntimeType;
version: string;
available: boolean;
default_image: string;
description: string;
}

View File

@ -0,0 +1,88 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
mode: 'development',
entry: './src/index.tsx',
devServer: {
port: 3003,
historyApiFallback: true,
static: {
directory: './public',
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
publicPath: 'auto',
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-react',
'@babel/preset-typescript',
],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'faas',
filename: 'remoteEntry.js',
exposes: {
'./FaaSApp': './src/App',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: true,
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: true,
},
'@mantine/core': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
},
'@mantine/hooks': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
},
'@mantine/notifications': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
},
'@tabler/icons-react': {
singleton: true,
requiredVersion: '^2.40.0',
eager: true,
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};