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
+3
View File
@@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}
+8
View File
@@ -0,0 +1,8 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3
View File
@@ -0,0 +1,3 @@
# mkd-backend-flow-builder
[Edit in StackBlitz next generation editor ⚡️](https://stackblitz.com/~/github.com/mytechpassport/mkd-backend-flow-builder)
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4529
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reactflow": "^11.10.4",
"zustand": "^4.5.2"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+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" />
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});