Files
gridline/src/components/db-viewer/EditConnectionModal.tsx
T
adrianbonpin 1195d2c3f9 v0.7.0: New Connection screen revamp + full MySQL DB viewer (#10)
* docs: correct competitor comparison for DB Pro, Beekeeper, TablePlus

Research-verified the 'Why Gridline vs the alternatives' claims against
vendor docs, pricing pages, GitHub, and release notes (May 2026):

- DB Pro is an Electron app (founder-confirmed), not native; add
  TablePlus column to the comparison table
- Fix wrong cells: DB Pro has query/dashboard folders + table tags and
  CSV/JSON export on the free tier; object-explorer depth corrected for
  DB Pro (tables/views/indexes/enums) and Beekeeper
  (tables/views/routines/triggers)
- Reframe differentiators: unlimited-everything framing dropped for
  Beekeeper (free tier is already unlimited on tabs/connections/queries);
  keep DB-to-DB sync as the genuinely unique feature
- Add a dated 'Competitor reality check' section to AGENTS.md so future
  edits don't re-assert inaccurate claims

* docs: add project roadmap, link it from README and AGENTS

New ROADMAP.md is the source of truth for planned work, reflecting the
in-flight v0.7.0 connection-screen-revamp spec (new-connection flow,
full MySQL DB viewer, capability gating, Supabase/Neon presets,
SQLite path mode, tag overflow scroll, styling sweep).

Next-up scope: PostgreSQL object management CRUD with companion
features (schema CRUD, global object search, copy-as-DDL, object
dependencies) and an admin follow-up (users/roles/grants,
VACUUM/ANALYZE/REINDEX). MySQL Objects view explicitly deferred.

Queue: Redis browsing, MariaDB/TimescaleDB, PlanetScale/Turso,
query workbench upgrades (multiple result sets, query cancel,
result streaming, visual query builder), schema/data tooling,
SQLite .dump, schema diff, more export formats.

Planned: BYOK AI, website & docs, rolling UI/UX polish (incl.
onboarding tour, settings import/export, SSH key management).

README roadmap section now links to ROADMAP.md; AGENTS.md Related
Documents + Implementation Status reference it and the v0.7.0 spec.

* docs: release notes reference prod as the production branch

The repo's production branch is prod (feature branches merge back to
prod), not main. Update the release-cut instructions in the README and
the trigger comment in release.yml.

* docs: add robust bug report issue template

Structured .github/ISSUE_TEMPLATE/bug_report.md covering environment
(OS, Gridline version, install type, DB type/version, hosted provider,
connection method incl. SSH/TLS/socket), steps to reproduce, expected vs
actual, screenshots, logs, impact, and workarounds — plus a duplicate
checklist and secrets-redaction note. Referenced from the README
Contributing section.

* docs: drop in-flight branch mention from roadmap; remove unused starter assets

- ROADMAP.md no longer references the in-flight feature branch/spec
  (removed at the end anyway when the branch PRs into prod)
- Remove unused Vite/Tauri starter SVGs from public/ (no favicon or
  asset references anywhere in the app)

* test: fix stale README comparison-table regex in docs-coverage (5-col table)

* feat: shared INPUT_ROUNDING constant + bump to v0.7.0 (Task 1.1)

* fix: map SQLite file path to host field + provider host detection (Task 1.2)

* test: bump version expectation to 0.7.0 (Task 1.1 follow-up)

* feat: db capability matrix for DB viewer gating (Task 1.3)

* feat: provider tab definitions, Supabase/Neon icons + setup guides (Task 1.4)

* feat(rust): MySQL SQL builders + identifier quoting (Task 2.1)

* chore(rust): sync Cargo.lock to gridline 0.7.0

* feat(rust): MySQL db_connect (SSL + SSH tunnel) + pool variant (Tasks 2.2-2.3)

* feat(rust): MySQL execute_query with wrapped pagination + raw fallback (Task 2.4)

* feat(rust): MySQL introspection + changes-queue editing + DDL (Task 2.5)

* fix(ui): show table toolbar immediately while tab is still loading first data

* feat: gate DB viewer sidebar nav by db capabilities (Task 3.1)

* feat: guard DB viewer views by capability + Redis unsupported state (Task 3.2)

* feat(ui): 2-column provider tab grid (Task 4.1)

* feat(ui): collapsible Supabase/Neon setup guide (Task 4.2)

* feat(ui): SQLite file-path input with Browse (Task 4.3)

* feat(ui): connection metadata row (label + tags/env/folder) (Task 4.4)

* feat(ui): rework GeneralTab (URI + OR + manual) + reduce Detailed form tabs (Task 4.5)

* feat(ui): NewConnectionScreen two-stage flow; remove SimpleConnectionForm (Task 5.1)

* feat(ui): scroll connection-card tag row past 3 tags (Task 5.2)

* style: sweep form controls from rounded-full to rounded-lg (Task 5.3)

* feat(ui): EditConnectionModal parity + managed-preset SSL hint (Task 5.4)

* fix(rust): decode MySQL VARBINARY metadata columns (information_schema/SHOW) as strings

* test: full suite green for v0.7.0 connection revamp (Task 5.5)

* feat(ui): schema dropdown + tables tree loading state while schema tree fetches

