Files
skybridge/faas/web/src/components/FunctionSidebar.tsx
2025-09-01 13:10:35 -04:00

256 lines
6.8 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Paper,
Stack,
Text,
Divider,
Box,
ScrollArea,
} from '@mantine/core';
import {
FormSidebar,
FormField
} from '@skybridge/web-components';
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 [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
const [codeContent, setCodeContent] = useState('');
// 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"
"time"
)
type Event map[string]interface{}
func Handler(ctx context.Context, event Event) (map[string]interface{}, error) {
fmt.Printf("Event: %+v\\n", event)
return map[string]interface{}{
"statusCode": 200,
"body": map[string]interface{}{
"message": "Hello from Go!",
"timestamp": time.Now().Format(time.RFC3339),
},
}, nil
}`
};
return templates[runtime] || templates['nodejs18'];
};
useEffect(() => {
loadRuntimeOptions();
if (editFunction) {
setCodeContent(editFunction.code || '');
}
}, [editFunction]);
const loadRuntimeOptions = async () => {
try {
const runtimes = await runtimeApi.listRuntimes();
const options = runtimes.map((runtime: RuntimeType) => ({
value: runtime.name,
label: `${runtime.name} (${runtime.version})`
}));
setRuntimeOptions(options);
} catch (error) {
console.error('Failed to load runtimes:', error);
// Fallback options
setRuntimeOptions([
{ value: 'nodejs18', label: 'Node.js 18' },
{ value: 'python3.9', label: 'Python 3.9' },
{ value: 'go1.20', label: 'Go 1.20' }
]);
}
};
const fields: FormField[] = [
{
name: 'name',
label: 'Function Name',
type: 'text',
required: true,
placeholder: 'my-function',
validation: { pattern: /^[a-z0-9-]+$/ },
},
{
name: 'description',
label: 'Description',
type: 'textarea',
required: false,
placeholder: 'Function description...',
},
{
name: 'runtime',
label: 'Runtime',
type: 'select',
required: true,
options: runtimeOptions,
defaultValue: 'nodejs18',
},
{
name: 'timeout',
label: 'Timeout (seconds)',
type: 'number',
required: true,
defaultValue: 30,
},
{
name: 'memory',
label: 'Memory (MB)',
type: 'number',
required: true,
defaultValue: 128,
},
{
name: 'environment_variables',
label: 'Environment Variables',
type: 'json',
required: false,
defaultValue: '{}',
},
];
const handleSubmit = async (values: any) => {
const submitData = {
...values,
code: codeContent,
docker_image: DEFAULT_IMAGES[values.runtime] || DEFAULT_IMAGES['nodejs18'],
};
if (editFunction) {
const updateRequest: UpdateFunctionRequest = {
description: submitData.description,
code: submitData.code,
timeout: submitData.timeout,
memory: submitData.memory,
environment_variables: submitData.environment_variables,
docker_image: submitData.docker_image,
};
await functionApi.updateFunction(editFunction.id, updateRequest);
} else {
const createRequest: CreateFunctionRequest = submitData;
await functionApi.createFunction(createRequest);
}
};
// Create a custom sidebar that includes the Monaco editor
return (
<Paper
style={{
position: 'fixed',
top: 60,
right: opened ? 0 : '-600px',
bottom: 0,
width: '600px',
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',
}}
>
<ScrollArea style={{ flex: 1 }}>
<Stack gap="md" p="md">
<FormSidebar
opened={true} // Always open since we're embedding it
onClose={() => {}} // Handled by parent
onSuccess={onSuccess}
title="Function"
editMode={!!editFunction}
editItem={editFunction}
fields={fields}
onSubmit={handleSubmit}
width={600}
style={{ position: 'relative', right: 'auto', top: 'auto', bottom: 'auto' }}
/>
<Divider />
<Box>
<Text fw={500} mb="sm">Code Editor</Text>
<Box h={300} style={{ border: '1px solid var(--mantine-color-gray-3)' }}>
<Editor
height="300px"
language={getEditorLanguage(editFunction?.runtime || 'nodejs18')}
value={codeContent}
onChange={(value) => setCodeContent(value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
}}
/>
</Box>
</Box>
</Stack>
</ScrollArea>
</Paper>
);
};