feat: add schema browser sidebar and app layout
Add SchemaTree with database/schema/category hierarchy, Sidebar with search, Toolbar, TabBar, StatusBar, and full app layout with resizable panels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
src/components/layout/Sidebar.tsx
Normal file
27
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SchemaTree } from "@/components/schema/SchemaTree";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
export function Sidebar() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-card">
|
||||
<div className="p-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search objects..."
|
||||
className="h-7 pl-7 text-xs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<SchemaTree />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/layout/StatusBar.tsx
Normal file
38
src/components/layout/StatusBar.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { useConnections } from "@/hooks/use-connections";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
rowCount?: number | null;
|
||||
executionTime?: number | null;
|
||||
}
|
||||
|
||||
export function StatusBar({ rowCount, executionTime }: Props) {
|
||||
const { activeConnectionId, connectedIds, pgVersion } = useAppStore();
|
||||
const { data: connections } = useConnections();
|
||||
|
||||
const activeConn = connections?.find((c) => c.id === activeConnectionId);
|
||||
const isConnected = activeConnectionId
|
||||
? connectedIds.has(activeConnectionId)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="flex h-6 items-center justify-between border-t bg-card px-3 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<Circle
|
||||
className={`h-2 w-2 ${isConnected ? "fill-green-500 text-green-500" : "fill-muted text-muted"}`}
|
||||
/>
|
||||
{activeConn ? activeConn.name : "No connection"}
|
||||
</span>
|
||||
{pgVersion && (
|
||||
<span className="hidden sm:inline">{pgVersion.split(",")[0]?.replace("PostgreSQL ", "PG ")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{rowCount != null && <span>{rowCount.toLocaleString()} rows</span>}
|
||||
{executionTime != null && <span>{executionTime} ms</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
src/components/layout/TabBar.tsx
Normal file
47
src/components/layout/TabBar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { X, Table2, Code, Columns } from "lucide-react";
|
||||
|
||||
export function TabBar() {
|
||||
const { tabs, activeTabId, setActiveTabId, closeTab } = useAppStore();
|
||||
|
||||
if (tabs.length === 0) return null;
|
||||
|
||||
const iconMap = {
|
||||
query: <Code className="h-3 w-3" />,
|
||||
table: <Table2 className="h-3 w-3" />,
|
||||
structure: <Columns className="h-3 w-3" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-b bg-card">
|
||||
<ScrollArea>
|
||||
<div className="flex">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`group flex h-8 cursor-pointer items-center gap-1.5 border-r px-3 text-xs ${
|
||||
activeTabId === tab.id
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setActiveTabId(tab.id)}
|
||||
>
|
||||
{iconMap[tab.type]}
|
||||
<span className="max-w-[120px] truncate">{tab.title}</span>
|
||||
<button
|
||||
className="ml-1 rounded-sm opacity-0 hover:bg-accent group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeTab(tab.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
src/components/layout/Toolbar.tsx
Normal file
86
src/components/layout/Toolbar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
||||
import { ConnectionList } from "@/components/connections/ConnectionList";
|
||||
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { Database, Plus } from "lucide-react";
|
||||
import type { ConnectionConfig, Tab } from "@/types";
|
||||
|
||||
export function Toolbar() {
|
||||
const [listOpen, setListOpen] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingConn, setEditingConn] = useState<ConnectionConfig | null>(null);
|
||||
const { activeConnectionId, addTab } = useAppStore();
|
||||
|
||||
const handleNewQuery = () => {
|
||||
if (!activeConnectionId) return;
|
||||
const tab: Tab = {
|
||||
id: crypto.randomUUID(),
|
||||
type: "query",
|
||||
title: "New Query",
|
||||
connectionId: activeConnectionId,
|
||||
sql: "",
|
||||
};
|
||||
addTab(tab);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-10 items-center gap-2 border-b px-3 bg-card">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
onClick={() => setListOpen(true)}
|
||||
>
|
||||
<Database className="h-3.5 w-3.5" />
|
||||
Connections
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
|
||||
<ConnectionSelector />
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
onClick={handleNewQuery}
|
||||
disabled={!activeConnectionId}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
New Query
|
||||
</Button>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<span className="text-xs font-semibold text-muted-foreground tracking-wide">
|
||||
TUSK
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ConnectionList
|
||||
open={listOpen}
|
||||
onOpenChange={setListOpen}
|
||||
onEdit={(conn) => {
|
||||
setEditingConn(conn);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
onNew={() => {
|
||||
setEditingConn(null);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConnectionDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
connection={editingConn}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user