v0.7.5: bundled PG tools, schema CRUD, object search, copy-as-DDL, object dependencies (#11)

* 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.
This commit is contained in:
2026-08-05 20:46:36 +08:00
committed by GitHub
parent cbc54119ed
commit 04e8ed3300
67 changed files with 2084 additions and 108 deletions
+118 -12
View File
@@ -6,6 +6,7 @@ import {
GitBranch,
ListChecks,
ListOrdered,
MoreVertical,
SquareFunction,
Tag,
Puzzle,
@@ -15,6 +16,7 @@ import {
} from "lucide-react";
import { useDbViewerStore } from "../../stores/dbViewerStore";
import { SelectDropdown } from "../ui/SelectDropdown";
import { DependencyDialog } from "./DependencyDialog";
import * as cmd from "../../lib/commands";
import type {
FunctionInfo,
@@ -24,17 +26,11 @@ import type {
ExtensionInfo,
IndexInfo,
ConstraintInfo,
ObjectType,
DependencyInfo,
} from "../../lib/types";
export type ObjectType =
| "functions"
| "triggers"
| "sequences"
| "enums"
| "extensions"
| "indexes"
| "constraints"
| "procedures";
interface ObjectExplorerPageProps {
connectionId: string;
@@ -120,6 +116,28 @@ function itemLabel(item: AnyObject): string {
return name;
}
function schemaOf(item: AnyObject): string {
return item.schema;
}
function objectName(item: AnyObject): string {
return item.name;
}
function typeToDdlType(type: ObjectType): string {
const map: Record<ObjectType, string> = {
functions: "function",
procedures: "procedure",
triggers: "trigger",
sequences: "sequence",
enums: "enum",
extensions: "extension",
indexes: "index",
constraints: "constraint",
};
return map[type];
}
// ─── syntax highlighting for PL/pgSQL / SQL ──────────────
const SQL_KEYWORDS = new Set([
@@ -1020,7 +1038,8 @@ function renderDetail(type: ObjectType, item: AnyObject) {
}
export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
const [type, setType] = useState<ObjectType>("functions");
const selectedObjectType = useDbViewerStore((s) => s.selectedObjectType);
const [type, setType] = useState<ObjectType>(selectedObjectType ?? "functions");
const [panelWidth, setPanelWidth] = useState(280);
const panelResizeRef = useRef<{ startX: number; startW: number } | null>(
null,
@@ -1061,6 +1080,9 @@ export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedItem, setSelectedItem] = useState<AnyObject | null>(null);
const [openKey, setOpenKey] = useState<string | null>(null);
const [depOpen, setDepOpen] = useState(false);
const [depDeps, setDepDeps] = useState<DependencyInfo[]>([]);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null);
@@ -1173,14 +1195,25 @@ export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
// Switching object type: reset selection/search, clear the stale list so
// the loading state renders (no flash of the previous type's objects), and
// reset the last-fetched-schema marker so the fetch effect re-runs.
const handleTypeChange = (next: ObjectType) => {
const handleTypeChange = useCallback((next: ObjectType) => {
setType(next);
setSearchQuery("");
setSelectedItem(null);
setOpenKey(null);
setItems(null);
setLoading(true);
lastSchemaRef.current = undefined;
};
}, []);
// Consume any object-type preselection from the Cmd+K palette.
useEffect(() => {
if (selectedObjectType && selectedObjectType !== type) {
handleTypeChange(selectedObjectType);
}
if (selectedObjectType) {
useDbViewerStore.getState().setSelectedObjectType(null);
}
}, [selectedObjectType, type, handleTypeChange]);
return (
<div className="flex flex-1 min-h-0 overflow-hidden">
@@ -1326,6 +1359,34 @@ export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
const isSelected =
selectedItem !== null &&
itemKey(selectedItem) === itemKey(item);
const menuOpen = openKey === key;
const handleCopyDdl = async () => {
setOpenKey(null);
const ddl = await cmd.getObjectDdl(
connectionId,
schemaOf(item),
typeToDdlType(type),
objectName(item),
);
try {
await navigator.clipboard?.writeText(ddl);
} catch {
// Ignore clipboard errors.
}
};
const handleViewDependencies = async () => {
setOpenKey(null);
const deps = await cmd.getObjectDependencies(
connectionId,
schemaOf(item),
typeToDdlType(type),
objectName(item),
);
setDepDeps(deps);
setDepOpen(true);
};
return (
<div
@@ -1341,6 +1402,44 @@ export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
<span className="flex-1 text-sm truncate">
{name}
</span>
<div
onClick={(e) => e.stopPropagation()}
className="relative"
>
<button
aria-label="options"
onClick={() =>
setOpenKey((k) =>
k === key ? null : key,
)
}
className={`w-6 h-6 rounded flex items-center justify-center text-text-muted hover:text-text hover:bg-surface-raised cursor-pointer transition-opacity ${
menuOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
>
<MoreVertical size={14} />
</button>
{menuOpen && (
<div className="absolute right-0 mt-1 z-20 min-w-[140px] rounded-md bg-surface border border-border shadow-lg py-1">
<button
type="button"
onClick={handleCopyDdl}
className="w-full text-left px-3 py-1.5 text-xs text-text hover:bg-surface-raised cursor-pointer"
>
Copy DDL
</button>
<button
type="button"
onClick={handleViewDependencies}
className="w-full text-left px-3 py-1.5 text-xs text-text hover:bg-surface-raised cursor-pointer"
>
Dependencies
</button>
</div>
)}
</div>
<ChevronRight
size={14}
className={`text-text-muted shrink-0 transition-transform ${
@@ -1399,6 +1498,13 @@ export function ObjectExplorerPage({ connectionId }: ObjectExplorerPageProps) {
</div>
)}
</div>
<DependencyDialog
open={depOpen}
deps={depDeps}
onProceed={() => setDepOpen(false)}
onCancel={() => setDepOpen(false)}
/>
</div>
);
}