feat: scope Saved Queries panel to active connection + clean up on delete
Two changes that close the "everything lives in one global pool" gap.
UI: Saved Queries sidebar now defaults to "This connection" and lists
only entries with `connection_id == activeConnectionId` plus unattached
entries (legacy global saves before F2). A small toggle ("All") above
the search box brings the previous behavior back when copying queries
between databases. Each row in "All" mode shows a tag with the source
connection's name; legacy global entries show "unattached".
Backend: delete_connection now best-effort cleans persisted state for
that connection — removes `memory/<id>.md` (delete_memory_core in
memory.rs) and drops every entry in saved_queries.json with the
matching `connection_id` (delete_by_connection_core in
saved_queries.rs). Entries with `connection_id == None` are deliberately
preserved. Cleanup errors are logged but don't block the deletion since
connections.json is the source of truth.
Memory was already per-connection (F1); query history already filters
by connection. This commit makes saved queries behave the same and
stops orphan files from accumulating.
This commit is contained in:
@@ -1,19 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSavedQueries, useDeleteSavedQuery } from "@/hooks/use-saved-queries";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, Trash2, Bookmark } from "lucide-react";
|
||||
import type { Tab } from "@/types";
|
||||
import type { SavedQuery, Tab } from "@/types";
|
||||
|
||||
type Scope = "active" | "all";
|
||||
|
||||
export function SavedQueriesPanel() {
|
||||
const [search, setSearch] = useState("");
|
||||
const { activeConnectionId, currentDatabase, addTab } = useAppStore();
|
||||
const [scope, setScope] = useState<Scope>("active");
|
||||
|
||||
const { activeConnectionId, currentDatabase, addTab, connections } =
|
||||
useAppStore();
|
||||
const { data: queries } = useSavedQueries(search || undefined);
|
||||
const deleteMutation = useDeleteSavedQuery();
|
||||
|
||||
const handleOpen = (sql: string, connectionId?: string) => {
|
||||
const cid = activeConnectionId ?? connectionId ?? "";
|
||||
// Effective scope: if no connection is active, "active" has nothing to filter
|
||||
// against, so we silently broaden to all.
|
||||
const effectiveScope: Scope = activeConnectionId ? scope : "all";
|
||||
|
||||
const visible = useMemo(() => {
|
||||
if (!queries) return [];
|
||||
if (effectiveScope === "all") return queries;
|
||||
return queries.filter(
|
||||
(q) => !q.connection_id || q.connection_id === activeConnectionId
|
||||
);
|
||||
}, [queries, effectiveScope, activeConnectionId]);
|
||||
|
||||
const connectionName = (id: string | undefined): string | undefined => {
|
||||
if (!id) return undefined;
|
||||
return connections.find((c) => c.id === id)?.name ?? id.slice(0, 8);
|
||||
};
|
||||
|
||||
const handleOpen = (sql: string, queryConnectionId?: string) => {
|
||||
const cid = activeConnectionId ?? queryConnectionId ?? "";
|
||||
if (!cid) return;
|
||||
const tab: Tab = {
|
||||
id: crypto.randomUUID(),
|
||||
@@ -28,7 +50,7 @@ export function SavedQueriesPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
<div className="flex flex-col gap-1.5 p-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -38,45 +60,121 @@ export function SavedQueriesPanel() {
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{activeConnectionId && (
|
||||
<div className="flex gap-1">
|
||||
<ScopeButton
|
||||
active={scope === "active"}
|
||||
onClick={() => setScope("active")}
|
||||
>
|
||||
This connection
|
||||
</ScopeButton>
|
||||
<ScopeButton
|
||||
active={scope === "all"}
|
||||
onClick={() => setScope("all")}
|
||||
>
|
||||
All
|
||||
</ScopeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
{queries?.map((query) => (
|
||||
<div
|
||||
{visible.map((query) => (
|
||||
<QueryRow
|
||||
key={query.id}
|
||||
className="group flex w-full flex-col gap-0.5 border-b px-3 py-2 text-left text-xs hover:bg-accent cursor-pointer"
|
||||
onDoubleClick={() => handleOpen(query.sql, query.connection_id)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Bookmark className="h-3 w-3 shrink-0 text-blue-400" />
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{query.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-5 w-5 shrink-0 opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteMutation.mutate(query.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<span className="truncate font-mono text-muted-foreground">
|
||||
{query.sql.length > 80 ? query.sql.slice(0, 80) + "..." : query.sql}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{new Date(query.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
query={query}
|
||||
showConnectionTag={effectiveScope === "all"}
|
||||
connectionLabel={connectionName(query.connection_id)}
|
||||
onOpen={() => handleOpen(query.sql, query.connection_id)}
|
||||
onDelete={() => deleteMutation.mutate(query.id)}
|
||||
/>
|
||||
))}
|
||||
{(!queries || queries.length === 0) && (
|
||||
{visible.length === 0 && (
|
||||
<div className="py-8 text-center text-xs text-muted-foreground">
|
||||
No saved queries
|
||||
{effectiveScope === "active"
|
||||
? "No saved queries for this connection"
|
||||
: "No saved queries"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScopeButton({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`h-6 rounded px-2 text-[11px] transition-colors ${
|
||||
active
|
||||
? "bg-accent text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function QueryRow({
|
||||
query,
|
||||
showConnectionTag,
|
||||
connectionLabel,
|
||||
onOpen,
|
||||
onDelete,
|
||||
}: {
|
||||
query: SavedQuery;
|
||||
showConnectionTag: boolean;
|
||||
connectionLabel?: string;
|
||||
onOpen: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const tag = !query.connection_id
|
||||
? "unattached"
|
||||
: showConnectionTag
|
||||
? connectionLabel
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex w-full flex-col gap-0.5 border-b px-3 py-2 text-left text-xs hover:bg-accent cursor-pointer"
|
||||
onDoubleClick={onOpen}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Bookmark className="h-3 w-3 shrink-0 text-blue-400" />
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{query.name}
|
||||
</span>
|
||||
{tag && (
|
||||
<span className="shrink-0 rounded bg-muted/40 px-1 py-px text-[9px] uppercase tracking-wide text-muted-foreground">
|
||||
{tag}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-5 w-5 shrink-0 opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<span className="truncate font-mono text-muted-foreground">
|
||||
{query.sql.length > 80 ? query.sql.slice(0, 80) + "..." : query.sql}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{new Date(query.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user