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)
This commit is contained in:
2026-08-04 21:13:20 +08:00
committed by GitHub
parent 002d8345ae
commit 1195d2c3f9
67 changed files with 3150 additions and 649 deletions
@@ -3,28 +3,37 @@ import { useConnectionStore } from "../../stores/connectionStore";
import { useNotificationStore } from "../../stores/notificationStore";
import { useSettingsStore } from "../../stores/settingsStore";
import { ConnectionFormShell } from "./ConnectionFormShell";
import { SimpleConnectionForm } from "./SimpleConnectionForm";
import { DetailedConnectionForm } from "./DetailedConnectionForm";
import { parseConnectionString } from "../../lib/connectionString";
import { ProviderTabsGrid } from "./ProviderTabsGrid";
import { ProviderSetupGuide } from "./ProviderSetupGuide";
import {
parseConnectionString,
detectProviderFromHost,
} from "../../lib/connectionString";
import { getProviderById, type ProviderId } from "../../lib/providers";
import { validateConnectionInput } from "../../lib/utils";
import { testConnection } from "../../lib/commands";
import type {
Folder,
Tag,
NewConnectionMode,
ConnectionInput,
} from "../../lib/types";
import { INPUT_ROUNDING } from "../../lib/uiConstants";
import type { ConnectionInput, Folder, Tag } from "../../lib/types";
import type { ConnectionFormData } from "./connectionFormData";
interface NewConnectionScreenProps {
defaultFolderId?: string | null;
prefilledConnectionString?: string;
folders: Folder[];
tags: Tag[];
folders?: Folder[];
tags?: Tag[];
onSaved?: () => void;
onCancel?: () => void;
}
type Stage = "entry" | "configured";
const FALLBACK_PORTS: Record<string, number> = {
postgresql: 5432,
mysql: 3306,
redis: 6379,
};
function createEmptyForm(
defaultFolderId: string | null = null,
defaultPorts?: Record<string, number | null>,
@@ -48,19 +57,24 @@ function createEmptyForm(
function getDefaultPort(dbType: string): number {
return (
useSettingsStore.getState().settings?.default_ports?.[dbType] ?? 5432
useSettingsStore.getState().settings?.default_ports?.[dbType] ??
FALLBACK_PORTS[dbType] ??
5432
);
}
export function NewConnectionScreen({
defaultFolderId = null,
prefilledConnectionString = "",
folders,
tags,
folders: _folders,
tags: _tags,
onSaved,
onCancel,
}: NewConnectionScreenProps) {
const [mode, setMode] = useState<NewConnectionMode>("simple");
const [stage, setStage] = useState<Stage>("entry");
const [managedPreset, setManagedPreset] = useState<
"supabase" | "neon" | null
>(null);
const [form, setForm] = useState<ConnectionFormData>(() =>
createEmptyForm(
defaultFolderId,
@@ -72,11 +86,23 @@ export function NewConnectionScreen({
const createConnection = useConnectionStore((s) => s.createConnection);
const notify = useNotificationStore((s) => s.notify);
const revealConfigured = useCallback(
(updates: Partial<ConnectionFormData>) => {
setForm((prev) => ({ ...prev, ...updates }));
setStage("configured");
},
[],
);
const handleConnectionStringChange = useCallback((value: string) => {
setForm((prev) => {
const parsed = parseConnectionString(value);
if (!parsed) return { ...prev, connection_string: value };
return {
const parsed = parseConnectionString(value);
if (parsed) {
const provider =
parsed.db_type === "postgresql"
? detectProviderFromHost(parsed.host)
: null;
setManagedPreset(provider);
setForm((prev) => ({
...prev,
connection_string: value,
db_type: parsed.db_type,
@@ -85,19 +111,82 @@ export function NewConnectionScreen({
username: parsed.username,
password: parsed.password,
database: parsed.database,
};
});
}));
} else {
setManagedPreset(null);
setForm((prev) => ({ ...prev, connection_string: value }));
}
}, []);
useEffect(() => {
if (
stage === "entry" &&
form.connection_string &&
parseConnectionString(form.connection_string)
) {
setStage("configured");
}
}, [form.connection_string, stage]);
useEffect(() => {
if (prefilledConnectionString) {
handleConnectionStringChange(prefilledConnectionString);
}
}, [prefilledConnectionString, handleConnectionStringChange]);
const updateForm = useCallback((updates: Partial<ConnectionFormData>) => {
setForm((prev) => ({ ...prev, ...updates }));
}, []);
const handleSelectProvider = useCallback(
(id: ProviderId) => {
const provider = getProviderById(id)!;
setManagedPreset(
provider.isManagedPreset ? (id as "supabase" | "neon") : null,
);
revealConfigured({
db_type: provider.dbType,
port:
provider.dbType === "sqlite"
? null
: getDefaultPort(provider.dbType),
...(provider.dbType === "sqlite" ? { host: "" } : {}),
});
},
[revealConfigured],
);
const updateForm = useCallback(
(updates: Partial<ConnectionFormData>) => {
setForm((prev) => {
if (
"connection_string" in updates &&
updates.connection_string !== undefined
) {
const value = updates.connection_string;
const parsed = parseConnectionString(value);
if (parsed) {
const provider =
parsed.db_type === "postgresql"
? detectProviderFromHost(parsed.host)
: null;
setManagedPreset(provider);
return {
...prev,
...updates,
db_type: parsed.db_type,
host: parsed.host,
port:
parsed.port ??
getDefaultPort(parsed.db_type),
username: parsed.username,
password: parsed.password,
database: parsed.database,
};
}
return { ...prev, ...updates };
}
return { ...prev, ...updates };
});
},
[],
);
const buildPayload = useCallback((): ConnectionInput => {
return {
@@ -169,44 +258,82 @@ export function NewConnectionScreen({
}
}, [validate, notify, testConnection, buildPayload]);
const onSimpleChange = useCallback(
(updates: Partial<ConnectionFormData>) => {
if (
"connection_string" in updates &&
updates.connection_string !== undefined
) {
handleConnectionStringChange(updates.connection_string);
} else {
updateForm(updates);
}
},
[handleConnectionStringChange, updateForm],
);
const onToggleMode = useCallback(() => {
setMode((m) => (m === "simple" ? "detailed" : "simple"));
}, []);
const isEntry = stage === "entry";
const showEntryUri = isEntry || form.db_type !== "sqlite";
return (
<ConnectionFormShell
mode={mode}
onBack={() => onCancel?.()}
onTest={handleTest}
onSave={handleSave}
onToggleMode={onToggleMode}
testLoading={testLoading}
saveLoading={saveLoading}
>
{mode === "simple" ? (
<SimpleConnectionForm
form={form}
folders={folders}
tags={tags}
onChange={onSimpleChange}
/>
<div>
{isEntry && showEntryUri && (
<label className="block text-sm text-text mb-1.5">
Connection URI
</label>
)}
{isEntry && form.db_type === "sqlite" ? (
<div>
<label className="block text-sm text-text mb-1.5">
File Path
</label>
<input
value={form.host}
onChange={(e) =>
setForm((prev) => ({
...prev,
host: e.target.value,
}))
}
placeholder="/path/to/database.sqlite"
aria-label="File Path"
className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-3 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors`}
/>
</div>
) : (
<input
value={form.connection_string}
onChange={(e) =>
handleConnectionStringChange(e.target.value)
}
placeholder="postgresql://user:password@host:5432/database"
aria-label={isEntry ? "Connection URI" : undefined}
className={`w-full ${INPUT_ROUNDING} bg-surface border border-border px-4 py-3 text-sm text-text font-mono placeholder-text-muted/60 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/50 transition-colors ${
isEntry ? "" : "sr-only"
}`}
/>
)}
{isEntry && showEntryUri && (
<p className="text-xs text-text-muted mt-1.5">
Paste a connection string to auto-detect, or pick a
provider below.
</p>
)}
</div>
{isEntry ? (
<>
<div className="flex items-center gap-3 my-1">
<div className="flex-1 h-px bg-border" />
<span className="text-xs text-text-muted">OR</span>
<div className="flex-1 h-px bg-border" />
</div>
<ProviderTabsGrid onSelect={handleSelectProvider} />
</>
) : (
<DetailedConnectionForm form={form} onChange={updateForm} />
<>
{managedPreset && (
<ProviderSetupGuide provider={managedPreset} />
)}
<DetailedConnectionForm
form={form}
onChange={updateForm}
/>
</>
)}
</ConnectionFormShell>
);
}
}