feat: copy connection URL from connection card menu (#22)
Add 'Copy connection URL' (with password, fetched from the OS keychain on demand) and 'Copy connection URL (no password)' to the connection card kebab menu. Builds postgresql://, mysql://, sqlite://, redis:// strings with percent-encoded credentials and the PG sslmode appended. Bump to v0.7.10.
This commit is contained in:
@@ -248,7 +248,7 @@ Planned work is prioritized in the [Project Roadmap](./ROADMAP.md) (source of tr
|
||||
| Move-to-folder bulk action | ✅ | Selection toolbar → Move to Folder dialog (folder picker, move confirmed via dialog) |
|
||||
| Favorites / Recent connections | ✅ | Star toggle in the connection card ⋮ menu (persisted `favorite` flag); Recent connections row (top 8 via `getRecentConnections`) |
|
||||
| Connection status indicator on cards | ✅ | Kebab menu → Test connection with inline idle/checking/online/offline result, on-demand via keychain + `testConnection`. **Reports real `server_version` + `latency_ms`** (PG/MySQL/SQLite queries + connect timing in the Rust backend); shows `Online · 16.4 · 42ms` or the error, re-check debounced 2s |
|
||||
| Connection card actions menu (⋮) | ✅ | Kebab dropdown: Favorite toggle, Test connection (inline status), Manage submenu (Edit… / Duplicate / Delete…) |
|
||||
| Connection card actions menu (⋮) | ✅ | Kebab dropdown: Favorite toggle, Test connection (inline status), **Copy connection URL** (with password) / **Copy connection URL (no password)** (builds a `postgresql://`/`mysql://`/`sqlite://`/`redis://` string from the saved connection, percent-encoded, sslmode appended for PG; password fetched from keychain on demand), Manage submenu (Edit… / Duplicate / Delete…) |
|
||||
|
||||
### Database Viewer
|
||||
| Feature | Status | Details |
|
||||
|
||||
@@ -30,21 +30,21 @@
|
||||
|
||||
## Download
|
||||
|
||||
Grab the installer for your OS from the [latest release](https://github.com/AdrianBonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.9**):
|
||||
Grab the installer for your OS from the [latest release](https://github.com/AdrianBonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.10**):
|
||||
|
||||
| OS | Architecture | Download |
|
||||
| :--------------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.9_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_aarch64.dmg) |
|
||||
| **macOS** | Intel | [Gridline_0.7.9_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_x64.dmg) |
|
||||
| **Windows** | x64 | [Gridline_0.7.9_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_x64-setup.exe) |
|
||||
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.9_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_amd64.deb) |
|
||||
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.9-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline-0.7.9-1.x86_64.rpm) |
|
||||
| **Other Linux** | amd64 | [Gridline_0.7.9_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_amd64.AppImage) |
|
||||
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.10_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_aarch64.dmg) |
|
||||
| **macOS** | Intel | [Gridline_0.7.10_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_x64.dmg) |
|
||||
| **Windows** | x64 | [Gridline_0.7.10_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_x64-setup.exe) |
|
||||
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.10_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_amd64.deb) |
|
||||
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.10-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline-0.7.10-1.x86_64.rpm) |
|
||||
| **Other Linux** | amd64 | [Gridline_0.7.10_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_amd64.AppImage) |
|
||||
|
||||
> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
|
||||
|
||||
<!--
|
||||
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.9
|
||||
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.10
|
||||
release assets, not at a moving "latest" target. On every new release,
|
||||
update BOTH tables here (Download + Which file should I download?) to the
|
||||
new version's asset names, which are tauri-action's default naming:
|
||||
@@ -89,6 +89,8 @@ Gridline is built for developers and small teams who manage multiple database en
|
||||
<details>
|
||||
<summary>View the full changelog</summary>
|
||||
|
||||
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
|
||||
|
||||
- **v0.7.9** — **macOS install/launch fixes**: the app's local store now opens under the OS app-data directory (a cwd-relative `gridline.db` made Finder-launched copies silently exit with a Rust panic before the UI started); macOS bundles are **ad-hoc signed at build time** (`signingIdentity "-"`), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusals) — users now get a standard one-time Gatekeeper prompt (see the [install notes](#installers-are-unsigned-for-now), incl. the `xattr` workaround); **OpenSSL is vendored** into the `ssh2` build so the release no longer depends on a Homebrew OpenSSL path; version bump.
|
||||
|
||||
- **v0.7.8** — **SQLite `.dump` backup/restore** plus **backup / restore / sync for MySQL & SQLite** (mysqldump-based, system-first); **Excel (.xlsx) export** in the grid toolbar alongside CSV/JSON/SQL/Markdown; **cancel long-running queries** per connection; **settings export/import** (JSON); **Windows/Linux title bar fix** (macOS-only overlay drag strip); **SQLite table editor** (Create/Edit Table with SQLite-aware type mapping); version bump.
|
||||
@@ -312,16 +314,16 @@ Code signing **will be added in the future** (Apple Developer Program + a Window
|
||||
|
||||
#### Which file should I download?
|
||||
|
||||
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.9**):
|
||||
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.10**):
|
||||
|
||||
| Your system | Download this | Notes |
|
||||
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
||||
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.9_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_aarch64.dmg) | `aarch64` = Apple's own chip |
|
||||
| macOS **Intel** | [Gridline_0.7.9_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_x64.dmg) | `x64` = Intel/AMD |
|
||||
| **Windows** (most PCs) | [Gridline_0.7.9_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
||||
| **Debian / Ubuntu** | [Gridline_0.7.9_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.9_amd64.deb` |
|
||||
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.9-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline-0.7.9-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.9-1.x86_64.rpm` |
|
||||
| **Any other Linux** | [Gridline_0.7.9_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.9/Gridline_0.7.9_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
||||
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.10_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_aarch64.dmg) | `aarch64` = Apple's own chip |
|
||||
| macOS **Intel** | [Gridline_0.7.10_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_x64.dmg) | `x64` = Intel/AMD |
|
||||
| **Windows** (most PCs) | [Gridline_0.7.10_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
||||
| **Debian / Ubuntu** | [Gridline_0.7.10_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.10_amd64.deb` |
|
||||
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.10-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline-0.7.10-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.10-1.x86_64.rpm` |
|
||||
| **Any other Linux** | [Gridline_0.7.10_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.10/Gridline_0.7.10_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
||||
|
||||
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
|
||||
|
||||
@@ -331,8 +333,8 @@ Cutting a release is one command — CI builds everything. **Releases are cut fr
|
||||
|
||||
```bash
|
||||
git checkout prod && git pull
|
||||
git tag v0.7.9
|
||||
git push origin v0.7.9
|
||||
git tag v0.7.10
|
||||
git push origin v0.7.10
|
||||
```
|
||||
|
||||
GitHub Actions (`.github/workflows/release.yml`) builds installers for **Apple Silicon, Intel Macs, Windows, and Linux**, then opens a **draft release** on the [Releases](https://github.com/adrianbonpin/gridline/releases) page — review it and hit **Publish release**.
|
||||
@@ -416,7 +418,7 @@ gridline/
|
||||
|
||||
## Roadmap
|
||||
|
||||
The full plan — next-up (v0.7.9), queue, and shipped history — lives in **[ROADMAP.md](./ROADMAP.md)**.
|
||||
The full plan — next-up (v0.7.10), queue, and shipped history — lives in **[ROADMAP.md](./ROADMAP.md)**.
|
||||
|
||||
Highlights of what's next:
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ This file is the **source of truth** for what Gridline is building. [AGENTS.md](
|
||||
|
||||
---
|
||||
|
||||
## ✅ Shipped (0.7.10)
|
||||
|
||||
- **Copy connection URL** — from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools: **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended.
|
||||
- **Version bump** 0.7.9 → **0.7.10**.
|
||||
|
||||
## ✅ Shipped (0.7.9)
|
||||
|
||||
- **macOS Finder launch fix** — the local store opens under the OS app-data directory instead of a cwd-relative `gridline.db` (Finder/LaunchServices launches run with cwd `/`, so the old path made the app silently exit with a Rust panic before the UI started).
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gridline",
|
||||
"private": true,
|
||||
"version": "0.7.9",
|
||||
"version": "0.7.10",
|
||||
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gridline"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Gridline",
|
||||
"version": "0.7.9",
|
||||
"version": "0.7.10",
|
||||
"identifier": "com.adrianbonpin.gridline",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
|
||||
@@ -200,4 +200,41 @@ describe("ConnectionCardMenu", () => {
|
||||
await userEvent.keyboard("{Escape}");
|
||||
expect(screen.queryByText("Manage")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Copy connection URL fetches the password and copies the full URL", async () => {
|
||||
vi.mocked(commands.getConnectionPassword).mockResolvedValue("s3cret");
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText },
|
||||
configurable: true,
|
||||
});
|
||||
renderMenu();
|
||||
await openMenu();
|
||||
|
||||
await userEvent.click(screen.getByText("Copy connection URL"));
|
||||
|
||||
expect(commands.getConnectionPassword).toHaveBeenCalledWith("c1");
|
||||
expect(writeText).toHaveBeenCalledWith(
|
||||
"postgresql://:s3cret@prod.example.com:5432",
|
||||
);
|
||||
expect(screen.queryByText("Manage")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Copy connection URL (no password) does not fetch the password", async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText },
|
||||
configurable: true,
|
||||
});
|
||||
renderMenu();
|
||||
await openMenu();
|
||||
|
||||
await userEvent.click(screen.getByText("Copy connection URL (no password)"));
|
||||
|
||||
expect(commands.getConnectionPassword).not.toHaveBeenCalled();
|
||||
expect(writeText).toHaveBeenCalledWith(
|
||||
"postgresql://prod.example.com:5432",
|
||||
);
|
||||
expect(screen.queryByText("Manage")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Copy,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
import type { Connection } from "../../lib/types";
|
||||
import { useConnectionStore } from "../../stores/connectionStore";
|
||||
import { buildConfigFromConnection } from "./ConnectionCard";
|
||||
import { buildConnectionUrl } from "../../lib/connectionString";
|
||||
import { useConnectionStatus } from "./useConnectionStatus";
|
||||
|
||||
interface ConnectionCardMenuProps {
|
||||
@@ -82,6 +84,19 @@ export function ConnectionCardMenu({
|
||||
setManageOpen(false);
|
||||
};
|
||||
|
||||
const copyUrl = async (withPassword: boolean) => {
|
||||
let password: string | null = null;
|
||||
if (withPassword) {
|
||||
password = await useConnectionStore
|
||||
.getState()
|
||||
.getConnectionPassword(connection.id)
|
||||
.catch(() => null);
|
||||
}
|
||||
const url = buildConnectionUrl(connection, password);
|
||||
await navigator.clipboard.writeText(url).catch(() => {});
|
||||
close();
|
||||
};
|
||||
|
||||
const statusLabel =
|
||||
state === "checking"
|
||||
? "Testing…"
|
||||
@@ -169,6 +184,28 @@ export function ConnectionCardMenu({
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void copyUrl(true);
|
||||
}}
|
||||
className={menuItemClass}
|
||||
>
|
||||
<Copy size={14} className="text-text-muted" />
|
||||
<span className="truncate">Copy connection URL</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void copyUrl(false);
|
||||
}}
|
||||
className={menuItemClass}
|
||||
>
|
||||
<EyeOff size={14} className="text-text-muted" />
|
||||
<span className="truncate">Copy connection URL (no password)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setManageOpen((m) => !m)}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||
|
||||
describe("tauri bundle config (v0.7.9)", () => {
|
||||
describe("tauri bundle config (v0.7.10)", () => {
|
||||
it("declares bundled pg_tools resources", () => {
|
||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||
});
|
||||
it("version is 0.7.9", () => {
|
||||
expect(tauriConf.version).toBe("0.7.9");
|
||||
it("version is 0.7.10", () => {
|
||||
expect(tauriConf.version).toBe("0.7.10");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,24 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseConnectionString, looksLikeConnectionString, detectProviderFromHost } from "./connectionString";
|
||||
import { parseConnectionString, looksLikeConnectionString, detectProviderFromHost, buildConnectionUrl } from "./connectionString";
|
||||
import type { Connection } from "./types";
|
||||
|
||||
function makeConn(overrides: Partial<Connection> = {}): Connection {
|
||||
return {
|
||||
id: "c1",
|
||||
name: "Test",
|
||||
db_type: "postgresql",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "user",
|
||||
folder_id: null,
|
||||
keychain_ref: null,
|
||||
tag_ids: [],
|
||||
favorite: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("parseConnectionString", () => {
|
||||
it("parses a PostgreSQL URL", () => {
|
||||
@@ -150,3 +169,75 @@ describe("detectProviderFromHost", () => {
|
||||
expect(detectProviderFromHost("prod.example.com")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConnectionUrl", () => {
|
||||
it("builds a PostgreSQL URL with password and sslmode", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ db_type: "postgresql", host: "prod.example.com", port: 5432, username: "alice", database: "app", ssl_mode: "require" }),
|
||||
"s3cret",
|
||||
);
|
||||
expect(url).toBe("postgresql://alice:s3cret@prod.example.com:5432/app?sslmode=require");
|
||||
});
|
||||
|
||||
it("omits password when null (no-password variant)", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ db_type: "postgresql", host: "prod.example.com", port: 5432, username: "alice", database: "app" }),
|
||||
null,
|
||||
);
|
||||
expect(url).toBe("postgresql://alice@prod.example.com:5432/app");
|
||||
});
|
||||
|
||||
it("omits sslmode when disable or unset", () => {
|
||||
expect(
|
||||
buildConnectionUrl(makeConn({ ssl_mode: "disable" }), null),
|
||||
).toBe("postgresql://user@localhost:5432");
|
||||
expect(
|
||||
buildConnectionUrl(makeConn({ ssl_mode: undefined }), null),
|
||||
).toBe("postgresql://user@localhost:5432");
|
||||
});
|
||||
|
||||
it("percent-encodes special characters in user/password/database", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ username: "a b", database: "my db" }),
|
||||
"p@ss w:rd",
|
||||
);
|
||||
expect(url).toBe("postgresql://a%20b:p%40ss%20w%3Ard@localhost:5432/my%20db");
|
||||
});
|
||||
|
||||
it("builds a MySQL URL", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ db_type: "mysql", host: "127.0.0.1", port: 3306, username: "root", database: "app" }),
|
||||
"pw",
|
||||
);
|
||||
expect(url).toBe("mysql://root:pw@127.0.0.1:3306/app");
|
||||
});
|
||||
|
||||
it("builds a Redis URL with password but no username", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ db_type: "redis", host: "localhost", port: 6379, username: null, database: "0" }),
|
||||
"pw",
|
||||
);
|
||||
expect(url).toBe("redis://:pw@localhost:6379/0");
|
||||
});
|
||||
|
||||
it("builds a SQLite URL from the host file path", () => {
|
||||
const url = buildConnectionUrl(
|
||||
makeConn({ db_type: "sqlite", host: "/Users/me/data.db", port: null, username: null, database: null }),
|
||||
null,
|
||||
);
|
||||
expect(url).toBe("sqlite:///Users/me/data.db");
|
||||
});
|
||||
|
||||
it("round-trips through parseConnectionString", () => {
|
||||
const conn = makeConn({ db_type: "postgresql", host: "localhost", port: 5432, username: "user", database: "mydb" });
|
||||
const url = buildConnectionUrl(conn, "pass");
|
||||
expect(parseConnectionString(url)).toEqual({
|
||||
db_type: "postgresql",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "user",
|
||||
password: "pass",
|
||||
database: "mydb",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DbType } from "./types";
|
||||
import type { Connection, DbType } from "./types";
|
||||
|
||||
export interface ParsedConnectionString {
|
||||
db_type: DbType;
|
||||
@@ -89,3 +89,49 @@ export function detectProviderFromHost(host: string): "supabase" | "neon" | null
|
||||
if (h.endsWith(".neon.tech")) return "neon";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a connection URL string from a saved connection, for copying into
|
||||
* env vars / other tools. `password` is the decrypted keychain value (or
|
||||
* `null` to omit it — e.g. the "no password" copy variant).
|
||||
*
|
||||
* Formats per DB type:
|
||||
* - postgresql: `postgresql://user:pass@host:port/db?sslmode=...`
|
||||
* - mysql: `mysql://user:pass@host:port/db`
|
||||
* - sqlite: `sqlite:///abs/path` (host stores the file path)
|
||||
* - redis: `redis://user:pass@host:port/db`
|
||||
*
|
||||
* Username/password/database are percent-encoded; the ssl_mode is appended as
|
||||
* a query param for PostgreSQL when set to anything other than `disable`.
|
||||
*/
|
||||
export function buildConnectionUrl(conn: Connection, password: string | null): string {
|
||||
const enc = (s: string) => encodeURIComponent(s);
|
||||
const port = conn.port ? `:${conn.port}` : "";
|
||||
const user = conn.username ? enc(conn.username) : "";
|
||||
const auth = password
|
||||
? user
|
||||
? `${user}:${enc(password)}@`
|
||||
: `:${enc(password)}@`
|
||||
: user
|
||||
? `${user}@`
|
||||
: "";
|
||||
const db = conn.database ? `/${enc(conn.database)}` : "";
|
||||
|
||||
switch (conn.db_type) {
|
||||
case "postgresql": {
|
||||
const ssl =
|
||||
conn.ssl_mode && conn.ssl_mode !== "disable"
|
||||
? `?sslmode=${encodeURIComponent(conn.ssl_mode)}`
|
||||
: "";
|
||||
return `postgresql://${auth}${conn.host}${port}${db}${ssl}`;
|
||||
}
|
||||
case "mysql":
|
||||
return `mysql://${auth}${conn.host}${port}${db}`;
|
||||
case "sqlite":
|
||||
// host stores the file path; `sqlite:///abs/path` round-trips through
|
||||
// parseConnectionString (pathname → host).
|
||||
return `sqlite://${conn.host}`;
|
||||
case "redis":
|
||||
return `redis://${auth}${conn.host}${port}${db}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
||||
import agents from "../../AGENTS.md?raw";
|
||||
import readme from "../../README.md?raw";
|
||||
|
||||
describe("v0.7.9 docs coverage", () => {
|
||||
describe("v0.7.10 docs coverage", () => {
|
||||
it("AGENTS.md marks inline cell editing complete", () => {
|
||||
expect(agents).toContain("Inline cell editing");
|
||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||
@@ -24,8 +24,8 @@ describe("v0.7.9 docs coverage", () => {
|
||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||
});
|
||||
it("README declares v0.7.9", () => {
|
||||
expect(readme).toContain("0.7.9");
|
||||
it("README declares v0.7.10", () => {
|
||||
expect(readme).toContain("0.7.10");
|
||||
});
|
||||
it("AGENTS.md marks schema CRUD complete", () => {
|
||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||
@@ -64,9 +64,9 @@ describe("v0.7.9 docs coverage", () => {
|
||||
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
||||
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
||||
});
|
||||
it("README links to v0.7.9 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.7.9/");
|
||||
expect(readme).toContain("Gridline_0.7.9_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.7.9-1.x86_64.rpm");
|
||||
it("README links to v0.7.10 assets in both download tables", () => {
|
||||
expect(readme).toContain("releases/download/v0.7.10/");
|
||||
expect(readme).toContain("Gridline_0.7.10_aarch64.dmg");
|
||||
expect(readme).toContain("Gridline-0.7.10-1.x86_64.rpm");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
describe("version", () => {
|
||||
it("declares v0.7.9 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.9");
|
||||
it("declares v0.7.10 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.10");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user