mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +00:00
Add ComponentTreeRenderer and expand features.json with tab component trees
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -1133,6 +1133,347 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"TableManagerTab": {
|
||||
"component": "Box",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"variant": "h5",
|
||||
"gutterBottom": true,
|
||||
"text": "{{feature.name}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Typography",
|
||||
"condition": "feature.description",
|
||||
"props": {
|
||||
"variant": "body2",
|
||||
"color": "text.secondary",
|
||||
"gutterBottom": true,
|
||||
"text": "{{feature.description}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Box",
|
||||
"props": {
|
||||
"sx": { "mt": 2, "mb": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "Button",
|
||||
"condition": "canCreate",
|
||||
"props": {
|
||||
"variant": "contained",
|
||||
"startIcon": "Add",
|
||||
"onClick": "openCreateDialog",
|
||||
"sx": { "mr": 2 },
|
||||
"text": "Create Table"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Button",
|
||||
"condition": "canDelete",
|
||||
"props": {
|
||||
"variant": "outlined",
|
||||
"color": "error",
|
||||
"startIcon": "Delete",
|
||||
"onClick": "openDropDialog",
|
||||
"text": "Drop Table"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "Paper",
|
||||
"props": {
|
||||
"sx": { "mt": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "Box",
|
||||
"props": {
|
||||
"sx": { "p": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"variant": "h6",
|
||||
"gutterBottom": true,
|
||||
"text": "Existing Tables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "List",
|
||||
"children": [
|
||||
{
|
||||
"component": "ListItem",
|
||||
"forEach": "tables",
|
||||
"children": [
|
||||
{
|
||||
"component": "ListItemIcon",
|
||||
"children": [
|
||||
{
|
||||
"component": "IconButton",
|
||||
"props": {
|
||||
"size": "small"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"text": "📊"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "ListItemText",
|
||||
"props": {
|
||||
"primary": "{{table.table_name}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "ListItem",
|
||||
"condition": "tables.length === 0",
|
||||
"children": [
|
||||
{
|
||||
"component": "ListItemText",
|
||||
"props": {
|
||||
"primary": "No tables found"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ColumnManagerTab": {
|
||||
"component": "Box",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"variant": "h5",
|
||||
"gutterBottom": true,
|
||||
"text": "{{feature.name}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Typography",
|
||||
"condition": "feature.description",
|
||||
"props": {
|
||||
"variant": "body2",
|
||||
"color": "text.secondary",
|
||||
"gutterBottom": true,
|
||||
"text": "{{feature.description}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Paper",
|
||||
"props": {
|
||||
"sx": { "p": 2, "mb": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "FormControl",
|
||||
"props": {
|
||||
"fullWidth": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "InputLabel",
|
||||
"props": {
|
||||
"text": "Select Table"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Select",
|
||||
"props": {
|
||||
"value": "{{selectedTable}}",
|
||||
"label": "Select Table",
|
||||
"onChange": "handleTableChange"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "MenuItem",
|
||||
"forEach": "tables",
|
||||
"props": {
|
||||
"value": "{{table.table_name}}",
|
||||
"text": "{{table.table_name}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "Box",
|
||||
"condition": "selectedTable && canAdd",
|
||||
"props": {
|
||||
"sx": { "mb": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "Button",
|
||||
"condition": "canAdd",
|
||||
"props": {
|
||||
"variant": "contained",
|
||||
"startIcon": "Add",
|
||||
"onClick": "openAddDialog",
|
||||
"sx": { "mr": 1 },
|
||||
"text": "Add Column"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Button",
|
||||
"condition": "canModify",
|
||||
"props": {
|
||||
"variant": "outlined",
|
||||
"startIcon": "Edit",
|
||||
"onClick": "openModifyDialog",
|
||||
"sx": { "mr": 1 },
|
||||
"text": "Modify Column"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "Button",
|
||||
"condition": "canDelete",
|
||||
"props": {
|
||||
"variant": "outlined",
|
||||
"color": "error",
|
||||
"startIcon": "Delete",
|
||||
"onClick": "openDropDialog",
|
||||
"text": "Drop Column"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "Paper",
|
||||
"condition": "tableSchema && tableSchema.columns",
|
||||
"props": {
|
||||
"sx": { "mt": 2 }
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "TableContainer",
|
||||
"children": [
|
||||
{
|
||||
"component": "Table",
|
||||
"props": {
|
||||
"size": "small"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"component": "TableHead",
|
||||
"children": [
|
||||
{
|
||||
"component": "TableRow",
|
||||
"children": [
|
||||
{
|
||||
"component": "TableCell",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"text": "Column Name"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"text": "Data Type"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"text": "Nullable"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"children": [
|
||||
{
|
||||
"component": "Typography",
|
||||
"props": {
|
||||
"text": "Default"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "TableBody",
|
||||
"children": [
|
||||
{
|
||||
"component": "TableRow",
|
||||
"forEach": "tableSchema.columns",
|
||||
"children": [
|
||||
{
|
||||
"component": "TableCell",
|
||||
"props": {
|
||||
"text": "{{column.column_name}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"props": {
|
||||
"text": "{{column.data_type}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"props": {
|
||||
"text": "{{column.is_nullable}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "TableCell",
|
||||
"props": {
|
||||
"text": "{{column.column_default || '-'}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"componentProps": {
|
||||
|
||||
270
src/utils/ComponentTreeRenderer.tsx
Normal file
270
src/utils/ComponentTreeRenderer.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Chip,
|
||||
Tooltip,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TableChartIcon from '@mui/icons-material/TableChart';
|
||||
import SpeedIcon from '@mui/icons-material/Speed';
|
||||
import { ComponentNode } from './featureConfig';
|
||||
|
||||
// Map of component names to actual components
|
||||
const componentMap: Record<string, React.ComponentType<any>> = {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Chip,
|
||||
Tooltip,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
};
|
||||
|
||||
// Map of icon names to icon components
|
||||
const iconMap: Record<string, React.ComponentType<any>> = {
|
||||
Add: AddIcon,
|
||||
Delete: DeleteIcon,
|
||||
Edit: EditIcon,
|
||||
TableChart: TableChartIcon,
|
||||
Speed: SpeedIcon,
|
||||
};
|
||||
|
||||
type ComponentTreeRendererProps = {
|
||||
tree: ComponentNode;
|
||||
data?: Record<string, any>;
|
||||
handlers?: Record<string, (...args: any[]) => void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate a condition string with the provided data context
|
||||
*/
|
||||
function evaluateCondition(condition: string, data: Record<string, any>): boolean {
|
||||
try {
|
||||
// Create a function that evaluates the condition in the data context
|
||||
const func = new Function(...Object.keys(data), `return ${condition}`);
|
||||
return func(...Object.values(data));
|
||||
} catch (error) {
|
||||
console.error('Error evaluating condition:', condition, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate template strings like {{variable}} with actual values from data
|
||||
*/
|
||||
function interpolateValue(value: any, data: Record<string, any>): any {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Check if it's a template string
|
||||
const templateMatch = value.match(/^\{\{(.+)\}\}$/);
|
||||
if (templateMatch) {
|
||||
const expression = templateMatch[1].trim();
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${expression}`);
|
||||
return func(...Object.values(data));
|
||||
} catch (error) {
|
||||
console.error('Error evaluating expression:', expression, error);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace inline templates
|
||||
return value.replace(/\{\{(.+?)\}\}/g, (_, expression) => {
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${expression.trim()}`);
|
||||
return func(...Object.values(data));
|
||||
} catch (error) {
|
||||
console.error('Error evaluating inline expression:', expression, error);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate all props in an object
|
||||
*/
|
||||
function interpolateProps(
|
||||
props: Record<string, any> | undefined,
|
||||
data: Record<string, any>,
|
||||
handlers: Record<string, (...args: any[]) => void>
|
||||
): Record<string, any> {
|
||||
if (!props) return {};
|
||||
|
||||
const interpolated: Record<string, any> = {};
|
||||
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (typeof value === 'string' && handlers[value]) {
|
||||
// If the value is a handler function name, use the handler
|
||||
interpolated[key] = handlers[value];
|
||||
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Recursively interpolate nested objects
|
||||
interpolated[key] = interpolateProps(value, data, handlers);
|
||||
} else {
|
||||
// Interpolate the value
|
||||
interpolated[key] = interpolateValue(value, data);
|
||||
}
|
||||
});
|
||||
|
||||
return interpolated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singular form of a plural word (simple implementation)
|
||||
*/
|
||||
function getSingular(plural: string): string {
|
||||
if (plural.endsWith('ies')) {
|
||||
return plural.slice(0, -3) + 'y';
|
||||
}
|
||||
if (plural.endsWith('es')) {
|
||||
return plural.slice(0, -2);
|
||||
}
|
||||
if (plural.endsWith('s')) {
|
||||
return plural.slice(0, -1);
|
||||
}
|
||||
return plural;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single component node
|
||||
*/
|
||||
function renderNode(
|
||||
node: ComponentNode,
|
||||
data: Record<string, any>,
|
||||
handlers: Record<string, (...args: any[]) => void>,
|
||||
key: string | number
|
||||
): React.ReactNode {
|
||||
// Evaluate condition
|
||||
if (node.condition && !evaluateCondition(node.condition, data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle forEach loops
|
||||
if (node.forEach) {
|
||||
const items = data[node.forEach];
|
||||
if (!Array.isArray(items)) {
|
||||
console.warn(`forEach data "${node.forEach}" is not an array`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const singularName = getSingular(node.forEach);
|
||||
return items.map((item, index) => {
|
||||
const itemData = { ...data, [singularName]: item, index };
|
||||
// Remove forEach from node to avoid infinite loop
|
||||
const nodeWithoutForEach = { ...node, forEach: undefined };
|
||||
return renderNode(nodeWithoutForEach, itemData, handlers, `${key}-${index}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Get the component
|
||||
const componentName = interpolateValue(node.component, data);
|
||||
const Component = componentMap[componentName];
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Component "${componentName}" not found in componentMap`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Interpolate props
|
||||
const props = interpolateProps(node.props, data, handlers);
|
||||
|
||||
// Handle special props
|
||||
if (props.startIcon && typeof props.startIcon === 'string') {
|
||||
const IconComponent = iconMap[props.startIcon];
|
||||
if (IconComponent) {
|
||||
props.startIcon = <IconComponent />;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.endIcon && typeof props.endIcon === 'string') {
|
||||
const IconComponent = iconMap[props.endIcon];
|
||||
if (IconComponent) {
|
||||
props.endIcon = <IconComponent />;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 'text' prop for Typography and Button
|
||||
let textContent = null;
|
||||
if (props.text !== undefined) {
|
||||
textContent = props.text;
|
||||
delete props.text;
|
||||
}
|
||||
|
||||
// Render children
|
||||
const children = node.children?.map((child, index) =>
|
||||
renderNode(child, data, handlers, `${key}-child-${index}`)
|
||||
);
|
||||
|
||||
return (
|
||||
<Component key={key} {...props}>
|
||||
{textContent}
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ComponentTreeRenderer - Renders a component tree from JSON configuration
|
||||
*/
|
||||
export default function ComponentTreeRenderer({
|
||||
tree,
|
||||
data = {},
|
||||
handlers = {},
|
||||
}: ComponentTreeRendererProps) {
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{renderNode(tree, data, handlers, 'root')}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user