feat: add cross-database entity lookup for searching column values across all databases
Enables searching for a specific column value (e.g. carrier_id=123) across all databases on a PostgreSQL server. The backend creates temporary connection pools per database (semaphore-limited to 5), queries information_schema for matching columns, and executes read-only SELECTs with real-time progress events. Results are grouped by database/table in a new "Entity Lookup" tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
116
src/components/lookup/LookupResultGroup.tsx
Normal file
116
src/components/lookup/LookupResultGroup.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
Database,
|
||||
} from "lucide-react";
|
||||
import { ResultsTable } from "@/components/results/ResultsTable";
|
||||
import type { LookupDatabaseResult } from "@/types";
|
||||
|
||||
interface Props {
|
||||
dbResult: LookupDatabaseResult;
|
||||
}
|
||||
|
||||
export function LookupResultGroup({ dbResult }: Props) {
|
||||
const [expanded, setExpanded] = useState(dbResult.tables.length > 0);
|
||||
const [expandedTables, setExpandedTables] = useState<Set<string>>(
|
||||
() => new Set(dbResult.tables.map((t) => `${t.schema}.${t.table}`))
|
||||
);
|
||||
|
||||
const totalRows = dbResult.tables.reduce((s, t) => s + t.row_count, 0);
|
||||
const hasError = !!dbResult.error;
|
||||
const hasMatches = dbResult.tables.length > 0;
|
||||
|
||||
const toggleTable = (key: string) => {
|
||||
setExpandedTables((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-md">
|
||||
<button
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-accent/50"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
<Database className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="font-medium">{dbResult.database}</span>
|
||||
|
||||
{hasMatches && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{dbResult.tables.length} table{dbResult.tables.length !== 1 && "s"},{" "}
|
||||
{totalRows} row{totalRows !== 1 && "s"}
|
||||
</span>
|
||||
)}
|
||||
{hasError && (
|
||||
<span className="flex items-center gap-1 text-xs text-destructive">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
{dbResult.error}
|
||||
</span>
|
||||
)}
|
||||
{!hasError && !hasMatches && (
|
||||
<span className="text-xs text-muted-foreground">no matches</span>
|
||||
)}
|
||||
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{dbResult.search_time_ms}ms
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expanded && hasMatches && (
|
||||
<div className="border-t">
|
||||
{dbResult.tables.map((table) => {
|
||||
const key = `${table.schema}.${table.table}`;
|
||||
const isOpen = expandedTables.has(key);
|
||||
|
||||
return (
|
||||
<div key={key} className="border-b last:border-b-0">
|
||||
<button
|
||||
className="flex w-full items-center gap-2 px-5 py-1.5 text-left text-xs hover:bg-accent/50"
|
||||
onClick={() => toggleTable(key)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-3 w-3 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 shrink-0" />
|
||||
)}
|
||||
<span className="font-medium">
|
||||
{table.schema}.{table.table}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
({table.row_count} row{table.row_count !== 1 && "s"}
|
||||
{table.total_count > table.row_count &&
|
||||
`, ${table.total_count} total`}
|
||||
)
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
[{table.column_type}]
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && table.columns.length > 0 && (
|
||||
<div className="h-[200px] border-t">
|
||||
<ResultsTable
|
||||
columns={table.columns}
|
||||
types={table.types}
|
||||
rows={table.rows as unknown[][]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user