* docs: update AGENTS/README/ROADMAP for v0.7.0 (connection revamp, MySQL viewer, gating)
2026-08-04 21:13:20 +08:00

150 lines
5.5 KiB
TypeScript

import { useState, useCallback } from "react";
import { AnimatedModal } from "../ui/AnimatedModal";
import { Button } from "../ui/Button";
import { DetailedConnectionForm } from "../connections/DetailedConnectionForm";
import { useConnectionStore } from "../../stores/connectionStore";
import { useNotificationStore } from "../../stores/notificationStore";
import { updateConnection, testConnection, saveConnectionPassword, saveConnectionSshPassword, saveConnectionSshPassphrase } from "../../lib/commands";
import { detectProviderFromHost } from "../../lib/connectionString";
import type { Connection, ConnectionInput } from "../../lib/types";
import type { ConnectionFormData } from "../connections/connectionFormData";
interface EditConnectionModalProps {
connection: Connection;
open: boolean;
onClose: () => void;
onSaved: (updated: Connection) => void;
}
export function EditConnectionModal({
connection,
open,
onClose,
onSaved,
}: EditConnectionModalProps) {
const managedPreset = detectProviderFromHost(connection.host);
const [form, setForm] = useState<ConnectionFormData>(() => ({
name: connection.name,
environment: (connection.environment as ConnectionFormData["environment"]) ?? null,
folder_id: connection.folder_id,
tag_ids: [...connection.tag_ids],
connection_string: "",
db_type: connection.db_type,
host: connection.host,
port: connection.port,
username: connection.username,
password: null,
database: connection.database ?? null,
use_keychain: false,
ssh_host: connection.ssh_host ?? null,
ssh_port: connection.ssh_port ?? null,
ssh_user: connection.ssh_user ?? null,
ssh_auth_method:
(connection.ssh_auth_method as "password" | "key" | null | undefined) ??
null,
ssh_private_key: connection.ssh_private_key_path ?? null,
ssh_password: null,
ssh_passphrase: null,
}));
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const loadAll = useConnectionStore((s) => s.loadAll);
const notify = useNotificationStore((s) => s.notify);
const handleSave = useCallback(async () => {
if (!form.name.trim()) return;
setSaving(true);
try {
const input: ConnectionInput = {
name: form.name,
db_type: form.db_type,
host: form.host,
port: form.port,
username: form.username,
password: form.password,
database: form.database,
folder_id: form.folder_id,
environment: form.environment,
tag_ids: form.tag_ids,
ssh_host: form.ssh_host ?? null,
ssh_port: form.ssh_port ?? null,
ssh_user: form.ssh_user ?? null,
ssh_auth_method: form.ssh_auth_method ?? null,
ssh_private_key_path: form.ssh_private_key ?? null,
ssh_password: form.ssh_password ?? null,
ssh_passphrase: form.ssh_passphrase ?? null,
};
const updated = await updateConnection(connection.id, input);
if (form.password) {
await saveConnectionPassword(connection.id, form.password).catch(() => {});
}
// Persist SSH secrets to the OS keychain (not SQLite)
if (form.ssh_host && (form.ssh_auth_method ?? "password") === "password" && form.ssh_password) {
await saveConnectionSshPassword(connection.id, form.ssh_password).catch(() => {});
}
if (form.ssh_host && form.ssh_passphrase) {
await saveConnectionSshPassphrase(connection.id, form.ssh_passphrase).catch(() => {});
}
notify("Connection updated", "success");
onSaved(updated);
onClose();
loadAll();
} catch (e) {
notify(`Failed to update: ${e instanceof Error ? e.message : e}`, "error");
} finally {
setSaving(false);
}
}, [form, connection.id, notify, onSaved, onClose, loadAll]);
const handleTest = useCallback(async () => {
setTesting(true);
try {
// Fetch password from keychain if not provided in form
let password = form.password;
if (!password) {
password = await useConnectionStore.getState().getConnectionPassword(connection.id).catch(() => null);
}
const result = await testConnection({
name: form.name,
db_type: form.db_type,
host: form.host,
port: form.port,
username: form.username,
password,
database: form.database,
folder_id: form.folder_id,
environment: form.environment,
tag_ids: form.tag_ids,
ssh_password: form.ssh_password ?? null,
});
if (result.ok) {
notify("Connection successful", "success");
} else {
notify(result.error ?? "Connection failed", "error");
}
} catch (e) {
notify(`Test failed: ${e instanceof Error ? e.message : e}`, "error");
} finally {
setTesting(false);
}
}, [form, notify, connection.id]);
return (
<AnimatedModal open={open} onClose={onClose}>
<div className="w-full min-w-md max-w-lg max-h-[80vh] overflow-y-auto">
<h3 className="font-heading text-text text-lg mb-4">Edit Connection</h3>
<DetailedConnectionForm form={form} onChange={(updates) => setForm((prev) => ({ ...prev, ...updates }))} managedPreset={managedPreset} />
<div className="flex justify-end gap-2 mt-4">
<Button variant="ghost" onClick={handleTest} disabled={testing}>
{testing ? "Testing..." : "Test"}
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save"}
</Button>
</div>
</div>
</AnimatedModal>
);
}