- new design system in globals.css: warm graphite surfaces, ivory text, honey accent; semantic status/data-type/syntax tokens replacing hardcoded colors - IBM Plex Mono as the universal UI font (sans + mono), tabular numerals - custom CodeMirror SQL theme (src/lib/editor-theme.ts) matching the palette - data grid: zebra striping + honey row hover, stronger sticky header - route status dots, JSON syntax, EXPLAIN cost, schema-tree icons and the read/write toggle through the new tokens - TUSK wordmark in the toolbar
170 lines
5.2 KiB
TypeScript
170 lines
5.2 KiB
TypeScript
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
|
import { ConnectionList } from "@/components/connections/ConnectionList";
|
|
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
|
import { ReadOnlyToggle } from "@/components/layout/ReadOnlyToggle";
|
|
import { useAppStore } from "@/stores/app-store";
|
|
import { useConnections, useReconnect } from "@/hooks/use-connections";
|
|
import { toast } from "sonner";
|
|
import { Database, Plus, RefreshCw, Settings, Sparkles } from "lucide-react";
|
|
import type { ConnectionConfig, Tab } from "@/types";
|
|
import { getEnvironment } from "@/lib/environment";
|
|
import { AppSettingsSheet } from "@/components/settings/AppSettingsSheet";
|
|
|
|
export function Toolbar() {
|
|
const [listOpen, setListOpen] = useState(false);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [editingConn, setEditingConn] = useState<ConnectionConfig | null>(null);
|
|
const { activeConnectionId, currentDatabase, addTab } = useAppStore();
|
|
const { data: connections } = useConnections();
|
|
const reconnectMutation = useReconnect();
|
|
const activeConn = connections?.find((c) => c.id === activeConnectionId);
|
|
const activeEnv = getEnvironment(activeConn?.environment);
|
|
const activeColor = activeEnv?.color ?? activeConn?.color;
|
|
|
|
const handleReconnect = () => {
|
|
if (!activeConn) return;
|
|
reconnectMutation.mutate(activeConn, {
|
|
onSuccess: () => toast.success("Reconnected"),
|
|
onError: (err) => toast.error("Reconnect failed", { description: String(err) }),
|
|
});
|
|
};
|
|
|
|
const handleNewQuery = () => {
|
|
if (!activeConnectionId) return;
|
|
const tab: Tab = {
|
|
id: crypto.randomUUID(),
|
|
type: "query",
|
|
title: "New Query",
|
|
connectionId: activeConnectionId,
|
|
database: currentDatabase ?? undefined,
|
|
sql: "",
|
|
};
|
|
addTab(tab);
|
|
};
|
|
|
|
const handleNewChat = () => {
|
|
if (!activeConnectionId) return;
|
|
const tab: Tab = {
|
|
id: crypto.randomUUID(),
|
|
type: "chat",
|
|
title: "Chat",
|
|
connectionId: activeConnectionId,
|
|
database: currentDatabase ?? undefined,
|
|
};
|
|
addTab(tab);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="tusk-toolbar tusk-conn-strip flex h-10 items-center gap-1.5 px-3"
|
|
style={{
|
|
"--strip-width": activeColor ? "3px" : "0px",
|
|
"--strip-color": activeColor ?? "transparent",
|
|
} as React.CSSProperties}
|
|
>
|
|
<span
|
|
className="tusk-wordmark select-none px-1 text-[12px] text-primary"
|
|
style={{ textShadow: "0 0 10px oklch(0.808 0.124 82 / 35%)" }}
|
|
>
|
|
TUSK
|
|
</span>
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={() => setListOpen(true)}
|
|
>
|
|
<Database className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">Connections</span>
|
|
</Button>
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<ConnectionSelector />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={handleReconnect}
|
|
disabled={!activeConnectionId || reconnectMutation.isPending}
|
|
title="Reconnect"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<RefreshCw className={`h-3.5 w-3.5 ${reconnectMutation.isPending ? "animate-spin" : ""}`} />
|
|
</Button>
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<ReadOnlyToggle />
|
|
|
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={handleNewChat}
|
|
disabled={!activeConnectionId}
|
|
>
|
|
<Sparkles className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">Ask AI</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
|
onClick={handleNewQuery}
|
|
disabled={!activeConnectionId}
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
<span className="text-xs font-medium">New Query</span>
|
|
</Button>
|
|
|
|
<div className="flex-1" />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => setSettingsOpen(true)}
|
|
title="Settings"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<Settings className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<ConnectionList
|
|
open={listOpen}
|
|
onOpenChange={setListOpen}
|
|
onEdit={(conn) => {
|
|
setEditingConn(conn);
|
|
setDialogOpen(true);
|
|
}}
|
|
onNew={() => {
|
|
setEditingConn(null);
|
|
setDialogOpen(true);
|
|
}}
|
|
/>
|
|
|
|
<ConnectionDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
connection={editingConn}
|
|
/>
|
|
|
|
<AppSettingsSheet
|
|
open={settingsOpen}
|
|
onOpenChange={setSettingsOpen}
|
|
/>
|
|
</>
|
|
);
|
|
}
|