This commit is contained in:
2025-08-31 00:33:57 -04:00
parent 9ec78ab51c
commit 279bbd3dcc
14 changed files with 598 additions and 76 deletions

View File

@ -10,11 +10,13 @@
"dependencies": {
"@mantine/code-highlight": "^7.0.0",
"@mantine/core": "^7.0.0",
"@mantine/dates": "^7.0.0",
"@mantine/form": "^7.0.0",
"@mantine/hooks": "^7.0.0",
"@mantine/notifications": "^7.0.0",
"@tabler/icons-react": "^2.40.0",
"axios": "^1.6.0",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0"
@ -736,6 +738,22 @@
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/dates": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.8.tgz",
"integrity": "sha512-KYog/YL83PnsMef7EZagpOFq9I2gfnK0eYSzC8YvV9Mb6t/x9InqRssGWVb0GIr+TNILpEkhKoGaSKZNy10Q1g==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"@mantine/core": "7.17.8",
"@mantine/hooks": "7.17.8",
"dayjs": ">=1.0.0",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/form": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.17.8.tgz",
@ -2052,6 +2070,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",

View File

@ -3,16 +3,18 @@
"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"
"@mantine/core": "^7.0.0",
"@mantine/hooks": "^7.0.0",
"@mantine/notifications": "^7.0.0",
"@mantine/dates": "^7.0.0",
"@mantine/form": "^7.0.0",
"@mantine/code-highlight": "^7.0.0",
"@tabler/icons-react": "^2.40.0",
"axios": "^1.11.0",
"dayjs": "^1.11.13"
},
"devDependencies": {
"@babel/core": "^7.22.0",
@ -32,6 +34,6 @@
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production",
"dev": "npm start"
"dev": "webpack serve --mode development"
}
}

View File

