-
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
1
demo/dist/396.js
vendored
1
demo/dist/396.js
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/540.js
vendored
2
demo/dist/540.js
vendored
File diff suppressed because one or more lines are too long
9
demo/dist/540.js.LICENSE.txt
vendored
9
demo/dist/540.js.LICENSE.txt
vendored
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license React
|
|
||||||
* react.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
1
demo/dist/665.js
vendored
1
demo/dist/665.js
vendored
@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[665],{665:(e,t,a)=>{a.r(t),a.d(t,{default:()=>c});var r=a(914),l=a.n(r),n=a(435),i=a(101);const c=()=>{const[e,t]=(0,r.useState)(0),[a,c]=(0,r.useState)(!1);(0,r.useEffect)(()=>{const e=setInterval(()=>{t(e=>e>=100?0:e+1)},100);return()=>clearInterval(e)},[]);const o=[{label:"Active Users",value:"1,234",icon:i.IconUsers,color:"blue"},{label:"Total Revenue",value:"$45,678",icon:i.IconChartLine,color:"green"},{label:"Projects",value:"89",icon:i.IconRocket,color:"orange"}];return l().createElement(n.Container,{size:"xl",py:"xl"},l().createElement(n.Stack,{gap:"xl"},l().createElement(n.Group,{justify:"space-between",align:"center"},l().createElement("div",null,l().createElement(n.Title,{order:1},"Demo Application"),l().createElement(n.Text,{c:"dimmed",size:"lg",mt:"xs"},"A sample federated application showcasing module federation")),l().createElement(n.ActionIcon,{size:"lg",variant:"light",loading:a,onClick:()=>{c(!0),setTimeout(()=>c(!1),1500)}},l().createElement(i.IconRefresh,{size:18}))),l().createElement(n.Alert,{icon:l().createElement(i.IconInfoCircle,{size:16}),title:"Welcome!",color:"blue",variant:"light"},"This is a demo application loaded via Module Federation. It demonstrates how microfrontends can be seamlessly integrated into the shell application."),l().createElement(n.SimpleGrid,{cols:{base:1,sm:3},spacing:"md"},o.map(e=>l().createElement(n.Paper,{key:e.label,p:"md",radius:"md",withBorder:!0},l().createElement(n.Group,{justify:"space-between"},l().createElement("div",null,l().createElement(n.Text,{c:"dimmed",size:"sm",fw:500,tt:"uppercase"},e.label),l().createElement(n.Text,{fw:700,size:"xl"},e.value)),l().createElement(e.icon,{size:24,color:`var(--mantine-color-${e.color}-6)`}))))),l().createElement(n.Card,{shadow:"sm",padding:"lg",radius:"md",withBorder:!0},l().createElement(n.Card.Section,{withBorder:!0,inheritPadding:!0,py:"xs"},l().createElement(n.Group,{justify:"space-between"},l().createElement(n.Text,{fw:500},"System Performance"),l().createElement(n.Badge,{color:"green",variant:"light"},"Healthy"))),l().createElement(n.Card.Section,{inheritPadding:!0,py:"md"},l().createElement(n.Stack,{gap:"xs"},l().createElement(n.Text,{size:"sm",c:"dimmed"},"CPU Usage: ",e.toFixed(1),"%"),l().createElement(n.Progress,{value:e,size:"sm",color:"blue",animated:!0})))),l().createElement("div",null,l().createElement(n.Title,{order:2,mb:"md"},"Features"),l().createElement(n.SimpleGrid,{cols:{base:1,sm:2},spacing:"md"},[{title:"Real-time Analytics",description:"Monitor your data in real-time"},{title:"Team Collaboration",description:"Work together seamlessly"},{title:"Cloud Integration",description:"Connect with cloud services"},{title:"Custom Reports",description:"Generate detailed reports"}].map((e,t)=>l().createElement(n.Card,{key:t,shadow:"sm",padding:"lg",radius:"md",withBorder:!0},l().createElement(n.Group,{mb:"xs"},l().createElement(i.IconCheck,{size:16,color:"var(--mantine-color-green-6)"}),l().createElement(n.Text,{fw:500},e.title)),l().createElement(n.Text,{size:"sm",c:"dimmed"},e.description))))),l().createElement(n.Divider,null),l().createElement(n.Group,{justify:"center"},l().createElement(n.Button,{variant:"outline",size:"md"},"View Documentation"),l().createElement(n.Button,{size:"md"},"Get Started"))))}}}]);
|
|
||||||
2
demo/dist/81.js
vendored
2
demo/dist/81.js
vendored
File diff suppressed because one or more lines are too long
9
demo/dist/81.js.LICENSE.txt
vendored
9
demo/dist/81.js.LICENSE.txt
vendored
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license React
|
|
||||||
* react-jsx-runtime.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
1
demo/dist/870.js
vendored
1
demo/dist/870.js
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/961.js
vendored
2
demo/dist/961.js
vendored
File diff suppressed because one or more lines are too long
19
demo/dist/961.js.LICENSE.txt
vendored
19
demo/dist/961.js.LICENSE.txt
vendored
@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license React
|
|
||||||
* react-dom.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @license React
|
|
||||||
* scheduler.production.min.js
|
|
||||||
*
|
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
1
demo/dist/index.html
vendored
1
demo/dist/index.html
vendored
@ -1 +0,0 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Demo App</title><script defer="defer" src="main.js"></script><script defer="defer" src="remoteEntry.js"></script></head><body><div id="root"></div></body></html>
|
|
||||||
1
demo/dist/main.js
vendored
1
demo/dist/main.js
vendored
File diff suppressed because one or more lines are too long
1
demo/dist/remoteEntry.js
vendored
1
demo/dist/remoteEntry.js
vendored
File diff suppressed because one or more lines are too long
@ -12,7 +12,11 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"@mantine/core": "^7.0.0",
|
"@mantine/core": "^7.0.0",
|
||||||
"@mantine/hooks": "^7.0.0",
|
"@mantine/hooks": "^7.0.0",
|
||||||
"@tabler/icons-react": "^2.40.0"
|
"@mantine/notifications": "^7.0.0",
|
||||||
|
"@mantine/form": "^7.0.0",
|
||||||
|
"@mantine/modals": "^7.0.0",
|
||||||
|
"@tabler/icons-react": "^2.40.0",
|
||||||
|
"@skybridge/web-components": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.20.12",
|
||||||
|
|||||||
121
demo/src/App.tsx
121
demo/src/App.tsx
@ -22,11 +22,25 @@ import {
|
|||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
IconPlus,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
TableColumn,
|
||||||
|
FormSidebar,
|
||||||
|
FormField
|
||||||
|
} from '@skybridge/web-components';
|
||||||
|
|
||||||
const DemoApp: React.FC = () => {
|
const DemoApp: React.FC = () => {
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showTable, setShowTable] = useState(false);
|
||||||
|
const [sidebarOpened, setSidebarOpened] = useState(false);
|
||||||
|
const [demoData, setDemoData] = useState([
|
||||||
|
{ id: '1', name: 'John Doe', email: 'john@example.com', status: 'active', role: 'admin', created_at: '2024-01-15' },
|
||||||
|
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', status: 'active', role: 'user', created_at: '2024-02-20' },
|
||||||
|
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', status: 'inactive', role: 'viewer', created_at: '2024-03-10' },
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@ -53,6 +67,63 @@ const DemoApp: React.FC = () => {
|
|||||||
{ title: 'Custom Reports', description: 'Generate detailed reports' },
|
{ title: 'Custom Reports', description: 'Generate detailed reports' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = [
|
||||||
|
{ key: 'name', label: 'Name', sortable: true },
|
||||||
|
{ key: 'email', label: 'Email', sortable: true },
|
||||||
|
{ key: 'role', label: 'Role', render: (value) => <Badge variant="light" size="sm">{value}</Badge> },
|
||||||
|
{ key: 'status', label: 'Status' }, // Uses default status rendering
|
||||||
|
{ key: 'created_at', label: 'Created', render: (value) => new Date(value).toLocaleDateString() },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formFields: FormField[] = [
|
||||||
|
{ name: 'name', label: 'Full Name', type: 'text', required: true, placeholder: 'Enter full name' },
|
||||||
|
{ name: 'email', label: 'Email', type: 'email', required: true, placeholder: 'Enter email address', validation: { email: true } },
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
label: 'Role',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'admin', label: 'Admin' },
|
||||||
|
{ value: 'user', label: 'User' },
|
||||||
|
{ value: 'viewer', label: 'Viewer' },
|
||||||
|
],
|
||||||
|
defaultValue: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'inactive', label: 'Inactive' },
|
||||||
|
],
|
||||||
|
defaultValue: 'active'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFormSubmit = async (values: any) => {
|
||||||
|
// Simulate API call
|
||||||
|
console.log('Form submitted:', values);
|
||||||
|
const newItem = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
...values,
|
||||||
|
created_at: new Date().toISOString().split('T')[0]
|
||||||
|
};
|
||||||
|
setDemoData([...demoData, newItem]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (item: any) => {
|
||||||
|
console.log('Edit item:', item);
|
||||||
|
// Would normally open form with item data
|
||||||
|
setSidebarOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (item: any) => {
|
||||||
|
setDemoData(demoData.filter(d => d.id !== item.id));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl">
|
<Container size="xl" py="xl">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
@ -135,6 +206,43 @@ const DemoApp: React.FC = () => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Title order={2} mb="md">Shared Components Demo</Title>
|
||||||
|
<Text c="dimmed" mb="lg">
|
||||||
|
Demonstration of shared components from @skybridge/web-components
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={16} />}
|
||||||
|
onClick={() => setShowTable(!showTable)}
|
||||||
|
>
|
||||||
|
{showTable ? 'Hide' : 'Show'} DataTable Demo
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSidebarOpened(true)}
|
||||||
|
>
|
||||||
|
Show FormSidebar Demo
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{showTable && (
|
||||||
|
<DataTable
|
||||||
|
data={demoData}
|
||||||
|
columns={tableColumns}
|
||||||
|
title="Demo User Management"
|
||||||
|
searchable
|
||||||
|
onAdd={() => setSidebarOpened(true)}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
emptyMessage="No demo data available"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
<Button variant="outline" size="md">
|
<Button variant="outline" size="md">
|
||||||
View Documentation
|
View Documentation
|
||||||
@ -144,6 +252,19 @@ const DemoApp: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<FormSidebar
|
||||||
|
opened={sidebarOpened}
|
||||||
|
onClose={() => setSidebarOpened(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setSidebarOpened(false);
|
||||||
|
setShowTable(true); // Show table after successful form submission
|
||||||
|
}}
|
||||||
|
title="Demo User"
|
||||||
|
fields={formFields}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
width={400}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"@mantine/dates": "^7.0.0",
|
"@mantine/dates": "^7.0.0",
|
||||||
"@mantine/form": "^7.0.0",
|
"@mantine/form": "^7.0.0",
|
||||||
"@mantine/hooks": "^7.0.0",
|
"@mantine/hooks": "^7.0.0",
|
||||||
|
"@mantine/modals": "^7.0.0",
|
||||||
"@mantine/notifications": "^7.0.0",
|
"@mantine/notifications": "^7.0.0",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tabler/icons-react": "^2.40.0",
|
"@tabler/icons-react": "^2.40.0",
|
||||||
@ -16,7 +17,8 @@
|
|||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.8.0"
|
"react-router-dom": "^6.8.0",
|
||||||
|
"@skybridge/web-components": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.0",
|
"@babel/core": "^7.22.0",
|
||||||
|
|||||||
@ -1,31 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
DataTable,
|
||||||
Button,
|
TableColumn,
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Group,
|
|
||||||
ActionIcon,
|
|
||||||
Badge,
|
Badge,
|
||||||
Card,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
Loader,
|
Stack
|
||||||
Alert,
|
} from '@skybridge/web-components';
|
||||||
Tooltip,
|
|
||||||
Menu,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
import {
|
||||||
IconPlayerPlay,
|
IconPlayerPlay,
|
||||||
IconSettings,
|
|
||||||
IconTrash,
|
|
||||||
IconRocket,
|
|
||||||
IconCode,
|
IconCode,
|
||||||
IconDots,
|
|
||||||
IconPlus,
|
|
||||||
IconRefresh,
|
|
||||||
IconExclamationCircle,
|
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { functionApi } from '../services/apiService';
|
import { functionApi } from '../services/apiService';
|
||||||
import { FunctionDefinition } from '../types';
|
import { FunctionDefinition } from '../types';
|
||||||
|
|
||||||
@ -48,12 +33,10 @@ export const FunctionList: React.FC<FunctionListProps> = ({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await functionApi.list();
|
const data = await functionApi.listFunctions();
|
||||||
// Ensure we have a valid array
|
setFunctions(data);
|
||||||
const functionsArray = response.data?.functions || [];
|
} catch (error) {
|
||||||
setFunctions(functionsArray);
|
console.error('Failed to load functions:', error);
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load functions:', err);
|
|
||||||
setError('Failed to load functions');
|
setError('Failed to load functions');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -65,229 +48,78 @@ export const FunctionList: React.FC<FunctionListProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDelete = async (func: FunctionDefinition) => {
|
const handleDelete = async (func: FunctionDefinition) => {
|
||||||
if (!confirm(`Are you sure you want to delete function "${func.name}"?`)) {
|
await functionApi.deleteFunction(func.id);
|
||||||
return;
|
loadFunctions();
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
const getStatusColor = (status: string) => {
|
||||||
await functionApi.delete(func.id);
|
switch (status) {
|
||||||
notifications.show({
|
case 'active': return 'green';
|
||||||
title: 'Success',
|
case 'inactive': return 'gray';
|
||||||
message: `Function "${func.name}" deleted successfully`,
|
case 'error': return 'red';
|
||||||
color: 'green',
|
case 'building': return 'yellow';
|
||||||
});
|
default: return 'blue';
|
||||||
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) => {
|
const columns: TableColumn[] = [
|
||||||
try {
|
{
|
||||||
await functionApi.deploy(func.id);
|
key: 'name',
|
||||||
notifications.show({
|
label: 'Function Name',
|
||||||
title: 'Success',
|
sortable: true,
|
||||||
message: `Function "${func.name}" deployed successfully`,
|
render: (value, func: FunctionDefinition) => (
|
||||||
color: 'green',
|
<Group gap="xs">
|
||||||
});
|
<IconCode size={16} />
|
||||||
} catch (err) {
|
<Text fw={500}>{value}</Text>
|
||||||
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 && functions.length === 0) {
|
|
||||||
return (
|
|
||||||
<Stack gap="lg">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Title order={2}>Functions</Title>
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconRefresh size={16} />}
|
|
||||||
onClick={loadFunctions}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconPlus size={16} />}
|
|
||||||
onClick={onCreateFunction}
|
|
||||||
>
|
|
||||||
Create Function
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'runtime',
|
||||||
|
label: 'Runtime',
|
||||||
|
render: (value) => (
|
||||||
|
<Badge variant="light" size="sm">{value}</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
render: (value) => (
|
||||||
|
<Badge color={getStatusColor(value)} size="sm">{value}</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
label: 'Created',
|
||||||
|
render: (value) => new Date(value).toLocaleDateString()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
<Stack align="center" justify="center" h={200}>
|
const customActions = [
|
||||||
<Loader size="lg" />
|
{
|
||||||
<Text>Loading functions...</Text>
|
key: 'execute',
|
||||||
</Stack>
|
label: 'Execute',
|
||||||
</Stack>
|
icon: <IconPlayerPlay size={14} />,
|
||||||
);
|
onClick: (func: FunctionDefinition) => onExecuteFunction(func),
|
||||||
}
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<Group justify="space-between">
|
<DataTable
|
||||||
<Title order={2}>Functions</Title>
|
data={functions}
|
||||||
<Group>
|
columns={columns}
|
||||||
<Button
|
loading={loading}
|
||||||
leftSection={<IconRefresh size={16} />}
|
error={error}
|
||||||
onClick={loadFunctions}
|
title="Functions"
|
||||||
loading={loading}
|
searchable
|
||||||
>
|
onAdd={onCreateFunction}
|
||||||
Refresh
|
onEdit={onEditFunction}
|
||||||
</Button>
|
onDelete={handleDelete}
|
||||||
<Button
|
onRefresh={loadFunctions}
|
||||||
leftSection={<IconPlus size={16} />}
|
customActions={customActions}
|
||||||
onClick={onCreateFunction}
|
emptyMessage="No functions found"
|
||||||
>
|
/>
|
||||||
Create Function
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert color="red" title="Error">
|
|
||||||
{error}
|
|
||||||
<Button variant="light" size="xs" mt="sm" onClick={loadFunctions}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{functions.length === 0 ? (
|
|
||||||
<Card shadow="sm" radius="md" withBorder p="xl">
|
|
||||||
<Stack align="center" gap="md">
|
|
||||||
<IconCode size={48} color="gray" />
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Text fw={500} mb="xs">
|
|
||||||
No functions found
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Create your first serverless function to get started
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconPlus size={16} />}
|
|
||||||
onClick={onCreateFunction}
|
|
||||||
>
|
|
||||||
Create Function
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card shadow="sm" radius="md" withBorder>
|
|
||||||
<Table>
|
|
||||||
<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.description || ''}</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.memoryLimit || 'N/A'} MB</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text size="sm">{func.timeout || 'N/A'}</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text size="sm">N/A</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text size="sm">
|
|
||||||
{func.createdAt ? new Date(func.createdAt).toLocaleDateString() : 'N/A'}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Tooltip label="Execute Function">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="green"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onExecuteFunction(func)}
|
|
||||||
>
|
|
||||||
<IconPlayerPlay size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu position="bottom-end">
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="subtle" 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>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,23 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
TextInput,
|
|
||||||
Select,
|
|
||||||
NumberInput,
|
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Divider,
|
Divider,
|
||||||
JsonInput,
|
|
||||||
Box,
|
Box,
|
||||||
Title,
|
|
||||||
ActionIcon,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconX } from '@tabler/icons-react';
|
import {
|
||||||
import { useForm } from '@mantine/form';
|
FormSidebar,
|
||||||
import { notifications } from '@mantine/notifications';
|
FormField
|
||||||
|
} from '@skybridge/web-components';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { functionApi, runtimeApi } from '../services/apiService';
|
import { functionApi, runtimeApi } from '../services/apiService';
|
||||||
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
|
import { FunctionDefinition, CreateFunctionRequest, UpdateFunctionRequest, RuntimeType } from '../types';
|
||||||
@ -35,9 +28,9 @@ export const FunctionSidebar: React.FC<FunctionSidebarProps> = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
editFunction,
|
editFunction,
|
||||||
}) => {
|
}) => {
|
||||||
const isEditing = !!editFunction;
|
|
||||||
const [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
|
const [runtimeOptions, setRuntimeOptions] = useState<Array<{value: string; label: string}>>([]);
|
||||||
|
const [codeContent, setCodeContent] = useState('');
|
||||||
|
|
||||||
// Default images for each runtime
|
// Default images for each runtime
|
||||||
const DEFAULT_IMAGES: Record<string, string> = {
|
const DEFAULT_IMAGES: Record<string, string> = {
|
||||||
'nodejs18': 'node:18-alpine',
|
'nodejs18': 'node:18-alpine',
|
||||||
@ -88,30 +81,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event map[string]interface{}
|
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) {
|
func Handler(ctx context.Context, event Event) (map[string]interface{}, error) {
|
||||||
eventJSON, _ := json.MarshalIndent(event, "", " ")
|
fmt.Printf("Event: %+v\\n", event)
|
||||||
log.Printf("Event: %s", eventJSON)
|
|
||||||
|
|
||||||
body := map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"message": "Hello from Go!",
|
"statusCode": 200,
|
||||||
"timestamp": time.Now().Format(time.RFC3339),
|
"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
|
}, nil
|
||||||
}`
|
}`
|
||||||
};
|
};
|
||||||
@ -119,196 +102,110 @@ func Handler(ctx context.Context, event Event) (Response, error) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch available runtimes from backend
|
loadRuntimeOptions();
|
||||||
const fetchRuntimes = async () => {
|
if (editFunction) {
|
||||||
try {
|
setCodeContent(editFunction.code || '');
|
||||||
const response = await runtimeApi.getRuntimes();
|
}
|
||||||
setRuntimeOptions(response.data.runtimes || []);
|
}, [editFunction]);
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch runtimes:', error);
|
const loadRuntimeOptions = async () => {
|
||||||
// Fallback to default options
|
try {
|
||||||
setRuntimeOptions([
|
const runtimes = await runtimeApi.listRuntimes();
|
||||||
{ value: 'nodejs18', label: 'Node.js 18.x' },
|
const options = runtimes.map((runtime: RuntimeType) => ({
|
||||||
{ value: 'python3.9', label: 'Python 3.9' },
|
value: runtime.name,
|
||||||
{ value: 'go1.20', label: 'Go 1.20' },
|
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 (opened) {
|
|
||||||
fetchRuntimes();
|
|
||||||
}
|
|
||||||
}, [opened]);
|
|
||||||
|
|
||||||
// Update form values when editFunction changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (editFunction) {
|
if (editFunction) {
|
||||||
form.setValues({
|
const updateRequest: UpdateFunctionRequest = {
|
||||||
name: editFunction.name || '',
|
description: submitData.description,
|
||||||
app_id: editFunction.app_id || 'default',
|
code: submitData.code,
|
||||||
runtime: editFunction.runtime || 'nodejs18' as RuntimeType,
|
timeout: submitData.timeout,
|
||||||
image: editFunction.image || DEFAULT_IMAGES['nodejs18'] || '',
|
memory: submitData.memory,
|
||||||
handler: editFunction.handler || 'index.handler',
|
environment_variables: submitData.environment_variables,
|
||||||
code: editFunction.code || '',
|
docker_image: submitData.docker_image,
|
||||||
environment: editFunction.environment ? JSON.stringify(editFunction.environment, null, 2) : '{}',
|
};
|
||||||
timeout: editFunction.timeout || '30s',
|
await functionApi.updateFunction(editFunction.id, updateRequest);
|
||||||
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 {
|
} else {
|
||||||
// Reset to default values when not editing
|
const createRequest: CreateFunctionRequest = submitData;
|
||||||
form.setValues({
|
await functionApi.createFunction(createRequest);
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create a custom sidebar that includes the Monaco editor
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 60, // Below header
|
top: 60,
|
||||||
right: opened ? 0 : '-500px',
|
right: opened ? 0 : '-600px',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: '500px',
|
width: '600px',
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -318,184 +215,41 @@ func Handler(ctx context.Context, event Event) (Response, error) {
|
|||||||
transition: 'right 0.3s ease',
|
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 }}>
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
<Box p="md">
|
<Stack gap="md" p="md">
|
||||||
<form onSubmit={(e) => {
|
<FormSidebar
|
||||||
console.log('Form submit event triggered');
|
opened={true} // Always open since we're embedding it
|
||||||
console.log('Form values:', form.values);
|
onClose={() => {}} // Handled by parent
|
||||||
console.log('Form errors:', form.errors);
|
onSuccess={onSuccess}
|
||||||
console.log('Is form valid?', form.isValid());
|
title="Function"
|
||||||
const result = form.onSubmit(handleSubmit)(e);
|
editMode={!!editFunction}
|
||||||
console.log('Form onSubmit result:', result);
|
editItem={editFunction}
|
||||||
return result;
|
fields={fields}
|
||||||
}}>
|
onSubmit={handleSubmit}
|
||||||
<Stack gap="md">
|
width={600}
|
||||||
<Group grow>
|
style={{ position: 'relative', right: 'auto', top: 'auto', bottom: 'auto' }}
|
||||||
<TextInput
|
/>
|
||||||
label="Function Name"
|
|
||||||
placeholder="my-function"
|
<Divider />
|
||||||
required
|
|
||||||
{...form.getInputProps('name')}
|
<Box>
|
||||||
/>
|
<Text fw={500} mb="sm">Code Editor</Text>
|
||||||
<TextInput
|
<Box h={300} style={{ border: '1px solid var(--mantine-color-gray-3)' }}>
|
||||||
label="App ID"
|
<Editor
|
||||||
placeholder="my-app"
|
height="300px"
|
||||||
required
|
language={getEditorLanguage(editFunction?.runtime || 'nodejs18')}
|
||||||
disabled={isEditing}
|
value={codeContent}
|
||||||
{...form.getInputProps('app_id')}
|
onChange={(value) => setCodeContent(value || '')}
|
||||||
/>
|
theme="vs-dark"
|
||||||
</Group>
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
<Group grow>
|
scrollBeyondLastLine: false,
|
||||||
<Select
|
fontSize: 14,
|
||||||
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>
|
||||||
<Box>
|
</Box>
|
||||||
<Text size="sm" fw={500} mb={5}>
|
</Stack>
|
||||||
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>
|
</ScrollArea>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
2
kms/web/dist/665.js
vendored
2
kms/web/dist/665.js
vendored
File diff suppressed because one or more lines are too long
2
kms/web/dist/main.js
vendored
2
kms/web/dist/main.js
vendored
File diff suppressed because one or more lines are too long
10
kms/web/dist/main.js.LICENSE.txt
vendored
10
kms/web/dist/main.js.LICENSE.txt
vendored
@ -1,3 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-jsx-runtime.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @remix-run/router v1.23.0
|
* @remix-run/router v1.23.0
|
||||||
*
|
*
|
||||||
|
|||||||
2
kms/web/dist/remoteEntry.js
vendored
2
kms/web/dist/remoteEntry.js
vendored
File diff suppressed because one or more lines are too long
@ -16,9 +16,11 @@
|
|||||||
"@mantine/notifications": "^7.0.0",
|
"@mantine/notifications": "^7.0.0",
|
||||||
"@mantine/dates": "^7.0.0",
|
"@mantine/dates": "^7.0.0",
|
||||||
"@mantine/form": "^7.0.0",
|
"@mantine/form": "^7.0.0",
|
||||||
|
"@mantine/modals": "^7.0.0",
|
||||||
"@tabler/icons-react": "^2.40.0",
|
"@tabler/icons-react": "^2.40.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"dayjs": "^1.11.13"
|
"dayjs": "^1.11.13",
|
||||||
|
"@skybridge/web-components": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.20.12",
|
||||||
|
|||||||
@ -1,19 +1,8 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
FormSidebar,
|
||||||
TextInput,
|
FormField
|
||||||
MultiSelect,
|
} from '@skybridge/web-components';
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
ActionIcon,
|
|
||||||
ScrollArea,
|
|
||||||
Box,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconX } from '@tabler/icons-react';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
|
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
|
||||||
|
|
||||||
interface ApplicationSidebarProps {
|
interface ApplicationSidebarProps {
|
||||||
@ -29,35 +18,6 @@ const ApplicationSidebar: React.FC<ApplicationSidebarProps> = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
editingApp,
|
editingApp,
|
||||||
}) => {
|
}) => {
|
||||||
const isEditing = !!editingApp;
|
|
||||||
|
|
||||||
const appTypeOptions = [
|
|
||||||
{ value: 'static', label: 'Static Token App' },
|
|
||||||
{ value: 'user', label: 'User Token App' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const form = useForm<CreateApplicationRequest>({
|
|
||||||
initialValues: {
|
|
||||||
app_id: '',
|
|
||||||
app_link: '',
|
|
||||||
type: [],
|
|
||||||
callback_url: '',
|
|
||||||
token_prefix: '',
|
|
||||||
token_renewal_duration: '24h',
|
|
||||||
max_token_duration: '168h',
|
|
||||||
owner: {
|
|
||||||
type: 'individual',
|
|
||||||
name: 'Admin User',
|
|
||||||
owner: 'admin@example.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
app_id: (value) => value.length < 1 ? 'App ID is required' : null,
|
|
||||||
app_link: (value) => value.length < 1 ? 'App Link is required' : null,
|
|
||||||
callback_url: (value) => value.length < 1 ? 'Callback URL is required' : null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const parseDuration = (duration: string): number => {
|
const parseDuration = (duration: string): number => {
|
||||||
// Convert duration string like "24h" to seconds
|
// Convert duration string like "24h" to seconds
|
||||||
const match = duration.match(/^(\d+)([hmd]?)$/);
|
const match = duration.match(/^(\d+)([hmd]?)$/);
|
||||||
@ -74,167 +34,97 @@ const ApplicationSidebar: React.FC<ApplicationSidebarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update form values when editingApp changes
|
const fields: FormField[] = [
|
||||||
useEffect(() => {
|
{
|
||||||
|
name: 'app_id',
|
||||||
|
label: 'Application ID',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'my-app-id',
|
||||||
|
disabled: !!editingApp, // Disable editing for existing apps
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app_link',
|
||||||
|
label: 'Application Link',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://myapp.example.com',
|
||||||
|
validation: { url: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
label: 'Application Type',
|
||||||
|
type: 'multiselect',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'static', label: 'Static Token App' },
|
||||||
|
{ value: 'user', label: 'User Token App' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'callback_url',
|
||||||
|
label: 'Callback URL',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://myapp.example.com/callback',
|
||||||
|
validation: { url: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'token_prefix',
|
||||||
|
label: 'Token Prefix (Optional)',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'myapp_',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'token_renewal_duration',
|
||||||
|
label: 'Token Renewal Duration',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: '24h',
|
||||||
|
defaultValue: '24h',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'max_token_duration',
|
||||||
|
label: 'Max Token Duration',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: '168h',
|
||||||
|
defaultValue: '168h',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
token_renewal_duration_seconds: parseDuration(values.token_renewal_duration || '24h'),
|
||||||
|
max_token_duration_seconds: parseDuration(values.max_token_duration || '168h'),
|
||||||
|
owner: {
|
||||||
|
type: 'individual',
|
||||||
|
name: 'Admin User',
|
||||||
|
owner: 'admin@example.com',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (editingApp) {
|
if (editingApp) {
|
||||||
form.setValues({
|
await apiService.updateApplication(editingApp.app_id, submitData);
|
||||||
app_id: editingApp.app_id || '',
|
|
||||||
app_link: editingApp.app_link || '',
|
|
||||||
type: editingApp.type || [],
|
|
||||||
callback_url: editingApp.callback_url || '',
|
|
||||||
token_prefix: editingApp.token_prefix || '',
|
|
||||||
token_renewal_duration: editingApp.token_renewal_duration || '24h',
|
|
||||||
max_token_duration: editingApp.max_token_duration || '168h',
|
|
||||||
owner: {
|
|
||||||
type: editingApp.owner?.type || 'individual',
|
|
||||||
name: editingApp.owner?.name || 'Admin User',
|
|
||||||
owner: editingApp.owner?.owner || 'admin@example.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Reset to default values when not editing
|
await apiService.createApplication(submitData);
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
}, [editingApp, opened]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: typeof form.values) => {
|
|
||||||
try {
|
|
||||||
const submitData = {
|
|
||||||
...values,
|
|
||||||
token_renewal_duration_seconds: parseDuration(values.token_renewal_duration),
|
|
||||||
max_token_duration_seconds: parseDuration(values.max_token_duration),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isEditing && editingApp) {
|
|
||||||
await apiService.updateApplication(editingApp.app_id, submitData);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'Application updated successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await apiService.createApplication(submitData);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'Application created successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess();
|
|
||||||
onClose();
|
|
||||||
form.reset();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving application:', error);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: `Failed to ${isEditing ? 'update' : 'create'} application`,
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<FormSidebar
|
||||||
style={{
|
opened={opened}
|
||||||
position: 'fixed',
|
onClose={onClose}
|
||||||
top: 60, // Below header
|
onSuccess={onSuccess}
|
||||||
right: opened ? 0 : '-450px',
|
title="Application"
|
||||||
bottom: 0,
|
editMode={!!editingApp}
|
||||||
width: '450px',
|
editItem={editingApp}
|
||||||
zIndex: 1000,
|
fields={fields}
|
||||||
borderRadius: 0,
|
onSubmit={handleSubmit}
|
||||||
display: 'flex',
|
width={450}
|
||||||
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 Application' : 'Create New Application'}
|
|
||||||
</Title>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Box p="md">
|
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
|
||||||
<Stack gap="md">
|
|
||||||
<TextInput
|
|
||||||
label="Application ID"
|
|
||||||
placeholder="my-app-id"
|
|
||||||
required
|
|
||||||
{...form.getInputProps('app_id')}
|
|
||||||
disabled={isEditing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Application Link"
|
|
||||||
placeholder="https://myapp.example.com"
|
|
||||||
required
|
|
||||||
{...form.getInputProps('app_link')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
label="Application Type"
|
|
||||||
placeholder="Select application types"
|
|
||||||
data={appTypeOptions}
|
|
||||||
required
|
|
||||||
{...form.getInputProps('type')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Callback URL"
|
|
||||||
placeholder="https://myapp.example.com/callback"
|
|
||||||
required
|
|
||||||
{...form.getInputProps('callback_url')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Token Prefix (Optional)"
|
|
||||||
placeholder="myapp_"
|
|
||||||
{...form.getInputProps('token_prefix')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group grow>
|
|
||||||
<TextInput
|
|
||||||
label="Token Renewal Duration"
|
|
||||||
placeholder="24h"
|
|
||||||
{...form.getInputProps('token_renewal_duration')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Max Token Duration"
|
|
||||||
placeholder="168h"
|
|
||||||
{...form.getInputProps('max_token_duration')}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">
|
|
||||||
{isEditing ? 'Update Application' : 'Create Application'}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
DataTable,
|
||||||
Button,
|
TableColumn,
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Modal,
|
|
||||||
TextInput,
|
|
||||||
MultiSelect,
|
|
||||||
Group,
|
|
||||||
ActionIcon,
|
|
||||||
Badge,
|
Badge,
|
||||||
Card,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
Loader,
|
Stack
|
||||||
Alert,
|
} from '@skybridge/web-components';
|
||||||
Textarea,
|
import { IconEye, IconCopy } from '@tabler/icons-react';
|
||||||
Select,
|
|
||||||
NumberInput,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconPlus,
|
|
||||||
IconEdit,
|
|
||||||
IconTrash,
|
|
||||||
IconEye,
|
|
||||||
IconCopy,
|
|
||||||
IconAlertCircle,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
|
import { apiService, Application } from '../services/apiService';
|
||||||
import ApplicationSidebar from './ApplicationSidebar';
|
import ApplicationSidebar from './ApplicationSidebar';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@ -37,30 +18,6 @@ const Applications: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [editingApp, setEditingApp] = useState<Application | null>(null);
|
const [editingApp, setEditingApp] = useState<Application | null>(null);
|
||||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
|
||||||
const [selectedApp, setSelectedApp] = useState<Application | null>(null);
|
|
||||||
|
|
||||||
const form = useForm<CreateApplicationRequest>({
|
|
||||||
initialValues: {
|
|
||||||
app_id: '',
|
|
||||||
app_link: '',
|
|
||||||
type: [],
|
|
||||||
callback_url: '',
|
|
||||||
token_prefix: '',
|
|
||||||
token_renewal_duration: '24h',
|
|
||||||
max_token_duration: '168h',
|
|
||||||
owner: {
|
|
||||||
type: 'individual',
|
|
||||||
name: 'Admin User',
|
|
||||||
owner: 'admin@example.com',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
app_id: (value) => value.length < 1 ? 'App ID is required' : null,
|
|
||||||
app_link: (value) => value.length < 1 ? 'App Link is required' : null,
|
|
||||||
callback_url: (value) => value.length < 1 ? 'Callback URL is required' : null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadApplications();
|
loadApplications();
|
||||||
@ -73,109 +30,30 @@ const Applications: React.FC = () => {
|
|||||||
setApplications(response.data);
|
setApplications(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load applications:', error);
|
console.error('Failed to load applications:', error);
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to load applications',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseDuration = (duration: string): number => {
|
const handleAdd = () => {
|
||||||
// Convert duration string like "24h" to seconds
|
setEditingApp(null);
|
||||||
const match = duration.match(/^(\d+)([hmd]?)$/);
|
setSidebarOpen(true);
|
||||||
if (!match) return 86400; // Default to 24h in seconds
|
|
||||||
|
|
||||||
const value = parseInt(match[1]);
|
|
||||||
const unit = match[2] || 'h';
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 'm': return value * 60; // minutes to seconds
|
|
||||||
case 'h': return value * 3600; // hours to seconds
|
|
||||||
case 'd': return value * 86400; // days to seconds
|
|
||||||
default: return value * 3600; // default to hours
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (values: CreateApplicationRequest) => {
|
|
||||||
try {
|
|
||||||
// Convert duration strings to seconds for API
|
|
||||||
const apiValues = {
|
|
||||||
...values,
|
|
||||||
token_renewal_duration: parseDuration(values.token_renewal_duration),
|
|
||||||
max_token_duration: parseDuration(values.max_token_duration),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (editingApp) {
|
|
||||||
await apiService.updateApplication(editingApp.app_id, apiValues);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'Application updated successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await apiService.createApplication(apiValues);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'Application created successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setSidebarOpen(false);
|
|
||||||
setEditingApp(null);
|
|
||||||
form.reset();
|
|
||||||
loadApplications();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save application:', error);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to save application',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (app: Application) => {
|
const handleEdit = (app: Application) => {
|
||||||
setEditingApp(app);
|
setEditingApp(app);
|
||||||
form.setValues({
|
|
||||||
app_id: app.app_id,
|
|
||||||
app_link: app.app_link,
|
|
||||||
type: app.type,
|
|
||||||
callback_url: app.callback_url,
|
|
||||||
token_prefix: app.token_prefix || '',
|
|
||||||
token_renewal_duration: `${app.token_renewal_duration / 3600}h`,
|
|
||||||
max_token_duration: `${app.max_token_duration / 3600}h`,
|
|
||||||
owner: app.owner,
|
|
||||||
});
|
|
||||||
setSidebarOpen(true);
|
setSidebarOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (appId: string) => {
|
const handleDelete = async (app: Application) => {
|
||||||
if (window.confirm('Are you sure you want to delete this application?')) {
|
await apiService.deleteApplication(app.app_id);
|
||||||
try {
|
loadApplications();
|
||||||
await apiService.deleteApplication(appId);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'Application deleted successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
loadApplications();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete application:', error);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to delete application',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewDetails = (app: Application) => {
|
const handleSuccess = () => {
|
||||||
setSelectedApp(app);
|
setSidebarOpen(false);
|
||||||
setDetailModalOpen(true);
|
setEditingApp(null);
|
||||||
|
loadApplications();
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
@ -187,216 +65,85 @@ const Applications: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const appTypeOptions = [
|
const columns: TableColumn[] = [
|
||||||
{ value: 'static', label: 'Static' },
|
{
|
||||||
{ value: 'user', label: 'User' },
|
key: 'app_id',
|
||||||
];
|
label: 'Application ID',
|
||||||
|
render: (value) => <Text fw={500}>{value}</Text>
|
||||||
const rows = applications.map((app) => (
|
},
|
||||||
<Table.Tr key={app.app_id}>
|
{
|
||||||
<Table.Td>
|
key: 'type',
|
||||||
<Text fw={500}>{app.app_id}</Text>
|
label: 'Type',
|
||||||
</Table.Td>
|
render: (value: string[]) => (
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{app.type.map((type) => (
|
{value.map((type) => (
|
||||||
<Badge key={type} variant="light" size="sm">
|
<Badge key={type} variant="light" size="sm">
|
||||||
{type}
|
{type}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
)
|
||||||
<Table.Td>
|
},
|
||||||
|
{
|
||||||
|
key: 'owner',
|
||||||
|
label: 'Owner',
|
||||||
|
render: (value: any) => (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{app.owner.name} ({app.owner.owner})
|
{value.name} ({value.owner})
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
)
|
||||||
<Table.Td>
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
label: 'Created',
|
||||||
|
render: (value) => (
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{dayjs(app.created_at).format('MMM DD, YYYY')}
|
{dayjs(value).format('MMM DD, YYYY')}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Td>
|
)
|
||||||
<Table.Td>
|
},
|
||||||
<Group gap="xs">
|
];
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
const customActions = [
|
||||||
color="blue"
|
{
|
||||||
onClick={() => handleViewDetails(app)}
|
key: 'view',
|
||||||
title="View Details"
|
label: 'View Details',
|
||||||
>
|
icon: <IconEye size={14} />,
|
||||||
<IconEye size={16} />
|
onClick: (app: Application) => {
|
||||||
</ActionIcon>
|
// Could open a modal or navigate to details page
|
||||||
<ActionIcon
|
console.log('View details for:', app.app_id);
|
||||||
variant="subtle"
|
},
|
||||||
color="gray"
|
},
|
||||||
onClick={() => handleEdit(app)}
|
{
|
||||||
title="Edit"
|
key: 'copy',
|
||||||
>
|
label: 'Copy App ID',
|
||||||
<IconEdit size={16} />
|
icon: <IconCopy size={14} />,
|
||||||
</ActionIcon>
|
onClick: (app: Application) => copyToClipboard(app.app_id),
|
||||||
<ActionIcon
|
},
|
||||||
variant="subtle"
|
];
|
||||||
color="red"
|
|
||||||
onClick={() => handleDelete(app.app_id)}
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack gap="md">
|
||||||
gap="lg"
|
<DataTable
|
||||||
style={{
|
data={applications}
|
||||||
transition: 'margin-right 0.3s ease',
|
columns={columns}
|
||||||
marginRight: sidebarOpen ? '450px' : '0',
|
loading={loading}
|
||||||
}}
|
title="Applications"
|
||||||
>
|
searchable
|
||||||
<Group justify="space-between">
|
onAdd={handleAdd}
|
||||||
<div>
|
onEdit={handleEdit}
|
||||||
<Title order={2} mb="xs">
|
onDelete={handleDelete}
|
||||||
Applications
|
onRefresh={loadApplications}
|
||||||
</Title>
|
customActions={customActions}
|
||||||
</div>
|
emptyMessage="No applications found"
|
||||||
<Button
|
/>
|
||||||
leftSection={<IconPlus size={16} />}
|
|
||||||
onClick={() => {
|
|
||||||
setEditingApp(null);
|
|
||||||
form.reset();
|
|
||||||
setSidebarOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
New Application
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Stack align="center" justify="center" h={200}>
|
|
||||||
<Loader size="lg" />
|
|
||||||
<Text>Loading applications...</Text>
|
|
||||||
</Stack>
|
|
||||||
) : applications.length === 0 ? (
|
|
||||||
<Card shadow="sm" radius="md" withBorder p="xl">
|
|
||||||
<Stack align="center" gap="md">
|
|
||||||
<IconAlertCircle size={48} color="gray" />
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Text fw={500} mb="xs">
|
|
||||||
No applications found
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Create your first application to get started with the key management system
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconPlus size={16} />}
|
|
||||||
onClick={() => {
|
|
||||||
setEditingApp(null);
|
|
||||||
form.reset();
|
|
||||||
setSidebarOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create Application
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card shadow="sm" radius="md" withBorder>
|
|
||||||
<Table>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>Application ID</Table.Th>
|
|
||||||
<Table.Th>Type</Table.Th>
|
|
||||||
<Table.Th>Owner</Table.Th>
|
|
||||||
<Table.Th>Created</Table.Th>
|
|
||||||
<Table.Th>Actions</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>{rows}</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ApplicationSidebar
|
<ApplicationSidebar
|
||||||
opened={sidebarOpen}
|
opened={sidebarOpen}
|
||||||
onClose={() => {
|
onClose={() => setSidebarOpen(false)}
|
||||||
setSidebarOpen(false);
|
onSuccess={handleSuccess}
|
||||||
setEditingApp(null);
|
|
||||||
}}
|
|
||||||
onSuccess={() => {
|
|
||||||
loadApplications();
|
|
||||||
}}
|
|
||||||
editingApp={editingApp}
|
editingApp={editingApp}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Detail Modal */}
|
|
||||||
<Modal
|
|
||||||
opened={detailModalOpen}
|
|
||||||
onClose={() => setDetailModalOpen(false)}
|
|
||||||
title="Application Details"
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
{selectedApp && (
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Application ID:</Text>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text>{selectedApp.app_id}</Text>
|
|
||||||
<ActionIcon
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => copyToClipboard(selectedApp.app_id)}
|
|
||||||
>
|
|
||||||
<IconCopy size={12} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>HMAC Key:</Text>
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text size="sm" style={{ fontFamily: 'monospace' }}>
|
|
||||||
{selectedApp.hmac_key.substring(0, 16)}...
|
|
||||||
</Text>
|
|
||||||
<ActionIcon
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => copyToClipboard(selectedApp.hmac_key)}
|
|
||||||
>
|
|
||||||
<IconCopy size={12} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Application Link:</Text>
|
|
||||||
<Text size="sm">{selectedApp.app_link}</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Callback URL:</Text>
|
|
||||||
<Text size="sm">{selectedApp.callback_url}</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Token Renewal:</Text>
|
|
||||||
<Text size="sm">{selectedApp.token_renewal_duration / 3600}h</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Max Duration:</Text>
|
|
||||||
<Text size="sm">{selectedApp.max_token_duration / 3600}h</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Text fw={500}>Created:</Text>
|
|
||||||
<Text size="sm">{dayjs(selectedApp.created_at).format('MMM DD, YYYY HH:mm')}</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,29 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
DataTable,
|
||||||
Group,
|
TableColumn,
|
||||||
Paper,
|
|
||||||
Text,
|
|
||||||
Table,
|
|
||||||
Badge,
|
Badge,
|
||||||
ActionIcon,
|
Group,
|
||||||
TextInput,
|
Text,
|
||||||
Select,
|
Stack
|
||||||
Pagination,
|
} from '@skybridge/web-components';
|
||||||
Stack,
|
import { Avatar } from '@mantine/core';
|
||||||
LoadingOverlay,
|
import { IconUser, IconMail } from '@tabler/icons-react';
|
||||||
Avatar,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconPlus,
|
|
||||||
IconEdit,
|
|
||||||
IconTrash,
|
|
||||||
IconSearch,
|
|
||||||
IconUser,
|
|
||||||
IconMail,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { modals } from '@mantine/modals';
|
|
||||||
import UserSidebar from './UserSidebar';
|
import UserSidebar from './UserSidebar';
|
||||||
import { userService } from '../services/userService';
|
import { userService } from '../services/userService';
|
||||||
import { User, UserStatus, UserRole, ListUsersRequest } from '../types/user';
|
import { User, UserStatus, UserRole, ListUsersRequest } from '../types/user';
|
||||||
@ -31,9 +16,7 @@ import { User, UserStatus, UserRole, ListUsersRequest } from '../types/user';
|
|||||||
const UserManagement: React.FC = () => {
|
const UserManagement: React.FC = () => {
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [filters, setFilters] = useState({});
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
|
||||||
const [roleFilter, setRoleFilter] = useState<string | null>(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalUsers, setTotalUsers] = useState(0);
|
const [totalUsers, setTotalUsers] = useState(0);
|
||||||
const [userSidebarOpened, setUserSidebarOpened] = useState(false);
|
const [userSidebarOpened, setUserSidebarOpened] = useState(false);
|
||||||
@ -41,13 +24,13 @@ const UserManagement: React.FC = () => {
|
|||||||
|
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
const loadUsers = async (page: number = currentPage) => {
|
const loadUsers = async (page: number = currentPage, newFilters = filters) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const request: ListUsersRequest = {
|
const request: ListUsersRequest = {
|
||||||
search: searchTerm || undefined,
|
search: newFilters.search || undefined,
|
||||||
status: statusFilter as UserStatus || undefined,
|
status: newFilters.status as UserStatus || undefined,
|
||||||
role: roleFilter as UserRole || undefined,
|
role: newFilters.role as UserRole || undefined,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (page - 1) * pageSize,
|
offset: (page - 1) * pageSize,
|
||||||
order_by: 'created_at',
|
order_by: 'created_at',
|
||||||
@ -60,95 +43,108 @@ const UserManagement: React.FC = () => {
|
|||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load users:', error);
|
console.error('Failed to load users:', error);
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to load users',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers(1);
|
loadUsers(1, filters);
|
||||||
}, [searchTerm, statusFilter, roleFilter]);
|
}, [filters]);
|
||||||
|
|
||||||
const handleCreateUser = () => {
|
const handleAdd = () => {
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
setUserSidebarOpened(true);
|
setUserSidebarOpened(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditUser = (user: User) => {
|
const handleEdit = (user: User) => {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setUserSidebarOpened(true);
|
setUserSidebarOpened(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteUser = (user: User) => {
|
const handleDelete = async (user: User) => {
|
||||||
modals.openConfirmModal({
|
await userService.deleteUser(user.id);
|
||||||
title: 'Delete User',
|
|
||||||
children: (
|
|
||||||
<Text size="sm">
|
|
||||||
Are you sure you want to delete {user.first_name} {user.last_name}? This action cannot be undone.
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
|
||||||
confirmProps: { color: 'red' },
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await userService.deleteUser(user.id);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'User deleted successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
loadUsers();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete user:', error);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to delete user',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserSidebarSuccess = () => {
|
|
||||||
loadUsers();
|
loadUsers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserSidebarClose = () => {
|
const handleSuccess = () => {
|
||||||
setUserSidebarOpened(false);
|
setUserSidebarOpened(false);
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
|
loadUsers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: UserStatus): string => {
|
const handleFiltersChange = (newFilters) => {
|
||||||
switch (status) {
|
setFilters(newFilters);
|
||||||
case 'active': return 'green';
|
setCurrentPage(1);
|
||||||
case 'inactive': return 'gray';
|
|
||||||
case 'suspended': return 'red';
|
|
||||||
case 'pending': return 'yellow';
|
|
||||||
default: return 'gray';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRoleColor = (role: UserRole): string => {
|
const columns: TableColumn[] = [
|
||||||
switch (role) {
|
{
|
||||||
case 'admin': return 'red';
|
key: 'user',
|
||||||
case 'moderator': return 'orange';
|
label: 'User',
|
||||||
case 'user': return 'blue';
|
render: (_, user: User) => (
|
||||||
case 'viewer': return 'gray';
|
<Group gap="sm">
|
||||||
default: return 'gray';
|
<Avatar
|
||||||
}
|
src={user.avatar || null}
|
||||||
};
|
radius="sm"
|
||||||
|
size={32}
|
||||||
const totalPages = Math.ceil(totalUsers / pageSize);
|
>
|
||||||
|
<IconUser size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{user.display_name || `${user.first_name} ${user.last_name}`}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
render: (value) => (
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMail size={14} />
|
||||||
|
<Text size="sm">{value}</Text>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'role',
|
||||||
|
label: 'Role',
|
||||||
|
render: (value) => {
|
||||||
|
const roleColors = {
|
||||||
|
admin: 'red',
|
||||||
|
moderator: 'orange',
|
||||||
|
user: 'blue',
|
||||||
|
viewer: 'gray'
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Badge color={roleColors[value] || 'blue'} size="sm">
|
||||||
|
{value}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
label: 'Created',
|
||||||
|
render: (value) => (
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(value).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{/* Main user management interface */}
|
|
||||||
<Stack
|
<Stack
|
||||||
gap="md"
|
gap="md"
|
||||||
style={{
|
style={{
|
||||||
@ -156,160 +152,32 @@ const UserManagement: React.FC = () => {
|
|||||||
marginRight: userSidebarOpened ? '400px' : '0',
|
marginRight: userSidebarOpened ? '400px' : '0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between">
|
<DataTable
|
||||||
<Button leftSection={<IconPlus size={16} />} onClick={handleCreateUser}>
|
data={users}
|
||||||
Add User
|
columns={columns}
|
||||||
</Button>
|
loading={loading}
|
||||||
</Group>
|
title="User Management"
|
||||||
|
total={totalUsers}
|
||||||
<Paper p="md" withBorder>
|
page={currentPage}
|
||||||
<Group justify="space-between" mb="md">
|
pageSize={pageSize}
|
||||||
<Group gap="sm">
|
onPageChange={loadUsers}
|
||||||
<TextInput
|
searchable
|
||||||
placeholder="Search users..."
|
filters={filters}
|
||||||
leftSection={<IconSearch size={16} />}
|
onFiltersChange={handleFiltersChange}
|
||||||
value={searchTerm}
|
onAdd={handleAdd}
|
||||||
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
onEdit={handleEdit}
|
||||||
style={{ minWidth: 250 }}
|
onDelete={handleDelete}
|
||||||
/>
|
onRefresh={() => loadUsers()}
|
||||||
<Select
|
emptyMessage="No users found"
|
||||||
placeholder="Filter by status"
|
/>
|
||||||
data={[
|
|
||||||
{ value: 'active', label: 'Active' },
|
|
||||||
{ value: 'inactive', label: 'Inactive' },
|
|
||||||
{ value: 'suspended', label: 'Suspended' },
|
|
||||||
{ value: 'pending', label: 'Pending' },
|
|
||||||
]}
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={setStatusFilter}
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
placeholder="Filter by role"
|
|
||||||
data={[
|
|
||||||
{ value: 'admin', label: 'Admin' },
|
|
||||||
{ value: 'moderator', label: 'Moderator' },
|
|
||||||
{ value: 'user', label: 'User' },
|
|
||||||
{ value: 'viewer', label: 'Viewer' },
|
|
||||||
]}
|
|
||||||
value={roleFilter}
|
|
||||||
onChange={setRoleFilter}
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{totalUsers} users found
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<LoadingOverlay visible={loading} />
|
|
||||||
<Table striped highlightOnHover>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>User</Table.Th>
|
|
||||||
<Table.Th>Email</Table.Th>
|
|
||||||
<Table.Th>Role</Table.Th>
|
|
||||||
<Table.Th>Status</Table.Th>
|
|
||||||
<Table.Th>Created</Table.Th>
|
|
||||||
<Table.Th>Actions</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{users.map((user) => (
|
|
||||||
<Table.Tr key={user.id}>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="sm">
|
|
||||||
<Avatar
|
|
||||||
src={user.avatar || null}
|
|
||||||
radius="sm"
|
|
||||||
size={32}
|
|
||||||
>
|
|
||||||
<IconUser size={16} />
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{user.display_name || `${user.first_name} ${user.last_name}`}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{user.first_name} {user.last_name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconMail size={14} />
|
|
||||||
<Text size="sm">{user.email}</Text>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Badge color={getRoleColor(user.role)} variant="light">
|
|
||||||
{user.role}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Badge color={getStatusColor(user.status)} variant="light">
|
|
||||||
{user.status}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Text size="sm">
|
|
||||||
{new Date(user.created_at).toLocaleDateString()}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="xs">
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEditUser(user)}
|
|
||||||
>
|
|
||||||
<IconEdit size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDeleteUser(user)}
|
|
||||||
>
|
|
||||||
<IconTrash size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{users.length === 0 && !loading && (
|
|
||||||
<Text ta="center" py="xl" c="dimmed">
|
|
||||||
No users found
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<Group justify="center" mt="md">
|
|
||||||
<Pagination
|
|
||||||
value={currentPage}
|
|
||||||
onChange={loadUsers}
|
|
||||||
total={totalPages}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* User sidebar */}
|
|
||||||
<UserSidebar
|
<UserSidebar
|
||||||
opened={userSidebarOpened}
|
opened={userSidebarOpened}
|
||||||
onClose={handleUserSidebarClose}
|
onClose={() => setUserSidebarOpened(false)}
|
||||||
onSuccess={handleUserSidebarSuccess}
|
onSuccess={handleSuccess}
|
||||||
editUser={editingUser}
|
editUser={editingUser}
|
||||||
/>
|
/>
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,10 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
FormSidebar,
|
||||||
TextInput,
|
FormField
|
||||||
Select,
|
} from '@skybridge/web-components';
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
ActionIcon,
|
|
||||||
ScrollArea,
|
|
||||||
Box,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { IconX } from '@tabler/icons-react';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { userService } from '../services/userService';
|
import { userService } from '../services/userService';
|
||||||
import { User, CreateUserRequest, UpdateUserRequest, UserRole, UserStatus } from '../types/user';
|
import { User } from '../types/user';
|
||||||
|
|
||||||
interface UserSidebarProps {
|
interface UserSidebarProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@ -30,225 +19,91 @@ const UserSidebar: React.FC<UserSidebarProps> = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
editUser,
|
editUser,
|
||||||
}) => {
|
}) => {
|
||||||
const isEditing = !!editUser;
|
const fields: FormField[] = [
|
||||||
|
{
|
||||||
const form = useForm({
|
name: 'first_name',
|
||||||
initialValues: {
|
label: 'First Name',
|
||||||
email: '',
|
type: 'text',
|
||||||
first_name: '',
|
required: true,
|
||||||
last_name: '',
|
placeholder: 'Enter first name',
|
||||||
display_name: '',
|
|
||||||
avatar: '',
|
|
||||||
role: 'user' as UserRole,
|
|
||||||
status: 'pending' as UserStatus,
|
|
||||||
},
|
},
|
||||||
validate: {
|
{
|
||||||
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
name: 'last_name',
|
||||||
first_name: (value) => (value.trim().length < 1 ? 'First name is required' : null),
|
label: 'Last Name',
|
||||||
last_name: (value) => (value.trim().length < 1 ? 'Last name is required' : null),
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Enter last name',
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
name: 'display_name',
|
||||||
|
label: 'Display Name',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Enter display name (optional)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Enter email address',
|
||||||
|
validation: { email: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatar',
|
||||||
|
label: 'Avatar URL',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
placeholder: 'Enter avatar URL (optional)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
label: 'Role',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'admin', label: 'Admin' },
|
||||||
|
{ value: 'moderator', label: 'Moderator' },
|
||||||
|
{ value: 'user', label: 'User' },
|
||||||
|
{ value: 'viewer', label: 'Viewer' },
|
||||||
|
],
|
||||||
|
defaultValue: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'inactive', label: 'Inactive' },
|
||||||
|
{ value: 'suspended', label: 'Suspended' },
|
||||||
|
{ value: 'pending', label: 'Pending' },
|
||||||
|
],
|
||||||
|
defaultValue: 'pending',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Update form values when editUser changes
|
const handleSubmit = async (values: any) => {
|
||||||
useEffect(() => {
|
|
||||||
if (editUser) {
|
if (editUser) {
|
||||||
form.setValues({
|
await userService.updateUser(editUser.id, values);
|
||||||
email: editUser.email || '',
|
|
||||||
first_name: editUser.first_name || '',
|
|
||||||
last_name: editUser.last_name || '',
|
|
||||||
display_name: editUser.display_name || '',
|
|
||||||
avatar: editUser.avatar || '',
|
|
||||||
role: editUser.role || 'user' as UserRole,
|
|
||||||
status: editUser.status || 'pending' as UserStatus,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Reset to default values when not editing
|
await userService.createUser(values);
|
||||||
form.setValues({
|
|
||||||
email: '',
|
|
||||||
first_name: '',
|
|
||||||
last_name: '',
|
|
||||||
display_name: '',
|
|
||||||
avatar: '',
|
|
||||||
role: 'user' as UserRole,
|
|
||||||
status: 'pending' as UserStatus,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [editUser, opened]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: typeof form.values) => {
|
|
||||||
try {
|
|
||||||
if (isEditing && editUser) {
|
|
||||||
const updateRequest: UpdateUserRequest = {
|
|
||||||
email: values.email !== editUser.email ? values.email : undefined,
|
|
||||||
first_name: values.first_name !== editUser.first_name ? values.first_name : undefined,
|
|
||||||
last_name: values.last_name !== editUser.last_name ? values.last_name : undefined,
|
|
||||||
display_name: values.display_name !== editUser.display_name ? values.display_name : undefined,
|
|
||||||
avatar: values.avatar !== editUser.avatar ? values.avatar : undefined,
|
|
||||||
role: values.role !== editUser.role ? values.role : undefined,
|
|
||||||
status: values.status !== editUser.status ? values.status : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only send fields that have changed
|
|
||||||
const hasChanges = Object.values(updateRequest).some(value => value !== undefined);
|
|
||||||
if (!hasChanges) {
|
|
||||||
notifications.show({
|
|
||||||
title: 'No Changes',
|
|
||||||
message: 'No changes detected',
|
|
||||||
color: 'blue',
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await userService.updateUser(editUser.id, updateRequest);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'User updated successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const createRequest: CreateUserRequest = {
|
|
||||||
email: values.email,
|
|
||||||
first_name: values.first_name,
|
|
||||||
last_name: values.last_name,
|
|
||||||
display_name: values.display_name || undefined,
|
|
||||||
avatar: values.avatar || undefined,
|
|
||||||
role: values.role,
|
|
||||||
status: values.status,
|
|
||||||
};
|
|
||||||
|
|
||||||
await userService.createUser(createRequest);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Success',
|
|
||||||
message: 'User created successfully',
|
|
||||||
color: 'green',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess();
|
|
||||||
onClose();
|
|
||||||
form.reset();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to save user:', error);
|
|
||||||
notifications.show({
|
|
||||||
title: 'Error',
|
|
||||||
message: error.message || 'Failed to save user',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<FormSidebar
|
||||||
style={{
|
opened={opened}
|
||||||
position: 'fixed',
|
onClose={onClose}
|
||||||
top: 60, // Below header
|
onSuccess={onSuccess}
|
||||||
right: opened ? 0 : '-400px',
|
title="User"
|
||||||
bottom: 0,
|
editMode={!!editUser}
|
||||||
width: '400px',
|
editItem={editUser}
|
||||||
zIndex: 1000,
|
fields={fields}
|
||||||
borderRadius: 0,
|
onSubmit={handleSubmit}
|
||||||
display: 'flex',
|
width={400}
|
||||||
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 User' : 'Create User'}
|
|
||||||
</Title>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Box p="md">
|
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group grow>
|
|
||||||
<TextInput
|
|
||||||
label="First Name"
|
|
||||||
placeholder="Enter first name"
|
|
||||||
required
|
|
||||||
{...form.getInputProps('first_name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Last Name"
|
|
||||||
placeholder="Enter last name"
|
|
||||||
required
|
|
||||||
{...form.getInputProps('last_name')}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Display Name"
|
|
||||||
placeholder="Enter display name (optional)"
|
|
||||||
{...form.getInputProps('display_name')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Email"
|
|
||||||
placeholder="Enter email address"
|
|
||||||
required
|
|
||||||
type="email"
|
|
||||||
{...form.getInputProps('email')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
label="Avatar URL"
|
|
||||||
placeholder="Enter avatar URL (optional)"
|
|
||||||
{...form.getInputProps('avatar')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group grow>
|
|
||||||
<Select
|
|
||||||
label="Role"
|
|
||||||
placeholder="Select role"
|
|
||||||
required
|
|
||||||
data={[
|
|
||||||
{ value: 'admin', label: 'Admin' },
|
|
||||||
{ value: 'moderator', label: 'Moderator' },
|
|
||||||
{ value: 'user', label: 'User' },
|
|
||||||
{ value: 'viewer', label: 'Viewer' },
|
|
||||||
]}
|
|
||||||
{...form.getInputProps('role')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
label="Status"
|
|
||||||
placeholder="Select status"
|
|
||||||
required
|
|
||||||
data={[
|
|
||||||
{ value: 'active', label: 'Active' },
|
|
||||||
{ value: 'inactive', label: 'Inactive' },
|
|
||||||
{ value: 'suspended', label: 'Suspended' },
|
|
||||||
{ value: 'pending', label: 'Pending' },
|
|
||||||
]}
|
|
||||||
{...form.getInputProps('status')}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="xl">
|
|
||||||
<Button variant="light" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">
|
|
||||||
{isEditing ? 'Update' : 'Create'} User
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
web/dist/main.js
vendored
2
web/dist/main.js
vendored
File diff suppressed because one or more lines are too long
@ -14,7 +14,10 @@
|
|||||||
"@mantine/core": "^7.0.0",
|
"@mantine/core": "^7.0.0",
|
||||||
"@mantine/hooks": "^7.0.0",
|
"@mantine/hooks": "^7.0.0",
|
||||||
"@mantine/notifications": "^7.0.0",
|
"@mantine/notifications": "^7.0.0",
|
||||||
"@tabler/icons-react": "^2.40.0"
|
"@mantine/form": "^7.0.0",
|
||||||
|
"@mantine/modals": "^7.0.0",
|
||||||
|
"@tabler/icons-react": "^2.40.0",
|
||||||
|
"@skybridge/web-components": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.20.12",
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Title,
|
Title,
|
||||||
Badge,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { Badge } from '@skybridge/web-components';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
IconStar,
|
IconStar,
|
||||||
|
|||||||
Reference in New Issue
Block a user