sidebar fix

This commit is contained in:
2025-08-31 23:15:50 -04:00
parent 1430c97ae7
commit 23dfc171b8
10 changed files with 1836 additions and 171 deletions

View File

@ -0,0 +1,502 @@
import React, { useState, useEffect } from 'react';
import {
Paper,
TextInput,
Select,
NumberInput,
Button,
Group,
Stack,
Text,
Divider,
JsonInput,
Box,
Title,
ActionIcon,
ScrollArea,
} from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import Editor from '@monaco-editor/react';
import { functionApi, runtimeApi } from '../services/apiService';
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
interface FunctionSidebarProps {
opened: boolean;
onClose: () => void;
onSuccess: () => void;
editFunction?: FunctionDefinition;
}
export const FunctionSidebar: React.FC<FunctionSidebarProps> = ({
opened,
onClose,
onSuccess,
editFunction,
}) => {
const isEditing = !!editFunction;
const [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
// Default images for each runtime
const DEFAULT_IMAGES: Record<string, string> = {
'nodejs18': 'node:18-alpine',
'python3.9': 'python:3.9-alpine',
'go1.20': 'golang:1.20-alpine',
};
// Map runtime to Monaco editor language
const getEditorLanguage = (runtime: string): string => {
const languageMap: Record<string, string> = {
'nodejs18': 'javascript',
'python3.9': 'python',
'go1.20': 'go',
};
return languageMap[runtime] || 'javascript';
};
// Get default code template based on runtime
const getDefaultCode = (runtime: string): string => {
const templates: Record<string, string> = {
'nodejs18': `exports.handler = async (event, context) => {
console.log('Event:', JSON.stringify(event, null, 2));
return {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from Node.js!',
timestamp: new Date().toISOString()
})
};
};`,
'python3.9': `import json
from datetime import datetime
def handler(event, context):
print('Event:', json.dumps(event, indent=2))
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Hello from Python!',
'timestamp': datetime.now().isoformat()
})
}`,
'go1.20': `package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
)
type Event map[string]interface{}
type Response struct {
StatusCode int \`json:"statusCode"\`
Body string \`json:"body"\`
}
func Handler(ctx context.Context, event Event) (Response, error) {
eventJSON, _ := json.MarshalIndent(event, "", " ")
log.Printf("Event: %s", eventJSON)
body := map[string]interface{}{
"message": "Hello from Go!",
"timestamp": time.Now().Format(time.RFC3339),
}
bodyJSON, _ := json.Marshal(body)
return Response{
StatusCode: 200,
Body: string(bodyJSON),
}, nil
}`
};
return templates[runtime] || templates['nodejs18'];
};
useEffect(() => {
// Fetch available runtimes from backend
const fetchRuntimes = async () => {
try {
const response = await runtimeApi.getRuntimes();
setRuntimeOptions(response.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]);
// Update form values when editFunction changes
useEffect(() => {
if (editFunction) {
form.setValues({
name: editFunction.name || '',
app_id: editFunction.app_id || 'default',
runtime: editFunction.runtime || 'nodejs18' as RuntimeType,
image: editFunction.image || DEFAULT_IMAGES['nodejs18'] || '',
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',
},
});
} else {
// Reset to default values when not editing
form.setValues({
name: '',
app_id: 'default',
runtime: 'nodejs18' as RuntimeType,
image: DEFAULT_IMAGES['nodejs18'] || '',
handler: 'index.handler',
code: getDefaultCode('nodejs18'),
environment: '{}',
timeout: '30s',
memory: 128,
owner: {
type: 'team' as const,
name: 'FaaS Team',
owner: 'admin@example.com',
},
});
}
}, [editFunction, opened]);
const form = useForm({
initialValues: {
name: '',
app_id: 'default',
runtime: 'nodejs18' as RuntimeType,
image: DEFAULT_IMAGES['nodejs18'] || '',
handler: 'index.handler',
code: getDefaultCode('nodejs18'),
environment: '{}',
timeout: '30s',
memory: 128,
owner: {
type: 'team' as const,
name: 'FaaS Team',
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);
// If creating a new function and no code is set, provide default template
if (!isEditing && runtime && (!form.values.code || form.values.code.trim() === '')) {
form.setFieldValue('code', getDefaultCode(runtime));
}
};
const handleSubmit = async (values: typeof form.values) => {
console.log('handleSubmit called with values:', values);
console.log('Form validation errors:', form.errors);
console.log('Is form valid?', form.isValid());
// Check each field individually
const fieldNames = ['name', 'app_id', 'runtime', 'image', 'handler', 'timeout', 'memory'];
fieldNames.forEach(field => {
const error = form.validateField(field);
console.log(`Field ${field} error:`, error);
});
if (!form.isValid()) {
console.log('Form is not valid, validation errors:', form.errors);
return;
}
try {
// Parse environment variables JSON
let parsedEnvironment;
try {
parsedEnvironment = values.environment ? JSON.parse(values.environment) : undefined;
} catch (error) {
console.error('Error parsing environment variables:', 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 (
<Paper
style={{
position: 'fixed',
top: 60, // Below header
right: opened ? 0 : '-500px',
bottom: 0,
width: '500px',
zIndex: 1000,
borderRadius: 0,
display: 'flex',
flexDirection: 'column',
borderLeft: '1px solid var(--mantine-color-gray-3)',
backgroundColor: 'var(--mantine-color-body)',
transition: 'right 0.3s ease',
}}
>
{/* Header */}
<Group justify="space-between" p="md" style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}>
<Title order={4}>
{isEditing ? 'Edit Function' : 'Create Function'}
</Title>
<ActionIcon
variant="subtle"
color="gray"
onClick={onClose}
>
<IconX size={18} />
</ActionIcon>
</Group>
{/* Content */}
<ScrollArea style={{ flex: 1 }}>
<Box p="md">
<form onSubmit={(e) => {
console.log('Form submit event triggered');
console.log('Form values:', form.values);
console.log('Form errors:', form.errors);
console.log('Is form valid?', form.isValid());
const result = form.onSubmit(handleSubmit)(e);
console.log('Form onSubmit result:', result);
return result;
}}>
<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')}
/>
<Box>
<Text size="sm" fw={500} mb={5}>
Function Code
</Text>
<Box
style={{
border: '1px solid #dee2e6',
borderRadius: '4px',
overflow: 'hidden'
}}
>
<Editor
height="300px"
language={getEditorLanguage(form.values.runtime)}
value={form.values.code}
onChange={(value) => form.setFieldValue('code', value || '')}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 12,
lineNumbers: 'on',
roundedSelection: false,
scrollbar: {
vertical: 'visible',
horizontal: 'visible'
},
automaticLayout: true,
wordWrap: 'on',
tabSize: 2,
insertSpaces: true,
folding: true,
lineDecorationsWidth: 0,
lineNumbersMinChars: 3,
renderLineHighlight: 'line',
selectOnLineNumbers: true,
theme: 'vs-light'
}}
loading={<Text ta="center" p="xl">Loading editor...</Text>}
/>
</Box>
{form.errors.code && (
<Text size="xs" c="red" mt={5}>
{form.errors.code}
</Text>
)}
</Box>
<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"
}`}
validationError="Invalid JSON - please check your syntax"
formatOnBlur
autosize
minRows={3}
{...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>
</Box>
</ScrollArea>
</Paper>
);
};