@ -1,26 +1,60 @@
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 { Box, Title, Tabs, Stack, ActionIcon, Group, Select } from '@mantine/core';
import {
IconFunction,
IconPlayerPlay,
IconStar,
IconStarFilled
} 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: any = {
colorScheme: 'light',
};
const App: React.FC = () => {
// Determine current route based on pathname
const getCurrentRoute = () => {
const path = window.location.pathname;
if (path.includes('/functions')) return 'functions';
if (path.includes('/executions')) return 'executions';
return 'functions';
};
const [currentRoute, setCurrentRoute] = useState(getCurrentRoute());
const [isFavorited, setIsFavorited] = useState(false);
const [selectedColor, setSelectedColor] = useState('');
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);
// Listen for URL changes (for when the shell navigates)
React.useEffect(() => {
const handlePopState = () => {
setCurrentRoute(getCurrentRoute());
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
const handleTabChange = (value: string | null) => {
if (value) {
// Use history.pushState to update URL and notify shell router
const basePath = '/app/faas';
const newPath = `${basePath}/${value}`;
// Update the URL and internal state
window.history.pushState(null, '', newPath);
setCurrentRoute(value);
// Dispatch a custom event so shell can respond if needed
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
}
};
const handleCreateFunction = () => {
console.log('handleCreateFunction called');
setEditingFunction(null);
setFunctionFormOpened(true);
};
@ -49,49 +83,121 @@ const App: React.FC = () => {
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>
const toggleFavorite = () => {
setIsFavorited(prev => !prev);
};
<AppShell.Main>
const colorOptions = [
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
{ value: 'green', label: 'Green' },
{ value: 'purple', label: 'Purple' },
{ value: 'orange', label: 'Orange' },
{ value: 'pink', label: 'Pink' },
{ value: 'teal', label: 'Teal' },
];
const renderContent = () => {
switch (currentRoute) {
case 'functions':
return (
<FunctionList
key={refreshKey}
onCreateFunction={handleCreateFunction}
onEditFunction={handleEditFunction}
onExecuteFunction={handleExecuteFunction}
/>
<FunctionForm
opened={functionFormOpened}
onClose={handleFormClose}
onSuccess={handleFormSuccess}
editFunction={editingFunction}
);
case 'executions':
return <div>Executions view coming soon...</div>;
default:
return (
<FunctionList
key={refreshKey}
onCreateFunction={handleCreateFunction}
onEditFunction={handleEditFunction}
onExecuteFunction={handleExecuteFunction}
/>
);
}
};
<ExecutionModal
opened={executionModalOpened}
onClose={handleExecutionClose}
function={executingFunction}
/>
</AppShell.Main>
</AppShell>
</MantineProvider>
return (
<Box w="100%" pos="relative">
<Stack gap="lg">
<div>
<Group justify="space-between" align="flex-start">
<div>
<Group align="center" gap="sm" mb="xs">
<Title order={1} size="h2">
Function as a Service
</Title>
<ActionIcon
variant="subtle"
size="lg"
onClick={toggleFavorite}
aria-label={isFavorited ? "Remove from favorites" : "Add to favorites"}
>
{isFavorited ? (
<IconStarFilled size={20} color="gold" />
) : (
<IconStar size={20} />
)}
</ActionIcon>
</Group>
</div>
{/* Right-side controls */}
<Group align="flex-start" gap="lg">
<div>
<Select
placeholder="Choose a color"
data={colorOptions}
value={selectedColor}
onChange={(value) => setSelectedColor(value || '')}
size="sm"
w={150}
/>
</div>
</Group>
</Group>
</div>
<Tabs value={currentRoute} onChange={handleTabChange}>
<Tabs.List>
<Tabs.Tab
value="functions"
leftSection={<IconFunction size={16} />}
>
Functions
</Tabs.Tab>
<Tabs.Tab
value="executions"
leftSection={<IconPlayerPlay size={16} />}
>
Executions
</Tabs.Tab>
</Tabs.List>
<Box pt="md">
{renderContent()}
</Box>
</Tabs>
</Stack>
<FunctionForm
opened={functionFormOpened}
onClose={handleFormClose}
onSuccess={handleFormSuccess}
editFunction={editingFunction}
/>
<ExecutionModal
opened={executionModalOpened}
onClose={handleExecutionClose}
function={executingFunction}
/>
</Box>
);
};

211
faas/web/src/App.tsx.backup Normal file
View File

@ -0,0 +1,211 @@
import React, { useState } from 'react';
import { Box, Title, Tabs, Stack, ActionIcon, Group, Select } from '@mantine/core';
import {
IconFunction,
IconPlay,
IconDashboard,
IconStar,
IconStarFilled
} from '@tabler/icons-react';
import { FunctionList } from './components/FunctionList';
import { FunctionForm } from './components/FunctionForm';
import { ExecutionModal } from './components/ExecutionModal';
import { FunctionDefinition } from './types';
const App: React.FC = () => {
// Determine current route based on pathname
const getCurrentRoute = () => {
const path = window.location.pathname;
if (path.includes('/functions')) return 'functions';
if (path.includes('/executions')) return 'executions';
return 'dashboard';
};
const [currentRoute, setCurrentRoute] = useState(getCurrentRoute());
const [isFavorited, setIsFavorited] = useState(false);
const [selectedColor, setSelectedColor] = useState('');
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);
// Listen for URL changes (for when the shell navigates)
React.useEffect(() => {
const handlePopState = () => {
setCurrentRoute(getCurrentRoute());
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
const handleTabChange = (value: string | null) => {
if (value) {
// Use history.pushState to update URL and notify shell router
const basePath = '/app/faas';
const newPath = value === 'dashboard' ? basePath : `${basePath}/${value}`;
// Update the URL and internal state
window.history.pushState(null, '', newPath);
setCurrentRoute(value);
// Dispatch a custom event so shell can respond if needed
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
}
};
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);
};
const toggleFavorite = () => {
setIsFavorited(prev => !prev);
};
const colorOptions = [
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
{ value: 'green', label: 'Green' },
{ value: 'purple', label: 'Purple' },
{ value: 'orange', label: 'Orange' },
{ value: 'pink', label: 'Pink' },
{ value: 'teal', label: 'Teal' },
];
const renderContent = () => {
switch (currentRoute) {
case 'functions':
return (
<FunctionList
key={refreshKey}
onCreateFunction={handleCreateFunction}
onEditFunction={handleEditFunction}
onExecuteFunction={handleExecuteFunction}
/>
);
case 'executions':
return <div>Executions view coming soon...</div>;
default:
return (
<FunctionList
key={refreshKey}
onCreateFunction={handleCreateFunction}
onEditFunction={handleEditFunction}
onExecuteFunction={handleExecuteFunction}
/>
);
}
};
return (
<Box w="100%" pos="relative">
<Stack gap="lg">
<div>
<Group justify="space-between" align="flex-start">
<div>
<Group align="center" gap="sm" mb="xs">
<Title order={1} size="h2">
Function as a Service
</Title>
<ActionIcon
variant="subtle"
size="lg"
onClick={toggleFavorite}
aria-label={isFavorited ? "Remove from favorites" : "Add to favorites"}
>
{isFavorited ? (
<IconStarFilled size={20} color="gold" />
) : (
<IconStar size={20} />
)}
</ActionIcon>
</Group>
</div>
{/* Right-side controls */}
<Group align="flex-start" gap="lg">
<div>
<Select
placeholder="Choose a color"
data={colorOptions}
value={selectedColor}
onChange={(value) => setSelectedColor(value || '')}
size="sm"
w={150}
/>
</div>
</Group>
</Group>
</div>
<Tabs value={currentRoute} onChange={handleTabChange}>
<Tabs.List>
<Tabs.Tab
value="dashboard"
leftSection={<IconDashboard size={16} />}
>
Dashboard
</Tabs.Tab>
<Tabs.Tab
value="functions"
leftSection={<IconFunction size={16} />}
>
Functions
</Tabs.Tab>
<Tabs.Tab
value="executions"
leftSection={<IconPlay size={16} />}
>
Executions
</Tabs.Tab>
</Tabs.List>
<Box pt="md">
{renderContent()}
</Box>
</Tabs>
</Stack>
<FunctionForm
opened={functionFormOpened}
onClose={handleFormClose}
onSuccess={handleFormSuccess}
editFunction={editingFunction}
/>
<ExecutionModal
opened={executionModalOpened}
onClose={handleExecutionClose}
function={executingFunction}
/>
</Box>
);
};
export default App;

View File

@ -18,7 +18,7 @@ import {
} from '@mantine/core';
import { IconPlayerPlay, IconPlayerStop, IconRefresh, IconCopy } from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
import { functionApi, executionApi } from '../services/api';
import { functionApi, executionApi } from '../services/apiService';
import { FunctionDefinition, ExecuteFunctionResponse, FunctionExecution } from '../types';
interface ExecutionModalProps {

View File

@ -15,7 +15,7 @@ import {
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { functionApi, runtimeApi } from '../services/api';
import { functionApi, runtimeApi } from '../services/apiService';
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
interface FunctionFormProps {

View File

@ -26,7 +26,7 @@ import {
IconExclamationCircle,
} from '@tabler/icons-react';
import { notifications } from '@mantine/notifications';
import { functionApi } from '../services/api';
import { functionApi } from '../services/apiService';
import { FunctionDefinition } from '../types';
interface FunctionListProps {

View File

@ -1,9 +1,21 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(<App />);
}
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<MantineProvider>
<Notifications />
<BrowserRouter>
<App />
</BrowserRouter>
</MantineProvider>
</React.StrictMode>
);

View File

@ -0,0 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<MantineProvider>
<Notifications />
<BrowserRouter>
<App />
</BrowserRouter>
</MantineProvider>
</React.StrictMode>
);

64
faas/web/src/types.ts Normal file
View File

@ -0,0 +1,64 @@
export interface FunctionDefinition {
id: string;
name: string;
description?: string;
runtime: RuntimeType;
code: string;
status: 'active' | 'inactive';
createdAt: string;
updatedAt: string;
tags?: string[];
timeout?: number;
memoryLimit?: number;
image?: string;
env_vars?: Record<string, string>;
}
export interface FunctionExecution {
id: string;
functionId: string;
input: string;
output?: string;
error?: string;
status: 'pending' | 'running' | 'completed' | 'failed';
startTime: string;
endTime?: string;
duration?: number;
}
export type RuntimeType = 'nodejs18' | 'python3.9' | 'go1.20';
export interface CreateFunctionRequest {
name: string;
description?: string;
runtime: RuntimeType;
code: string;
image?: string;
timeout?: number;
memory_limit?: number;
env_vars?: Record<string, string>;
tags?: string[];
}
export interface UpdateFunctionRequest extends Partial<CreateFunctionRequest> {}
export interface ExecuteFunctionRequest {
function_id: string;
input?: any;
async?: boolean;
}
export interface ExecuteFunctionResponse {
execution_id: string;
output?: any;
error?: string;
status: 'pending' | 'running' | 'completed' | 'failed';
duration?: number;
}
export interface RuntimeInfo {
runtime: RuntimeType;
image: string;
version: string;
available: boolean;
}

View File

@ -1,22 +1,16 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const webpack = require('webpack');
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'],
},
@ -46,43 +40,46 @@ module.exports = {
name: 'faas',
filename: 'remoteEntry.js',
exposes: {
'./FaaSApp': './src/App',
'./App': './src/App.tsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: true,
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: true,
eager: false,
},
'@mantine/core': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
eager: false,
},
'@mantine/hooks': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
eager: false,
},
'@mantine/notifications': {
singleton: true,
requiredVersion: '^7.0.0',
eager: true,
eager: false,
},
'@tabler/icons-react': {
singleton: true,
requiredVersion: '^2.40.0',
eager: true,
eager: false,
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new webpack.DefinePlugin({
'process.env': JSON.stringify(process.env),
}),
],
};

View File

@ -0,0 +1,85 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: './src/index.tsx',
devServer: {
port: 3003,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
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: {
'./App': './src/App.tsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
'@mantine/core': {
singleton: true,
requiredVersion: '^7.0.0',
eager: false,
},
'@mantine/hooks': {
singleton: true,
requiredVersion: '^7.0.0',
eager: false,
},
'@mantine/notifications': {
singleton: true,
requiredVersion: '^7.0.0',
eager: false,
},
'@tabler/icons-react': {
singleton: true,
requiredVersion: '^2.40.0',
eager: false,
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new webpack.DefinePlugin({
'process.env': JSON.stringify(process.env),
}),
],
};

View File

@ -6,7 +6,7 @@ import Breadcrumbs from './Breadcrumbs';
const DemoApp = React.lazy(() => import('demo/App'));
const KMSApp = React.lazy(() => import('kms/App'));
const FaaSApp = React.lazy(() => import('faas/FaaSApp'));
const FaaSApp = React.lazy(() => import('faas/App'));
const AppLoader: React.FC = () => {
const { appName } = useParams<{ appName: string }>();