2026-03-10 03:58:08 +00:00
|
|
|
import { IconDatabase } from "@tabler/icons-react";
|
|
|
|
|
import { useState } from "react";
|
2026-03-10 04:40:19 +00:00
|
|
|
import { Badge, Card, Col, Container, Row, Table } from "react-bootstrap";
|
2026-03-10 03:58:08 +00:00
|
|
|
import { useQuery } from "@tanstack/react-query";
|
2026-03-10 04:40:19 +00:00
|
|
|
import { getTables, getTableData } from "src/api/backend";
|
2026-03-10 03:58:08 +00:00
|
|
|
import { HasPermission, Loading } from "src/components";
|
|
|
|
|
import { ADMIN, VIEW } from "src/modules/Permissions";
|
|
|
|
|
|
|
|
|
|
export default function DatabaseManager() {
|
|
|
|
|
const [activeTable, setActiveTable] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const { data: tables, isLoading: tablesLoading } = useQuery({
|
|
|
|
|
queryKey: ["database-tables"],
|
|
|
|
|
queryFn: getTables,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: tableData, isLoading: tableDataLoading } = useQuery({
|
|
|
|
|
queryKey: ["database-table", activeTable, 0, 50],
|
|
|
|
|
queryFn: () => getTableData(activeTable as string, 0, 50),
|
2026-03-10 04:40:19 +00:00
|
|
|
enabled: !!activeTable,
|
2026-03-10 03:58:08 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Select the first table by default when tables are loaded
|
|
|
|
|
if (tables && tables.length > 0 && !activeTable) {
|
|
|
|
|
setActiveTable(tables[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 04:25:40 +00:00
|
|
|
const renderTableData = (data: any[]) => {
|
2026-03-10 03:58:08 +00:00
|
|
|
if (!data || data.length === 0) return <div className="text-muted p-3">No data</div>;
|
2026-03-10 04:25:40 +00:00
|
|
|
// In SQLite, raw SQL mapping might mismatch explicit schemas, so strictly read keys from the first row.
|
|
|
|
|
const columns = Object.keys(data[0]);
|
2026-03-10 03:58:08 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="table-responsive">
|
|
|
|
|
<Table striped bordered hover size="sm" className="mb-0 text-nowrap">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
{columns.map((col: string) => (
|
|
|
|
|
<th key={col}>{col}</th>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{data.map((row: any, i: number) => (
|
|
|
|
|
<tr key={i}>
|
|
|
|
|
{columns.map((col: string) => (
|
|
|
|
|
<td key={`${i}-${col}`}>
|
|
|
|
|
{row[col] === null ? (
|
|
|
|
|
<span className="text-muted">NULL</span>
|
|
|
|
|
) : typeof row[col] === "object" ? (
|
|
|
|
|
JSON.stringify(row[col])
|
|
|
|
|
) : (
|
|
|
|
|
String(row[col])
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
))}
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<HasPermission section={ADMIN} permission={VIEW}>
|
|
|
|
|
<Container className="my-4" fluid>
|
|
|
|
|
<div className="d-flex align-items-center mb-4">
|
|
|
|
|
<IconDatabase size={28} className="me-2 text-primary" />
|
|
|
|
|
<h2 className="mb-0">Database Manager</h2>
|
|
|
|
|
<Badge bg="danger" className="ms-3 rounded-pill text-uppercase" style={{ letterSpacing: "1px" }}>
|
|
|
|
|
Super Admin Only
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-10 04:40:19 +00:00
|
|
|
<Row>
|
|
|
|
|
<Col md={3}>
|
|
|
|
|
<Card className="shadow-sm">
|
|
|
|
|
<Card.Header className="bg-body-tertiary text-body fw-bold">Tables</Card.Header>
|
|
|
|
|
<div className="list-group list-group-flush" style={{ maxHeight: "70vh", overflowY: "auto" }}>
|
|
|
|
|
{tablesLoading && <div className="p-3"><Loading /></div>}
|
|
|
|
|
{tables?.map((table: string) => (
|
|
|
|
|
<button
|
|
|
|
|
key={table}
|
|
|
|
|
className={`list-group-item list-group-item-action ${activeTable === table ? "active" : ""}`}
|
|
|
|
|
onClick={() => setActiveTable(table)}
|
|
|
|
|
>
|
|
|
|
|
{table}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-03-10 03:58:08 +00:00
|
|
|
</div>
|
2026-03-10 04:40:19 +00:00
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col md={9}>
|
|
|
|
|
<Card className="shadow-sm">
|
|
|
|
|
<Card.Header className="bg-body-tertiary text-body d-flex justify-content-between align-items-center">
|
|
|
|
|
<h5 className="mb-0 fw-bold">{activeTable || "Select a table"}</h5>
|
|
|
|
|
{tableData && <Badge bg="secondary">{tableData.total} rows</Badge>}
|
|
|
|
|
</Card.Header>
|
|
|
|
|
<Card.Body className="p-0" style={{ maxHeight: "70vh", overflowY: "auto" }}>
|
|
|
|
|
{tableDataLoading ? (
|
|
|
|
|
<div className="p-5 d-flex justify-content-center"><Loading /></div>
|
|
|
|
|
) : (
|
|
|
|
|
tableData && renderTableData(tableData.rows)
|
|
|
|
|
)}
|
|
|
|
|
</Card.Body>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
2026-03-10 03:58:08 +00:00
|
|
|
</Container>
|
|
|
|
|
</HasPermission>
|
|
|
|
|
);
|
|
|
|
|
}
|