Initial commit

This commit is contained in:
mytechpassport
2024-11-15 04:44:20 -05:00
commit a090d6ffb4
32 changed files with 6851 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { ReactFlowProvider } from 'reactflow';
import { Sidebar } from './components/Sidebar';
function App() {
return (
<div className="w-full h-screen">
<ReactFlowProvider>
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 bg-white" />
</div>
</ReactFlowProvider>
</div>
);
}
export default App;
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { Lock, Globe, ArrowUpDown, Code, Database, Variable } from 'lucide-react';
const components = [
{ id: 'auth', name: 'Authentication', icon: Lock },
{ id: 'url', name: 'URL Route', icon: Globe },
{ id: 'output', name: 'Output Data', icon: ArrowUpDown },
{ id: 'logic', name: 'Logic', icon: Code },
{ id: 'variable', name: 'Variable', icon: Variable },
{ id: 'db-find', name: 'DB Find', icon: Database },
{ id: 'db-insert', name: 'DB Insert', icon: Database },
{ id: 'db-update', name: 'DB Update', icon: Database },
{ id: 'db-delete', name: 'DB Delete', icon: Database },
{ id: 'db-query', name: 'DB Query', icon: Database },
];
export function ComponentsPanel() {
const onDragStart = (event: React.DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<div className="w-64 border-r border-gray-200 p-4 bg-white">
<h3 className="text-sm font-medium text-gray-900 mb-3">Components</h3>
<div className="grid grid-cols-2 gap-3">
{components.map((component) => {
const Icon = component.icon;
return (
<div
key={component.id}
className="flex flex-col items-center justify-center p-3 bg-gray-50 rounded-lg cursor-move hover:bg-gray-100 transition-colors"
onDragStart={(event) => onDragStart(event, component.id)}
draggable
>
<Icon className="w-6 h-6 mb-2" />
<span className="text-sm text-center">{component.name}</span>
</div>
);
})}
</div>
</div>
);
}
+408
View File
@@ -0,0 +1,408 @@
import React, { useState } from 'react';
import { X, Plus, Trash } from 'lucide-react';
import { Node } from 'reactflow';
import { useFlowStore } from '../store/flowStore';
interface ConfigPanelProps {
node: Node | null;
onClose: () => void;
onUpdateNode: (id: string, data: any) => void;
}
interface Field {
name: string;
type: string;
validation?: string;
}
export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
const { models } = useFlowStore();
const [newField, setNewField] = useState<Field>({ name: '', type: 'string' });
if (!node) return null;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
onUpdateNode(node.id, {
...node.data,
[e.target.name]: e.target.value,
});
};
const handleArrayChange = (index: number, field: string, value: string, arrayName: string) => {
const array = [...(node.data[arrayName] || [])];
array[index] = { ...array[index], [field]: value };
onUpdateNode(node.id, {
...node.data,
[arrayName]: array,
});
};
const addField = (arrayName: string) => {
const array = [...(node.data[arrayName] || []), newField];
onUpdateNode(node.id, {
...node.data,
[arrayName]: array,
});
setNewField({ name: '', type: 'string' });
};
const removeField = (index: number, arrayName: string) => {
const array = [...(node.data[arrayName] || [])];
array.splice(index, 1);
onUpdateNode(node.id, {
...node.data,
[arrayName]: array,
});
};
const renderDatabaseFields = () => (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Model</label>
<select
name="model"
value={node.data.model || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="">Select Model</option>
{models.map((model) => (
<option key={model.id} value={model.id}>{model.name}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">SQL Query</label>
<textarea
name="query"
value={node.data.query || ''}
onChange={handleChange}
className="w-full p-2 border rounded h-32 font-mono text-sm"
placeholder="SELECT * FROM table WHERE id = :id"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Save Result In</label>
<input
type="text"
name="resultVar"
value={node.data.resultVar || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="result"
/>
</div>
</>
);
const renderFields = () => {
switch (node.type) {
case 'auth':
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Auth Type</label>
<select
name="authType"
value={node.data.authType || 'bearer'}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="bearer">Bearer Token</option>
<option value="basic">Basic Auth</option>
<option value="jwt">JWT</option>
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Token Variable</label>
<input
type="text"
name="tokenVar"
value={node.data.tokenVar || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="token"
/>
</div>
</>
);
case 'url':
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Method</label>
<select
name="method"
value={node.data.method || 'GET'}
onChange={handleChange}
className="w-full p-2 border rounded"
>
{['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].map(method => (
<option key={method} value={method}>{method}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Route Path</label>
<input
type="text"
name="path"
value={node.data.path || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="/api/users/:id"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Fields</label>
{(node.data.fields || []).map((field: Field, index: number) => (
<div key={index} className="flex gap-2 mb-2">
<input
type="text"
value={field.name}
onChange={(e) => handleArrayChange(index, 'name', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
placeholder="Field name"
/>
<select
value={field.type}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')}
className="w-24 p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
</select>
<input
type="text"
value={field.validation}
onChange={(e) => handleArrayChange(index, 'validation', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
placeholder="Validation"
/>
<button
onClick={() => removeField(index, 'fields')}
className="p-2 text-red-500 hover:bg-red-50 rounded"
>
<Trash className="w-4 h-4" />
</button>
</div>
))}
<div className="flex gap-2 mt-2">
<input
type="text"
value={newField.name}
onChange={(e) => setNewField({ ...newField, name: e.target.value })}
className="flex-1 p-2 border rounded"
placeholder="New field name"
/>
<select
value={newField.type}
onChange={(e) => setNewField({ ...newField, type: e.target.value })}
className="w-24 p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
</select>
<button
onClick={() => addField('fields')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</>
);
case 'output':
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Fields</label>
{(node.data.fields || []).map((field: Field, index: number) => (
<div key={index} className="flex gap-2 mb-2">
<input
type="text"
value={field.name}
onChange={(e) => handleArrayChange(index, 'name', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
placeholder="Field name"
/>
<select
value={field.type}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')}
className="w-24 p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
</select>
<button
onClick={() => removeField(index, 'fields')}
className="p-2 text-red-500 hover:bg-red-50 rounded"
>
<Trash className="w-4 h-4" />
</button>
</div>
))}
<div className="flex gap-2 mt-2">
<input
type="text"
value={newField.name}
onChange={(e) => setNewField({ ...newField, name: e.target.value })}
className="flex-1 p-2 border rounded"
placeholder="New field name"
/>
<select
value={newField.type}
onChange={(e) => setNewField({ ...newField, type: e.target.value })}
className="w-24 p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
</select>
<button
onClick={() => addField('fields')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</>
);
case 'variable':
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variable Name</label>
<input
type="text"
name="name"
value={node.data.name || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="myVariable"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Type</label>
<select
name="type"
value={node.data.type || 'string'}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="object">Object</option>
<option value="array">Array</option>
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Default Value</label>
<input
type="text"
name="defaultValue"
value={node.data.defaultValue || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="Default value"
/>
</div>
</>
);
case 'logic':
return (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">JavaScript Code</label>
<textarea
name="code"
value={node.data.code || ''}
onChange={handleChange}
className="w-full p-2 border rounded h-40 font-mono text-sm"
placeholder="// Write your JavaScript code here"
/>
</div>
);
case 'db-find':
case 'db-query':
return renderDatabaseFields();
case 'db-insert':
return (
<>
{renderDatabaseFields()}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variables</label>
<textarea
name="variables"
value={node.data.variables || ''}
onChange={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number"
/>
</div>
</>
);
case 'db-update':
case 'db-delete':
return (
<>
{renderDatabaseFields()}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">ID Field</label>
<input
type="text"
name="idField"
value={node.data.idField || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="id"
/>
</div>
{node.type === 'db-update' && (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variables</label>
<textarea
name="variables"
value={node.data.variables || ''}
onChange={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number"
/>
</div>
)}
</>
);
default:
return null;
}
};
return (
<div className="fixed right-0 top-0 h-full w-80 bg-white border-l border-gray-200 p-4 shadow-lg transform transition-transform overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Configure Node</h3>
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
<X className="w-5 h-5" />
</button>
</div>
{renderFields()}
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { Lock, Globe, ArrowUpDown, Code, Database, Variable } from 'lucide-react';
const CustomNode = ({ data, type }: NodeProps) => {
const getIcon = () => {
switch (type) {
case 'auth':
return <Lock className="w-5 h-5" />;
case 'url':
return <Globe className="w-5 h-5" />;
case 'output':
return <ArrowUpDown className="w-5 h-5" />;
case 'logic':
return <Code className="w-5 h-5" />;
case 'variable':
return <Variable className="w-5 h-5" />;
case 'db-find':
case 'db-insert':
case 'db-update':
case 'db-delete':
case 'db-query':
return <Database className="w-5 h-5" />;
default:
return null;
}
};
return (
<div className="relative px-4 py-2 shadow-md rounded-md bg-white border-2 border-gray-200 min-w-[180px] cursor-grab active:cursor-grabbing">
<Handle
type="target"
position={Position.Left}
className="!w-3 !h-3 !bg-blue-500 !border-2 !border-white"
/>
<div className="flex items-center">
<div className="rounded-full w-8 h-8 flex items-center justify-center bg-gray-50 mr-2">
{getIcon()}
</div>
<div>
<div className="text-sm font-bold">{data.label}</div>
<div className="text-xs text-gray-500">
{type.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</div>
</div>
</div>
<Handle
type="source"
position={Position.Right}
className="!w-3 !h-3 !bg-blue-500 !border-2 !border-white"
/>
</div>
);
};
export default memo(CustomNode);
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import ReactFlow, {
Background,
Controls,
ReactFlowProvider,
addEdge,
Connection,
useReactFlow,
applyNodeChanges,
applyEdgeChanges,
} from 'reactflow';
import { useFlowStore } from '../store/flowStore';
import { ConfigPanel } from './ConfigPanel';
import CustomNode from './CustomNode';
const nodeTypes = {
auth: CustomNode,
url: CustomNode,
output: CustomNode,
logic: CustomNode,
variable: CustomNode,
'db-find': CustomNode,
'db-insert': CustomNode,
'db-update': CustomNode,
'db-delete': CustomNode,
'db-query': CustomNode,
};
export function FlowEditor() {
const { project } = useReactFlow();
const {
nodes,
edges,
setNodes,
setEdges,
selectedNode,
setSelectedNode,
updateNodeData,
} = useFlowStore();
const onNodesChange = React.useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = React.useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = React.useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const onNodeClick = React.useCallback(
(_: React.MouseEvent, node: any) => {
setSelectedNode(node);
},
[setSelectedNode]
);
const onPaneClick = React.useCallback(() => {
setSelectedNode(null);
}, [setSelectedNode]);
return (
<div className="flex h-screen">
<div className="flex-1">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
fitView
>
<Background />
<Controls />
</ReactFlow>
</div>
<ConfigPanel
node={selectedNode}
onClose={() => setSelectedNode(null)}
onUpdateNode={updateNodeData}
/>
</div>
);
}
+254
View File
@@ -0,0 +1,254 @@
import React, { useState, useEffect } from 'react';
import { X, Plus, Trash } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
interface Field {
name: string;
type: string;
defaultValue: string;
validation: string;
mapping?: string;
}
interface ModelModalProps {
isOpen: boolean;
onClose: () => void;
model?: {
id: string;
name: string;
fields: Field[];
};
}
const fieldTypes = [
'string',
'number',
'boolean',
'date',
'mapping',
'array',
'object',
];
const validationRules = [
'required',
'email',
'url',
'min',
'max',
'pattern',
'enum',
];
const initialNewField = {
name: '',
type: 'string',
defaultValue: '',
validation: '',
};
const createInitialModelData = () => ({
id: `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: '',
fields: [],
});
export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
const [modelData, setModelData] = useState(createInitialModelData());
const [newField, setNewField] = useState<Field>(initialNewField);
const { addModel, updateModel } = useFlowStore();
useEffect(() => {
if (model) {
setModelData({
id: model.id,
name: model.name,
fields: [...model.fields],
});
} else {
setModelData(createInitialModelData());
}
}, [model, isOpen]);
const handleAddField = () => {
if (newField.name) {
setModelData({
...modelData,
fields: [...modelData.fields, { ...newField }],
});
setNewField(initialNewField);
}
};
const handleRemoveField = (index: number) => {
const updatedFields = [...modelData.fields];
updatedFields.splice(index, 1);
setModelData({
...modelData,
fields: updatedFields,
});
};
const handleSave = () => {
if (modelData.name) {
if (model) {
updateModel(modelData);
} else {
addModel(modelData);
}
onClose();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 className="text-lg font-semibold">
{model ? 'Edit Model' : 'Add New Model'}
</h2>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 overflow-y-auto">
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Model Name</label>
<input
type="text"
value={modelData.name}
onChange={(e) => setModelData({ ...modelData, name: e.target.value })}
className="w-full p-2 border rounded"
placeholder="Enter model name"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Fields</label>
<div className="space-y-2">
{modelData.fields.map((field, index) => (
<div key={index} className="flex gap-2 items-center">
<input
type="text"
value={field.name}
readOnly
className="flex-1 p-2 border rounded bg-gray-50"
/>
<span className="px-2 py-1 bg-gray-100 rounded text-sm">
{field.type}
</span>
{field.validation && (
<span className="px-2 py-1 bg-blue-100 rounded text-sm">
{field.validation}
</span>
)}
<button
onClick={() => handleRemoveField(index)}
className="p-1 text-red-500 hover:bg-red-50 rounded"
>
<Trash className="w-4 h-4" />
</button>
</div>
))}
</div>
<div className="mt-2 space-y-2">
<div className="flex gap-2">
<input
type="text"
value={newField.name}
onChange={(e) =>
setNewField({ ...newField, name: e.target.value })
}
className="flex-1 p-2 border rounded"
placeholder="Field name"
/>
<select
value={newField.type}
onChange={(e) =>
setNewField({ ...newField, type: e.target.value })
}
className="w-32 p-2 border rounded"
>
{fieldTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<input
type="text"
value={newField.defaultValue}
onChange={(e) =>
setNewField({ ...newField, defaultValue: e.target.value })
}
className="flex-1 p-2 border rounded"
placeholder="Default value"
/>
<select
value={newField.validation}
onChange={(e) =>
setNewField({ ...newField, validation: e.target.value })
}
className="w-32 p-2 border rounded"
>
<option value="">Validation</option>
{validationRules.map((rule) => (
<option key={rule} value={rule}>
{rule}
</option>
))}
</select>
</div>
{newField.type === 'mapping' && (
<input
type="text"
value={newField.mapping || ''}
onChange={(e) =>
setNewField({ ...newField, mapping: e.target.value })
}
className="w-full p-2 border rounded"
placeholder="key:value,key2:value2"
/>
)}
<button
onClick={handleAddField}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
<Plus className="w-4 h-4" />
Add Field
</button>
</div>
</div>
</div>
<div className="p-4 border-t border-gray-200 flex justify-end gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{model ? 'Update' : 'Save'} Model
</button>
</div>
</div>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
import React, { useState } from 'react';
import { Plus, Edit2 } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
import { ModelModal } from './ModelModal';
export function ModelPanel() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<any>(null);
const { models } = useFlowStore();
const handleEditModel = (model: any) => {
setSelectedModel(model);
setIsModalOpen(true);
};
return (
<div className="space-y-4">
<button
onClick={() => {
setSelectedModel(null);
setIsModalOpen(true);
}}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Plus className="w-4 h-4" />
Add Model
</button>
<div className="space-y-3">
{models.map((model) => (
<div
key={model.id}
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium">{model.name}</h3>
<p className="text-sm text-gray-500">
{model.fields.length} field{model.fields.length !== 1 ? 's' : ''}
</p>
</div>
<button
className="p-1 hover:bg-gray-100 rounded"
onClick={(e) => {
e.stopPropagation();
handleEditModel(model);
}}
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
<ModelModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedModel(null);
}}
model={selectedModel}
/>
</div>
);
}
+285
View File
@@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import { X, Trash2 } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
interface RoleModalProps {
isOpen: boolean;
onClose: () => void;
role?: {
id: string;
name: string;
slug: string;
permissions: {
authRequired: boolean;
routes: string[];
canCreateUsers: boolean;
canEditUsers: boolean;
canDeleteUsers: boolean;
canManageRoles: boolean;
};
};
}
const createInitialFormData = () => ({
id: `role_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: '',
slug: '',
permissions: {
authRequired: false,
routes: [],
canCreateUsers: false,
canEditUsers: false,
canDeleteUsers: false,
canManageRoles: false,
},
});
export function RoleModal({ isOpen, onClose, role }: RoleModalProps) {
const { routes, addRole, updateRole, deleteRole } = useFlowStore();
const [formData, setFormData] = useState(createInitialFormData());
useEffect(() => {
if (role) {
setFormData(role);
} else {
setFormData(createInitialFormData());
}
}, [role, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name.startsWith('permissions.')) {
const permissionKey = name.split('.')[1];
setFormData({
...formData,
permissions: {
...formData.permissions,
[permissionKey]: checked,
},
});
}
} else {
setFormData({
...formData,
[name]: value,
...(name === 'name' && !role
? {
slug: value
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, ''),
}
: {}),
});
}
};
const handleRouteChange = (routeId: string, checked: boolean) => {
setFormData({
...formData,
permissions: {
...formData.permissions,
routes: checked
? [...formData.permissions.routes, routeId]
: formData.permissions.routes.filter((id) => id !== routeId),
},
});
};
const handleSelectAllRoutes = (checked: boolean) => {
setFormData({
...formData,
permissions: {
...formData.permissions,
routes: checked ? routes.map((route) => route.id) : [],
},
});
};
const handleSave = () => {
if (role) {
updateRole(formData);
} else {
addRole(formData);
}
onClose();
};
const handleDelete = () => {
if (role) {
deleteRole(role.id);
onClose();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 className="text-lg font-semibold">
{role ? 'Edit Role' : 'Add New Role'}
</h2>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 overflow-y-auto">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Role Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="Enter role name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Role Slug</label>
<input
type="text"
name="slug"
value={formData.slug}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="role-slug"
readOnly={!!role}
/>
</div>
<div>
<h3 className="text-sm font-medium mb-2">Permissions</h3>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
name="permissions.authRequired"
checked={formData.permissions.authRequired}
onChange={handleChange}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Authentication Required</span>
</label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">Accessible Routes</h4>
<label className="flex items-center">
<input
type="checkbox"
checked={formData.permissions.routes.length === routes.length}
onChange={(e) => handleSelectAllRoutes(e.target.checked)}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Select All</span>
</label>
</div>
<div className="max-h-40 overflow-y-auto border rounded p-2 space-y-2">
{routes.map((route) => (
<label key={route.id} className="flex items-center">
<input
type="checkbox"
checked={formData.permissions.routes.includes(route.id)}
onChange={(e) => handleRouteChange(route.id, e.target.checked)}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">
{route.name} ({route.method} {route.url})
</span>
</label>
))}
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">User Management</h4>
<div className="space-y-2 ml-4">
<label className="flex items-center">
<input
type="checkbox"
name="permissions.canCreateUsers"
checked={formData.permissions.canCreateUsers}
onChange={handleChange}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Can Create Users</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
name="permissions.canEditUsers"
checked={formData.permissions.canEditUsers}
onChange={handleChange}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Can Edit Users</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
name="permissions.canDeleteUsers"
checked={formData.permissions.canDeleteUsers}
onChange={handleChange}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Can Delete Users</span>
</label>
</div>
</div>
<label className="flex items-center">
<input
type="checkbox"
name="permissions.canManageRoles"
checked={formData.permissions.canManageRoles}
onChange={handleChange}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm">Can Manage Roles</span>
</label>
</div>
</div>
</div>
</div>
<div className="p-4 border-t border-gray-200 flex justify-between">
{role && (
<button
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Delete Role
</button>
)}
<div className="flex gap-2 ml-auto">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{role ? 'Update' : 'Save'} Role
</button>
</div>
</div>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Plus, Edit2 } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
import { RoleModal } from './RoleModal';
export function RolesPanel() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<any>(null);
const { roles } = useFlowStore();
return (
<div className="space-y-4">
<button
onClick={() => {
setSelectedRole(null);
setIsModalOpen(true);
}}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Plus className="w-4 h-4" />
Add Role
</button>
<div className="space-y-3">
{roles.map((role) => (
<div
key={role.id}
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium">{role.name}</h3>
<p className="text-sm text-gray-500">{role.slug}</p>
</div>
<button
onClick={() => {
setSelectedRole(role);
setIsModalOpen(true);
}}
className="p-1 hover:bg-gray-100 rounded"
>
<Edit2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
<RoleModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedRole(null);
}}
role={selectedRole}
/>
</div>
);
}
+197
View File
@@ -0,0 +1,197 @@
import React, { useCallback, useRef, useEffect } from 'react';
import ReactFlow, {
Background,
Controls,
ReactFlowProvider,
addEdge,
Connection,
useReactFlow,
applyNodeChanges,
applyEdgeChanges,
Panel,
} from 'reactflow';
import { ArrowLeft, Save } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
import { ConfigPanel } from './ConfigPanel';
import { ComponentsPanel } from './ComponentsPanel';
import CustomNode from './CustomNode';
import 'reactflow/dist/style.css';
const nodeTypes = {
auth: CustomNode,
url: CustomNode,
output: CustomNode,
logic: CustomNode,
variable: CustomNode,
'db-find': CustomNode,
'db-insert': CustomNode,
'db-update': CustomNode,
'db-delete': CustomNode,
'db-query': CustomNode,
};
interface FlowEditorContentProps {
route: any;
onClose: () => void;
}
function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { project } = useReactFlow();
const {
nodes,
edges,
setNodes,
setEdges,
selectedNode,
setSelectedNode,
updateNodeData,
updateRoute,
} = useFlowStore();
useEffect(() => {
// Load saved flow data if it exists
if (route.flowData) {
setNodes(route.flowData.nodes);
setEdges(route.flowData.edges);
} else {
setNodes([]);
setEdges([]);
}
}, [route, setNodes, setEdges]);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (!type || !reactFlowWrapper.current) return;
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const position = project({
x: event.clientX - reactFlowBounds.left - 75,
y: event.clientY - reactFlowBounds.top - 25,
});
const newNode = {
id: `node_${Date.now()}`,
type,
position,
data: { label: type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ') },
};
setNodes((nds) => [...nds, newNode]);
},
[project, setNodes]
);
const onNodeClick = useCallback(
(_: React.MouseEvent, node: any) => {
setSelectedNode(node);
},
[setSelectedNode]
);
const onPaneClick = useCallback(() => {
setSelectedNode(null);
}, [setSelectedNode]);
const handleSave = () => {
updateRoute({
...route,
flowData: {
nodes,
edges,
},
});
onClose();
};
return (
<div className="flex flex-col h-full">
<div className="border-b border-gray-200 p-4 flex items-center justify-between bg-white">
<div className="flex items-center">
<button
onClick={onClose}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
>
<ArrowLeft className="w-5 h-5" />
Back to Routes
</button>
<h2 className="ml-4 text-lg font-semibold">
{route.name} ({route.method} {route.url})
</h2>
</div>
<button
onClick={handleSave}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
<Save className="w-4 h-4" />
Save Changes
</button>
</div>
<div className="flex flex-1 overflow-hidden">
<ComponentsPanel />
<div className="flex-1 h-full" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
deleteKeyCode="Delete"
multiSelectionKeyCode="Control"
selectionKeyCode="Shift"
snapToGrid={true}
snapGrid={[15, 15]}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
fitView
>
<Background />
<Controls />
</ReactFlow>
</div>
{selectedNode && (
<ConfigPanel
node={selectedNode}
onClose={() => setSelectedNode(null)}
onUpdateNode={updateNodeData}
/>
)}
</div>
</div>
);
}
export function RouteFlowEditor({ route, onClose }: FlowEditorContentProps) {
return (
<ReactFlowProvider>
<FlowEditorContent route={route} onClose={onClose} />
</ReactFlowProvider>
);
}
+148
View File
@@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react';
import { X, Trash2 } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
interface RouteModalProps {
isOpen: boolean;
onClose: () => void;
route?: {
id: string;
name: string;
url: string;
method: string;
};
}
const createInitialFormData = () => ({
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: '',
url: '',
method: 'GET',
});
export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
const { addRoute, updateRoute, deleteRoute } = useFlowStore();
const [formData, setFormData] = useState(createInitialFormData());
useEffect(() => {
if (route) {
setFormData(route);
} else {
setFormData(createInitialFormData());
}
}, [route, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSave = () => {
if (route) {
updateRoute(formData);
} else {
addRoute(formData);
}
onClose();
};
const handleDelete = () => {
if (route) {
deleteRoute(route.id);
onClose();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-md">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 className="text-lg font-semibold">
{route ? 'Edit Route' : 'Add New Route'}
</h2>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Route Name</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="Enter route name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Method</label>
<select
name="method"
value={formData.method}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">URL</label>
<input
type="text"
name="url"
value={formData.url}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="/api/resource/:id"
/>
</div>
</div>
</div>
<div className="p-4 border-t border-gray-200 flex justify-between">
{route && (
<button
onClick={handleDelete}
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Delete Route
</button>
)}
<div className="flex gap-2 ml-auto">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{route ? 'Update' : 'Save'} Route
</button>
</div>
</div>
</div>
</div>
);
}
+84
View File
@@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { Plus, Edit2, Code } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
import { RouteModal } from './RouteModal';
import { RouteFlowEditor } from './RouteFlowEditor';
export function RoutesPanel() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedRoute, setSelectedRoute] = useState<any>(null);
const [editingRoute, setEditingRoute] = useState<any>(null);
const { routes } = useFlowStore();
return (
<div className="space-y-4">
{!editingRoute ? (
<>
<button
onClick={() => {
setSelectedRoute(null);
setIsModalOpen(true);
}}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Plus className="w-4 h-4" />
Add Route
</button>
<div className="space-y-3">
{routes.map((route) => (
<div
key={route.id}
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium">{route.name}</h3>
<p className="text-sm text-gray-500">
{route.method} {route.url}
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setSelectedRoute(route);
setIsModalOpen(true);
}}
className="p-1 hover:bg-gray-100 rounded"
title="Edit Route"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => setEditingRoute(route)}
className="p-1 hover:bg-gray-100 rounded"
title="Edit Components"
>
<Code className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
<RouteModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedRoute(null);
}}
route={selectedRoute}
/>
</>
) : (
<div className="fixed inset-0 bg-white">
<RouteFlowEditor
route={editingRoute}
onClose={() => setEditingRoute(null)}
/>
</div>
)}
</div>
);
}
+145
View File
@@ -0,0 +1,145 @@
import React from 'react';
import { Save } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
export function SettingsForm() {
const { settings, updateSettings } = useFlowStore();
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
updateSettings({
...settings,
[e.target.name]: e.target.value,
});
};
const handleSave = () => {
updateSettings(settings);
};
return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Global Key</label>
<input
type="text"
name="globalKey"
value={settings?.globalKey || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="Enter global key"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Database Type</label>
<select
name="databaseType"
value={settings?.databaseType || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="">Select Database Type</option>
<option value="mysql">MySQL</option>
<option value="postgresql">PostgreSQL</option>
<option value="mongodb">MongoDB</option>
<option value="sqlite">SQLite</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Authentication Type</label>
<select
name="authType"
value={settings?.authType || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="session">Session</option>
<option value="jwt">JWT</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Timezone</label>
<select
name="timezone"
value={settings?.timezone || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
>
<option value="UTC">UTC</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
</select>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium">Database Credentials</label>
<div>
<input
type="text"
name="dbHost"
value={settings?.dbHost || ''}
onChange={handleChange}
className="w-full p-2 border rounded mb-2"
placeholder="Host"
/>
</div>
<div>
<input
type="text"
name="dbPort"
value={settings?.dbPort || ''}
onChange={handleChange}
className="w-full p-2 border rounded mb-2"
placeholder="Port"
/>
</div>
<div>
<input
type="text"
name="dbUser"
value={settings?.dbUser || ''}
onChange={handleChange}
className="w-full p-2 border rounded mb-2"
placeholder="Username"
/>
</div>
<div>
<input
type="password"
name="dbPassword"
value={settings?.dbPassword || ''}
onChange={handleChange}
className="w-full p-2 border rounded mb-2"
placeholder="Password"
/>
</div>
<div>
<input
type="text"
name="dbName"
value={settings?.dbName || ''}
onChange={handleChange}
className="w-full p-2 border rounded"
placeholder="Database Name"
/>
</div>
</div>
<button
onClick={handleSave}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
<Save className="w-4 h-4" />
Save Settings
</button>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Lock, Globe, ArrowUpDown, Code, Database, Variable, Plus, X } from 'lucide-react';
import { ModelPanel } from './ModelPanel';
import { SettingsForm } from './SettingsForm';
import { RolesPanel } from './RolesPanel';
import { RoutesPanel } from './RoutesPanel';
type Tab = 'models' | 'roles' | 'routes' | 'settings';
export function Sidebar() {
const [activeTab, setActiveTab] = useState<Tab>('models');
return (
<div className="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
{/* Tab Buttons */}
<div className="flex flex-wrap border-b border-gray-200">
<button
className={`flex-1 py-3 text-sm font-medium ${
activeTab === 'models'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('models')}
>
Models
</button>
<button
className={`flex-1 py-3 text-sm font-medium ${
activeTab === 'roles'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('roles')}
>
Roles
</button>
<button
className={`flex-1 py-3 text-sm font-medium ${
activeTab === 'routes'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('routes')}
>
Routes
</button>
<button
className={`flex-1 py-3 text-sm font-medium ${
activeTab === 'settings'
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
onClick={() => setActiveTab('settings')}
>
Settings
</button>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto p-4">
{activeTab === 'models' && <ModelPanel />}
{activeTab === 'roles' && <RolesPanel />}
{activeTab === 'routes' && <RoutesPanel />}
{activeTab === 'settings' && <SettingsForm />}
</div>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.react-flow__node {
@apply select-none;
}
.react-flow__handle {
width: 8px !important;
height: 8px !important;
background: #3b82f6 !important;
border: 2px solid white !important;
}
.react-flow__handle-left {
left: -4px !important;
}
.react-flow__handle-right {
right: -4px !important;
}
.react-flow__node-default {
width: auto !important;
padding: 8px 16px;
border-radius: 6px;
}
.react-flow__edge-path {
stroke: #64748b;
stroke-width: 2;
}
.react-flow__edge.selected .react-flow__edge-path {
stroke: #3b82f6;
stroke-width: 3;
}
.react-flow__node.selected {
box-shadow: 0 0 0 2px #3b82f6;
}
.react-flow__controls {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.react-flow__panel {
background: white;
border-radius: 4px;
}
.react-flow {
background-color: #f8fafc;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
+141
View File
@@ -0,0 +1,141 @@
import { create } from 'zustand';
import { Node, Edge } from 'reactflow';
interface Model {
id: string;
name: string;
fields: {
name: string;
type: string;
defaultValue: string;
validation: string;
mapping?: string;
}[];
}
interface Role {
id: string;
name: string;
slug: string;
permissions: {
authRequired: boolean;
routes: string[];
canCreateUsers?: boolean;
canEditUsers?: boolean;
canDeleteUsers?: boolean;
canManageRoles?: boolean;
};
}
interface Route {
id: string;
name: string;
url: string;
method: string;
flowData?: {
nodes: Node[];
edges: Edge[];
};
}
interface Settings {
globalKey: string;
databaseType: string;
authType: string;
timezone: string;
dbHost: string;
dbPort: string;
dbUser: string;
dbPassword: string;
dbName: string;
}
interface FlowState {
nodes: Node[];
edges: Edge[];
selectedNode: Node | null;
models: Model[];
roles: Role[];
routes: Route[];
settings: Settings;
setNodes: (nodes: Node[] | ((prev: Node[]) => Node[])) => void;
setEdges: (edges: Edge[] | ((prev: Edge[]) => Edge[])) => void;
setSelectedNode: (node: Node | null) => void;
updateNodeData: (nodeId: string, newData: any) => void;
addModel: (model: Model) => void;
updateModel: (model: Model) => void;
addRole: (role: Role) => void;
updateRole: (role: Role) => void;
deleteRole: (roleId: string) => void;
addRoute: (route: Route) => void;
updateRoute: (route: Route) => void;
deleteRoute: (routeId: string) => void;
updateSettings: (settings: Settings) => void;
}
export const useFlowStore = create<FlowState>((set) => ({
nodes: [],
edges: [],
selectedNode: null,
models: [],
roles: [],
routes: [],
settings: {
globalKey: '',
databaseType: '',
authType: 'session',
timezone: 'UTC',
dbHost: '',
dbPort: '',
dbUser: '',
dbPassword: '',
dbName: '',
},
setNodes: (nodes) => set((state) => ({
nodes: typeof nodes === 'function' ? nodes(state.nodes) : nodes
})),
setEdges: (edges) => set((state) => ({
edges: typeof edges === 'function' ? edges(state.edges) : edges
})),
setSelectedNode: (node) => set({ selectedNode: node }),
updateNodeData: (nodeId, newData) =>
set((state) => ({
nodes: state.nodes.map((node) =>
node.id === nodeId ? { ...node, data: { ...node.data, ...newData } } : node
),
})),
addModel: (model) =>
set((state) => ({
models: [...state.models, model],
})),
updateModel: (model) =>
set((state) => ({
models: state.models.map((m) => (m.id === model.id ? model : m)),
})),
addRole: (role) =>
set((state) => ({
roles: [...state.roles, role],
})),
updateRole: (role) =>
set((state) => ({
roles: state.roles.map((r) => (r.id === role.id ? role : r)),
})),
deleteRole: (roleId) =>
set((state) => ({
roles: state.roles.filter((r) => r.id !== roleId),
})),
addRoute: (route) =>
set((state) => ({
routes: [...state.routes, route],
})),
updateRoute: (route) =>
set((state) => ({
routes: state.routes.map((r) => (r.id === route.id ? route : r)),
})),
deleteRoute: (routeId) =>
set((state) => ({
routes: state.routes.filter((r) => r.id !== routeId),
})),
updateSettings: (settings) =>
set({ settings }),
}));
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />