mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +00:00
Merge pull request #23 from johndoe6345789/copilot/refactor-app-folder-structure
Refactor admin dashboard to be config-driven using features.json
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -81,7 +81,7 @@
|
||||
"storybook": "^10.1.11",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15",
|
||||
"vitest-browser-react": "^2.0.2"
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"storybook": "^10.1.11",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15",
|
||||
"vitest-browser-react": "^2.0.2"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,369 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import TableChartIcon from '@mui/icons-material/TableChart';
|
||||
import {
|
||||
Alert,
|
||||
AppBar,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { theme } from '@/utils/theme';
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
|
||||
type TabPanelProps = {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`tabpanel-${index}`}
|
||||
aria-labelledby={`tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const router = useRouter();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [tables, setTables] = useState<any[]>([]);
|
||||
const [selectedTable, setSelectedTable] = useState<string>('');
|
||||
const [queryText, setQueryText] = useState('');
|
||||
const [queryResult, setQueryResult] = useState<any>(null);
|
||||
const [tableSchema, setTableSchema] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
|
||||
// Dialog states
|
||||
const [openCreateDialog, setOpenCreateDialog] = useState(false);
|
||||
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [editingRecord, setEditingRecord] = useState<any>(null);
|
||||
const [deletingRecord, setDeletingRecord] = useState<any>(null);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
|
||||
const fetchTables = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/tables');
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
router.push('/admin/login');
|
||||
return;
|
||||
}
|
||||
throw new Error('Failed to fetch tables');
|
||||
}
|
||||
const data = await response.json();
|
||||
setTables(data.tables);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTables();
|
||||
}, [fetchTables]);
|
||||
|
||||
const handleTableClick = async (tableName: string) => {
|
||||
setSelectedTable(tableName);
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccessMessage('');
|
||||
setQueryResult(null);
|
||||
|
||||
try {
|
||||
// Fetch table data
|
||||
const dataResponse = await fetch('/api/admin/table-data', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ tableName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await dataResponse.json();
|
||||
throw new Error(data.error || 'Query failed');
|
||||
}
|
||||
|
||||
const data = await dataResponse.json();
|
||||
setQueryResult(data);
|
||||
|
||||
// Fetch table schema
|
||||
const schemaResponse = await fetch('/api/admin/table-schema', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ tableName }),
|
||||
});
|
||||
|
||||
if (schemaResponse.ok) {
|
||||
const schemaData = await schemaResponse.json();
|
||||
setTableSchema(schemaData);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuerySubmit = async () => {
|
||||
if (!queryText.trim()) {
|
||||
setError('Please enter a query');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setQueryResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query: queryText }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Query failed');
|
||||
}
|
||||
|
||||
setQueryResult(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/admin/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
router.push('/admin/login');
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{ zIndex: theme => theme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<Toolbar>
|
||||
<StorageIcon sx={{ mr: 2 }} />
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
Postgres Admin Panel
|
||||
</Typography>
|
||||
<Button color="inherit" onClick={handleLogout} startIcon={<LogoutIcon />}>
|
||||
Logout
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
'width': DRAWER_WIDTH,
|
||||
'flexShrink': 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: DRAWER_WIDTH,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={() => setTabValue(0)}>
|
||||
<ListItemIcon>
|
||||
<StorageIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Tables" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={() => setTabValue(1)}>
|
||||
<ListItemIcon>
|
||||
<CodeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="SQL Query" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
bgcolor: 'background.default',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Database Tables
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ mt: 2, mb: 2 }}>
|
||||
<List>
|
||||
{tables.map(table => (
|
||||
<ListItem key={table.table_name} disablePadding>
|
||||
<ListItemButton onClick={() => handleTableClick(table.table_name)}>
|
||||
<ListItemIcon>
|
||||
<StorageIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={table.table_name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
{selectedTable && (
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Table:
|
||||
{' '}
|
||||
{selectedTable}
|
||||
</Typography>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
SQL Query Interface
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={6}
|
||||
label="SQL Query (SELECT only)"
|
||||
variant="outlined"
|
||||
value={queryText}
|
||||
onChange={e => setQueryText(e.target.value)}
|
||||
placeholder="SELECT * FROM your_table LIMIT 10;"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleQuerySubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : 'Execute Query'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{queryResult && !loading && (
|
||||
<Paper sx={{ mt: 2, overflow: 'auto' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Rows returned:
|
||||
{' '}
|
||||
{queryResult.rowCount}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{queryResult.fields?.map((field: any) => (
|
||||
<TableCell key={field.name}>
|
||||
<strong>{field.name}</strong>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{queryResult.rows?.map((row: any, idx: number) => (
|
||||
<TableRow key={idx}>
|
||||
{queryResult.fields?.map((field: any) => (
|
||||
<TableCell key={field.name}>
|
||||
{row[field.name] !== null
|
||||
? String(row[field.name])
|
||||
: 'NULL'}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
72
src/components/admin/SQLQueryTab.tsx
Normal file
72
src/components/admin/SQLQueryTab.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { getFeatureById } from '@/utils/featureConfig';
|
||||
|
||||
type SQLQueryTabProps = {
|
||||
onExecuteQuery: (query: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export default function SQLQueryTab({ onExecuteQuery }: SQLQueryTabProps) {
|
||||
const [queryText, setQueryText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Get feature configuration from JSON
|
||||
const feature = getFeatureById('sql-query');
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!queryText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onExecuteQuery(queryText);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{feature?.name || 'SQL Query Interface'}
|
||||
</Typography>
|
||||
|
||||
{feature?.description && (
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{feature.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={6}
|
||||
label="SQL Query (SELECT only)"
|
||||
variant="outlined"
|
||||
value={queryText}
|
||||
onChange={e => setQueryText(e.target.value)}
|
||||
placeholder="SELECT * FROM your_table LIMIT 10;"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleExecute}
|
||||
disabled={loading || !queryText.trim()}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : 'Execute Query'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
72
src/components/admin/TablesTab.tsx
Normal file
72
src/components/admin/TablesTab.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { getFeatureById } from '@/utils/featureConfig';
|
||||
|
||||
type TablesTabProps = {
|
||||
tables: Array<{ table_name: string }>;
|
||||
selectedTable: string;
|
||||
onTableClick: (tableName: string) => void;
|
||||
};
|
||||
|
||||
export default function TablesTab({
|
||||
tables,
|
||||
selectedTable,
|
||||
onTableClick,
|
||||
}: TablesTabProps) {
|
||||
// Get feature configuration from JSON
|
||||
const feature = getFeatureById('database-crud');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{feature?.name || 'Database Tables'}
|
||||
</Typography>
|
||||
|
||||
{feature?.description && (
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{feature.description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Paper sx={{ mt: 2, mb: 2 }}>
|
||||
<List>
|
||||
{tables.map(table => (
|
||||
<ListItem key={table.table_name} disablePadding>
|
||||
<ListItemButton
|
||||
selected={selectedTable === table.table_name}
|
||||
onClick={() => onTableClick(table.table_name)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<StorageIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={table.table_name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{tables.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary="No tables found" />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
{selectedTable && (
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Table: {selectedTable}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -2863,6 +2863,12 @@
|
||||
"icon": "TableChart",
|
||||
"featureId": "table-management"
|
||||
},
|
||||
{
|
||||
"id": "column-manager",
|
||||
"label": "Column Manager",
|
||||
"icon": "ViewColumn",
|
||||
"featureId": "column-management"
|
||||
},
|
||||
{
|
||||
"id": "constraints",
|
||||
"label": "Constraints",
|
||||
|
||||
Reference in New Issue
Block a user