* chore: bump version to 0.7.5 (Task 1.1) * feat(db): pure object DDL/search/depend builders (Task 1.2) * feat(models): ObjectSearchHit, DependencyInfo, PgToolPaths, tool source fields (Task 1.3) * feat(backup): bundle-aware pg tool resolution, system-first fallback (Task 2.1) * feat(objects): schema CRUD commands + integration test (Task 2.2) * feat(objects): search_objects command (current-schema, all types) (Task 2.3) * feat(objects): get_object_ddl for all browsable types (Task 2.4) * feat(objects): pg_depend object dependencies + schema contents (Task 2.5) * feat(ipc): register object management commands (Task 3.1) * feat(ipc): frontend wrappers + types for object management (Task 3.2) * feat(build): declare bundled pg_tools resources (Task 3.3) * test(capabilities): lock objects capability PG-only for search/ddl/dependencies (Task 3.4) * feat(ui): Cmd+K object search palette in DB viewer (Task 4.1) * feat(ui): schema CRUD menu + dependency dialog (Task 4.2) * feat(ui): copy-as-DDL + dependency view context menu in Objects (Task 4.3) * feat(ui): dependency check before table drop + bundled-tool status (Task 4.4) * docs: v0.7.5 release notes + status table + bundled-tools (Task 5.1) * ci: build/verify bundled pg client tools per platform before tauri build (Task 5.2) * fix(objects): report pg_rewrite dependencies as the dependent view (pg_class) pg_depend records view dependencies via the view's internal rewrite rule (classid = pg_rewrite). The user-facing dependent object is the VIEW itself, so map that classid to pg_class (name still resolved through ev_class). Fixes the live-PG integration test object_dependencies_for_table_includes_view and makes the dependency dialog readable for view dependents. * test: add idempotent PG integration-test seed script Seeds the first PG test db (GRIDLINE_TEST_SRC) with the objects the #[ignore] integration tests assert against: users (+users_id_seq), products (3 rows), orders (3 rows), order_summary view, audit_log + get_user functions, user_role enum. Idempotent: DROP + recreate. * fix(build): regenerate multi-resolution icons + roadmap Keychain item - icon.ico previously contained a single 16x16 frame (Windows scaled it up -> blurry taskbar/start-menu icon). Regenerated from the 512px source via 'tauri icon': ICO now has 16/24/32/48/64/256 frames, icns has full @2x coverage up to 1024px, PNGs re-rendered from the same source. - Added icons/icon.png (512px) to bundle.icon so Linux hicolor installs a high-DPI entry. - ROADMAP: log the 'Enable Keychain' toggle (currently a form-only placeholder) under Next up -> Connection & credentials. * docs(readme): dynamic version-free download links + 3-col table - release.yml: set releaseAssetNamePattern to '[name]_[platform]_[arch][setup][ext]' (version-free), so asset filenames are stable across releases: Gridline_darwin_aarch64.dmg, Gridline_windows_x64-setup.exe, Gridline_linux_amd64.deb, Gridline_linux_x86_64.rpm, etc. - README: both download tables now 3-column (OS | Architecture | Download) and link via GitHub's releases/latest/download/<file> redirect — they always point at the newest published release, no per-release edits. - AGENTS.md: releases checklist updated — download links stay version-free. * docs(readme): platform-per-column download table (macOS | Windows | Linux) Table now mirrors the release layout: one column per OS with a logo row and a download-links row underneath. Links stay version-free via releases/latest/download/<file> (releaseAssetNamePattern in release.yml). * docs(readme): revert download table to clean image-less 3-col layout Logo row was hard to read on GitHub dark mode; the plain OS | Architecture | Download table is cleaner and still uses dynamic releases/latest links.
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { Search } from "lucide-react";
|
|
import { useDbViewerStore } from "../../stores/dbViewerStore";
|
|
import * as cmd from "../../lib/commands";
|
|
import type { ObjectSearchHit, ObjectType } from "../../lib/types";
|
|
|
|
const TYPE_TO_OBJECTS: Record<string, ObjectType> = {
|
|
FUNCTION: "functions",
|
|
PROCEDURE: "procedures",
|
|
TRIGGER: "triggers",
|
|
SEQUENCE: "sequences",
|
|
ENUM: "enums",
|
|
EXTENSION: "extensions",
|
|
INDEX: "indexes",
|
|
CONSTRAINT: "constraints",
|
|
};
|
|
|
|
interface ObjectSearchPaletteProps {
|
|
connectionId: string;
|
|
}
|
|
|
|
export function ObjectSearchPalette({ connectionId }: ObjectSearchPaletteProps) {
|
|
const open = useDbViewerStore((s) => s.objectSearchOpen);
|
|
const setOpen = useDbViewerStore((s) => s.setObjectSearchOpen);
|
|
const storeSchema = useDbViewerStore((s) => s.currentSchema);
|
|
const currentSchema = storeSchema ?? "public";
|
|
const openTab = useDbViewerStore((s) => s.openTab);
|
|
const setCurrentSchema = useDbViewerStore((s) => s.setCurrentSchema);
|
|
const setSelectedObjectType = useDbViewerStore((s) => s.setSelectedObjectType);
|
|
|
|
const [query, setQuery] = useState("");
|
|
const [hits, setHits] = useState<ObjectSearchHit[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
// Clear the transient state whenever the palette is closed so it reopens
|
|
// with an empty search and no stale results.
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setQuery("");
|
|
setHits([]);
|
|
}
|
|
}, [open]);
|
|
|
|
// Debounced search against the current schema.
|
|
useEffect(() => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
|
|
if (!query.trim()) {
|
|
setHits([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
timerRef.current = setTimeout(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const results = await cmd.searchObjects(
|
|
connectionId,
|
|
currentSchema,
|
|
query,
|
|
);
|
|
setHits(results);
|
|
} catch {
|
|
setHits([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, 150);
|
|
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
};
|
|
}, [query, connectionId, currentSchema]);
|
|
|
|
// Esc closes the palette.
|
|
useEffect(() => {
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
window.addEventListener("keydown", onKeyDown);
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
}, [setOpen]);
|
|
|
|
if (!open) return null;
|
|
|
|
const grouped = hits.reduce<Record<string, ObjectSearchHit[]>>((acc, hit) => {
|
|
const list = acc[hit.object_type] ?? [];
|
|
list.push(hit);
|
|
acc[hit.object_type] = list;
|
|
return acc;
|
|
}, {});
|
|
|
|
const handleSelect = (hit: ObjectSearchHit) => {
|
|
if (
|
|
hit.object_type === "TABLE" ||
|
|
hit.object_type === "VIEW" ||
|
|
hit.object_type === "MATERIALIZED VIEW"
|
|
) {
|
|
openTab(hit.schema, hit.name);
|
|
} else {
|
|
setCurrentSchema(hit.schema);
|
|
setSelectedObjectType(
|
|
TYPE_TO_OBJECTS[hit.object_type] ?? "functions",
|
|
);
|
|
}
|
|
setOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 pt-24"
|
|
onClick={() => setOpen(false)}
|
|
role="dialog"
|
|
aria-label="Search objects"
|
|
>
|
|
<div
|
|
className="w-[520px] max-h-[60vh] overflow-auto rounded-xl border border-border bg-surface shadow-xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
<Search size={14} className="text-text-muted" />
|
|
<input
|
|
autoFocus
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder="Search objects in current schema…"
|
|
className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted"
|
|
/>
|
|
{loading && (
|
|
<span className="text-xs text-text-muted">loading</span>
|
|
)}
|
|
</div>
|
|
|
|
{Object.entries(grouped).map(([type, list]) => (
|
|
<div key={type}>
|
|
<div className="px-3 py-1 text-[10px] uppercase text-text-subtle">
|
|
{type}
|
|
</div>
|
|
{list.map((hit) => (
|
|
<button
|
|
key={`${hit.object_type}:${hit.schema}:${hit.name}`}
|
|
type="button"
|
|
onClick={() => handleSelect(hit)}
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-text hover:bg-surface-raised"
|
|
>
|
|
<span className="truncate">{hit.name}</span>
|
|
<span className="text-[10px] text-text-subtle">
|
|
{hit.schema}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
))}
|
|
|
|
{!loading && query.trim() && hits.length === 0 && (
|
|
<div className="px-3 py-4 text-sm text-text-muted">No matches</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |