Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d1bbb0fa3 | ||
|
|
df3e3957a2 | ||
|
|
5bc31a14a6 | ||
|
|
38de9b4da1 | ||
|
|
091d9df6f0 |
@@ -3,12 +3,15 @@ name: Bug report
|
|||||||
about: Report a bug or unexpected behavior in Gridline so we can fix it
|
about: Report a bug or unexpected behavior in Gridline so we can fix it
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
assignees: ''
|
assignees: ['AdrianBonpin']
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Thank you for reporting a bug! A complete report is the fastest way to get it fixed.
|
Thank you for reporting a bug! A complete report is the fastest way to get it fixed.
|
||||||
Please fill in every section you can — the environment table alone often closes issues.
|
Please fill in every section you can — the environment table alone often closes issues.
|
||||||
|
|
||||||
|
Is this a request for something new instead? Use the
|
||||||
|
Feature request or Improvement templates — those get triaged faster.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Before you submit
|
## Before you submit
|
||||||
@@ -58,7 +61,9 @@ assignees: ''
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Paste the error text, console output, or a crash report here.
|
Paste the error text, console output, or a crash report here.
|
||||||
On macOS: Console.app → Gridline. On Windows: Event Viewer → Application.
|
- macOS: Console.app → Gridline
|
||||||
|
- Windows: Event Viewer → Application
|
||||||
|
- Linux: run Gridline from a terminal and capture stdout/stderr (or check the app-data dir, e.g. ~/.local/share/…)
|
||||||
Remove anything that looks like a host/user/password/token before pasting.
|
Remove anything that looks like a host/user/password/token before pasting.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -76,6 +81,10 @@ assignees: ''
|
|||||||
|
|
||||||
<!-- Anything that gets you unstuck today (e.g. "works when I connect without SSH", "restarting the app fixes it"). Even a partial workaround helps others. -->
|
<!-- Anything that gets you unstuck today (e.g. "works when I connect without SSH", "restarting the app fixes it"). Even a partial workaround helps others. -->
|
||||||
|
|
||||||
|
## Related issues / PRs
|
||||||
|
|
||||||
|
<!-- Links to related issues, PRs, or roadmap items. -->
|
||||||
|
|
||||||
## Additional context
|
## Additional context
|
||||||
|
|
||||||
<!-- Anything else: does it reproduce with a fresh connection or only one specific database? Did it work in an earlier version? Frequency (always / sometimes)? -->
|
<!-- Anything else: does it reproduce with a fresh connection or only one specific database? Did it work in an earlier version? Frequency (always / sometimes)? Does it happen on the demo SQLite database? -->
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a new feature or capability for Gridline
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: ['AdrianBonpin']
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting a feature! 🎉
|
||||||
|
Before you submit, check that it isn't already shipped or planned:
|
||||||
|
- README feature list: https://github.com/AdrianBonpin/gridline#features
|
||||||
|
- Roadmap: https://github.com/AdrianBonpin/gridline/blob/prod/ROADMAP.md
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Before you submit
|
||||||
|
|
||||||
|
- [ ] I searched [existing issues](https://github.com/AdrianBonpin/gridline/issues?q=is%3Aissue) and this isn't a duplicate
|
||||||
|
- [ ] I checked the [Roadmap](https://github.com/AdrianBonpin/gridline/blob/prod/ROADMAP.md) — it isn't already planned
|
||||||
|
- [ ] I checked the [README feature list](https://github.com/AdrianBonpin/gridline#features) — it isn't already shipped
|
||||||
|
- [ ] I will **not** paste secrets — redact any credentials as `<REDACTED>`
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
<!-- What can't you do today? Describe the workflow that's blocked or painful. The problem matters more than the solution you have in mind. -->
|
||||||
|
|
||||||
|
## Proposed solution
|
||||||
|
|
||||||
|
<!-- How should it work? High-level description. Consider where it lives in the UI (home screen, DB viewer, object explorer, query editor, tools). Mockups welcome — see below. -->
|
||||||
|
|
||||||
|
## Example scenario
|
||||||
|
|
||||||
|
<!-- A concrete walkthrough: "I want to compare row counts across three production schemas, so I open … and …" -->
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
<!-- What you do today instead: other tools, manual SQL, workarounds. This helps us judge how much it's really needed. -->
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
<!-- Rough size so we can prioritize. -->
|
||||||
|
|
||||||
|
- [ ] Small — UI polish / single command
|
||||||
|
- [ ] Medium — a new view or feature area
|
||||||
|
- [ ] Large — new DB type, new driver, multi-week effort
|
||||||
|
|
||||||
|
## Mockups / screenshots
|
||||||
|
|
||||||
|
<!-- Drag & drop sketches or screenshots; ASCII art is fine. If referencing how another tool does it, name the tool (TablePlus, Beekeeper Studio, DB Pro, …). -->
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
<!-- Roadmap items, related issues/PRs, competitor references, docs links. -->
|
||||||
|
|
||||||
|
## Priority
|
||||||
|
|
||||||
|
<!-- Optional: how important is this to you? -->
|
||||||
|
|
||||||
|
- [ ] Nice to have
|
||||||
|
- [ ] Important — would save me time every day
|
||||||
|
- [ ] Critical — blocks my workflow without it
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Improvement
|
||||||
|
about: Suggest a refinement or UX polish for an existing Gridline feature
|
||||||
|
title: "[Improvement]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
assignees: ['AdrianBonpin']
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For refinements to something that already exists — not brand-new features
|
||||||
|
(use the Feature request template for those) and not bugs (use Bug report).
|
||||||
|
Examples: a confusing label, a clunky interaction, a missing keyboard shortcut,
|
||||||
|
faster scroll, better default, accessibility fix.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Before you submit
|
||||||
|
|
||||||
|
- [ ] I searched [existing issues](https://github.com/AdrianBonpin/gridline/issues?q=is%3Aissue) and this isn't a duplicate
|
||||||
|
- [ ] This targets an **existing** feature (not a new one)
|
||||||
|
- [ ] I will **not** paste secrets — redact any credentials as `<REDACTED>`
|
||||||
|
|
||||||
|
## What's lacking today?
|
||||||
|
|
||||||
|
<!-- The pain point in the current behavior. Point at the exact screen/flow. -->
|
||||||
|
|
||||||
|
## Suggested change
|
||||||
|
|
||||||
|
<!-- The refinement. What should it look like / behave like? Specific beats vague. -->
|
||||||
|
|
||||||
|
## Why it matters
|
||||||
|
|
||||||
|
<!-- Who benefits and how (time saved, fewer misclicks, consistency, accessibility, …). -->
|
||||||
|
|
||||||
|
## Screenshots / recordings
|
||||||
|
|
||||||
|
<!-- Current behavior, plus any mock of the desired behavior. -->
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
<!-- Links to related issues/PRs. -->
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Template chooser settings — see
|
||||||
|
# https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Gridline Roadmap
|
||||||
|
url: https://github.com/AdrianBonpin/gridline/blob/prod/ROADMAP.md
|
||||||
|
about: Check what's already planned before requesting a feature
|
||||||
|
- name: README — feature list
|
||||||
|
url: https://github.com/AdrianBonpin/gridline#features
|
||||||
|
about: Confirm a feature isn't already shipped
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Auto-assign new issues
|
||||||
|
|
||||||
|
# Assigns every newly opened issue to the repository owner so nothing
|
||||||
|
# lands in the inbox untriaged. The `issues` event only fires in the base
|
||||||
|
# repository (not forks) and this job never checks out code — it only
|
||||||
|
# talks to the GitHub API, so there is no fork/pwn-request exposure.
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
name: Assign issue to repo owner
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Assign to ${{ github.repository_owner }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
run: gh issue edit "$ISSUE_URL" --add-assignee "$OWNER"
|
||||||
@@ -7,12 +7,16 @@ name: Release
|
|||||||
# git checkout prod && git pull
|
# git checkout prod && git pull
|
||||||
# git tag v0.5.0 && git push origin v0.5.0
|
# git tag v0.5.0 && git push origin v0.5.0
|
||||||
#
|
#
|
||||||
# SIGNING STATUS: builds are UNSIGNED for now (no code-signing certs yet —
|
# SIGNING STATUS: macOS bundles are AD-HOC signed via tauri.conf.json
|
||||||
# see README "Download a release"). tauri-action automatically signs +
|
# (bundle.macOS.signingIdentity: "-", hardenedRuntime: false). That replaces
|
||||||
# notarizes when the signing secrets are present, so the moment we add
|
# the linker-only signature that made macOS report the app as "damaged" and
|
||||||
# APPLE_CERTIFICATE / APPLE_API_KEY / WINDOWS_CERTIFICATE (or Azure Trusted
|
# refuse Finder launches — users now get the standard "Open Anyway" prompt
|
||||||
# Signing) to repo secrets, future builds are signed — no changes to this
|
# (or run `sudo xattr -dr com.apple.quarantine` for the damaged-error case, see
|
||||||
# file required.
|
# README). Still NOT Developer-ID signed / notarized (no certs yet).
|
||||||
|
# tauri-action automatically signs + notarizes when the signing secrets are
|
||||||
|
# present, so the moment we add APPLE_CERTIFICATE / APPLE_API_KEY /
|
||||||
|
# WINDOWS_CERTIFICATE (or Azure Trusted Signing) to repo secrets, future
|
||||||
|
# builds are properly signed — no changes to this file required.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
@@ -166,8 +166,9 @@ Cut a release by tagging the **`prod`** branch once the PR is merged — `git ta
|
|||||||
**Before tagging**, keep everything in sync:
|
**Before tagging**, keep everything in sync:
|
||||||
- Version number across `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json`
|
- Version number across `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json`
|
||||||
- `src/lib/version.test.ts` and `src/lib/docs-coverage.test.ts` if they assert the version
|
- `src/lib/version.test.ts` and `src/lib/docs-coverage.test.ts` if they assert the version
|
||||||
- **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.8/<file>`). tauri-action uses default versioned asset names (`Gridline_<ver>_aarch64.dmg`, `Gridline-<ver>-1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md).
|
- **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.9/<file>`). tauri-action uses default versioned asset names (`Gridline_<ver>_aarch64.dmg`, `Gridline-<ver>-1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md).
|
||||||
- **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step.
|
- **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step.
|
||||||
|
- **Release notes must include the macOS first-launch instructions** — until a Developer ID signing certificate is secured, releases are ad-hoc signed only (not notarized), so every macOS user hits a Gatekeeper prompt. The draft release body must include: right-click → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**) for "developer cannot be verified", and `sudo xattr -dr com.apple.quarantine /Applications/Gridline.app` (with the re-run-after-every-upgrade note) for "damaged and can't be opened".
|
||||||
|
|
||||||
### Adding a Tauri Command
|
### Adding a Tauri Command
|
||||||
|
|
||||||
@@ -247,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) |
|
| 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`) |
|
| 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 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
|
### Database Viewer
|
||||||
| Feature | Status | Details |
|
| Feature | Status | Details |
|
||||||
|
|||||||
@@ -30,21 +30,21 @@
|
|||||||
|
|
||||||
## Download
|
## 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.8**):
|
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 |
|
| OS | Architecture | Download |
|
||||||
| :--------------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
|
| :--------------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.8_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_aarch64.dmg) |
|
| **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.8_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_x64.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.8_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_x64-setup.exe) |
|
| **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.8_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_amd64.deb) |
|
| **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.8-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline-0.7.8-1.x86_64.rpm) |
|
| **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.8_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_amd64.AppImage) |
|
| **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) |
|
||||||
|
|
||||||
> Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below. All installers are **unsigned** — see the [notes](#which-file-should-i-download) on first-launch warnings.
|
> **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.8
|
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,
|
release assets, not at a moving "latest" target. On every new release,
|
||||||
update BOTH tables here (Download + Which file should I download?) to the
|
update BOTH tables here (Download + Which file should I download?) to the
|
||||||
new version's asset names, which are tauri-action's default naming:
|
new version's asset names, which are tauri-action's default naming:
|
||||||
@@ -89,6 +89,10 @@ Gridline is built for developers and small teams who manage multiple database en
|
|||||||
<details>
|
<details>
|
||||||
<summary>View the full changelog</summary>
|
<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.
|
- **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.
|
||||||
|
|
||||||
- **v0.7.7** — PostgreSQL **roles & grants** management (create/edit/drop roles with attributes + a per-role privilege explorer across tables/sequences/routines/schemas/databases — collapsible grouped lists with GRANT/REVOKE staging) and **table maintenance** (VACUUM / ANALYZE / REINDEX from the table menu). **Create Table / Edit Table** visual editor: columns grid with type dropdowns and drag-to-reorder, single + composite primary keys, column-diff staging (ADD / DROP / RENAME COLUMN, ALTER TYPE, SET | DROP DEFAULT, SET | DROP NOT NULL — one statement per queue item), atomic column-reorder table rebuild (single transaction, preserves constraints/indexes/FKs/grants/sequences, fail-closed for triggers/RLS/inheritance/partitioning). **FK management** — multi-column composer with cross-schema references and ON DELETE/UPDATE, inlined into CREATE TABLE; **table options** (tablespace, row-level security); changes queue auto-refreshes the object tree after schema-modifying commits; version bump.
|
- **v0.7.7** — PostgreSQL **roles & grants** management (create/edit/drop roles with attributes + a per-role privilege explorer across tables/sequences/routines/schemas/databases — collapsible grouped lists with GRANT/REVOKE staging) and **table maintenance** (VACUUM / ANALYZE / REINDEX from the table menu). **Create Table / Edit Table** visual editor: columns grid with type dropdowns and drag-to-reorder, single + composite primary keys, column-diff staging (ADD / DROP / RENAME COLUMN, ALTER TYPE, SET | DROP DEFAULT, SET | DROP NOT NULL — one statement per queue item), atomic column-reorder table rebuild (single transaction, preserves constraints/indexes/FKs/grants/sequences, fail-closed for triggers/RLS/inheritance/partitioning). **FK management** — multi-column composer with cross-schema references and ON DELETE/UPDATE, inlined into CREATE TABLE; **table options** (tablespace, row-level security); changes queue auto-refreshes the object tree after schema-modifying commits; version bump.
|
||||||
@@ -293,9 +297,16 @@ Pre-built installers for macOS, Windows, and Linux are published on the [Release
|
|||||||
|
|
||||||
#### Installers are unsigned (for now)
|
#### Installers are unsigned (for now)
|
||||||
|
|
||||||
Gridline is currently distributed **unsigned** — it doesn't pay for code-signing certificates yet. Your OS will warn you the first time you open it. This is expected — the app is safe, it just hasn't paid the signing fee:
|
Gridline is currently distributed **unsigned** — it doesn't pay for code-signing certificates yet (macOS builds are *ad-hoc signed*, so they pass Apple Silicon's launch checks but aren't Developer-ID signed or notarized). Your OS will warn you the first time you open it. This is expected — the app is safe, it just hasn't paid the signing fee:
|
||||||
|
|
||||||
- **macOS:** right-click the app → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**). Do this once per version.
|
- **macOS:** if you see *"Gridline can't be opened because the developer cannot be verified"* (or the app simply won't open from Finder), right-click the app → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**). Do this once per version.
|
||||||
|
- **macOS — "Gridline is damaged and can't be opened":** remove the quarantine flag macOS attaches to downloaded apps, then launch normally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo xattr -dr com.apple.quarantine /Applications/Gridline.app
|
||||||
|
```
|
||||||
|
|
||||||
|
(`sudo` is required — some files inside the bundle are read-only, so removing the flag needs admin rights.) Re-run after **every upgrade** — each freshly-downloaded copy gets re-quarantined.
|
||||||
- **Windows:** on the SmartScreen prompt, click **More info** → **Run anyway**.
|
- **Windows:** on the SmartScreen prompt, click **More info** → **Run anyway**.
|
||||||
- **Linux:** no warning — install and run normally.
|
- **Linux:** no warning — install and run normally.
|
||||||
|
|
||||||
@@ -303,16 +314,16 @@ Code signing **will be added in the future** (Apple Developer Program + a Window
|
|||||||
|
|
||||||
#### Which file should I download?
|
#### 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.8**):
|
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 |
|
| Your system | Download this | Notes |
|
||||||
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
||||||
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.8_aarch64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_aarch64.dmg) | `aarch64` = Apple's own chip |
|
| 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.8_x64.dmg](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_x64.dmg) | `x64` = Intel/AMD |
|
| 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.8_x64-setup.exe](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
| **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.8_amd64.deb](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.8_amd64.deb` |
|
| **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.8-1.x86_64.rpm](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline-0.7.8-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.8-1.x86_64.rpm` |
|
| **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.8_amd64.AppImage](https://github.com/AdrianBonpin/gridline/releases/download/v0.7.8/Gridline_0.7.8_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
| **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.
|
**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.
|
||||||
|
|
||||||
@@ -322,8 +333,8 @@ Cutting a release is one command — CI builds everything. **Releases are cut fr
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout prod && git pull
|
git checkout prod && git pull
|
||||||
git tag v0.7.8
|
git tag v0.7.10
|
||||||
git push origin v0.7.8
|
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**.
|
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**.
|
||||||
@@ -407,7 +418,7 @@ gridline/
|
|||||||
|
|
||||||
## Roadmap
|
## 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:
|
Highlights of what's next:
|
||||||
|
|
||||||
@@ -443,7 +454,7 @@ Highlights of what's next:
|
|||||||
|
|
||||||
Contributions, bug reports, and feature ideas are welcome. Gridline is Apache 2.0-licensed and intentionally stays open — no paywalled tiers, no bundled proprietary services.
|
Contributions, bug reports, and feature ideas are welcome. Gridline is Apache 2.0-licensed and intentionally stays open — no paywalled tiers, no bundled proprietary services.
|
||||||
|
|
||||||
- Report a bug via the **[issue template](.github/ISSUE_TEMPLATE/bug_report.md)** — it walks you through environment details (OS, Gridline version, DB type/version, connection method) so we can reproduce issues quickly. Opening a [new issue](https://github.com/AdrianBonpin/gridline/issues/new) pre-fills the template automatically.
|
- Open an issue via the **[template chooser](https://github.com/AdrianBonpin/gridline/issues/new)** — pick **Bug report** (with an environment table so we can reproduce issues quickly), **Feature request**, or **Improvement** (UX polish for existing features). New issues are auto-assigned to the maintainer.
|
||||||
- Submit a pull request. Keep Tauri commands thin, type IPC boundaries explicitly, and follow the existing Rust/React conventions.
|
- Submit a pull request. Keep Tauri commands thin, type IPC boundaries explicitly, and follow the existing Rust/React conventions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+13
-1
@@ -6,7 +6,19 @@ This file is the **source of truth** for what Gridline is building. [AGENTS.md](
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Next up (0.7.9) — TBD
|
## ✅ 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).
|
||||||
|
- **macOS ad-hoc code signing** — bundles are signed at build time (`bundle.macOS.signingIdentity "-"`, hardened runtime off), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusal). Users still get a one-time Gatekeeper prompt; README documents the `sudo xattr -dr com.apple.quarantine` workaround.
|
||||||
|
- **Vendored OpenSSL for `ssh2`** — the release binary no longer links an absolute Homebrew `/opt/homebrew/.../libssl.3.dylib` path (dyld aborted on machines without it).
|
||||||
|
- **Version bump** 0.7.8 → **0.7.9**.
|
||||||
|
|
||||||
|
## 🎯 Next up (0.8.0) — TBD
|
||||||
|
|
||||||
Scope to be determined.
|
Scope to be determined.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gridline",
|
"name": "gridline",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.8",
|
"version": "0.7.10",
|
||||||
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+11
-1
@@ -1783,7 +1783,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.8"
|
version = "0.7.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
@@ -3047,6 +3047,15 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.6.1+3.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.117"
|
version = "0.9.117"
|
||||||
@@ -3055,6 +3064,7 @@ checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.8"
|
version = "0.7.10"
|
||||||
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -34,7 +34,10 @@ tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1"
|
|||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
||||||
redis = { version = "0.27", features = ["tokio-comp"] }
|
redis = { version = "0.27", features = ["tokio-comp"] }
|
||||||
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
||||||
ssh2 = { version = "0.9" }
|
# vendored-openssl: link OpenSSL statically so the release binary has NO
|
||||||
|
# runtime dependency on the build machine's Homebrew openssl (@/opt/homebrew
|
||||||
|
# absolute LC_LOAD_DYLIB — dyld aborts on user machines without it).
|
||||||
|
ssh2 = { version = "0.9", features = ["vendored-openssl"] }
|
||||||
deadpool-postgres = { version = "0.14" }
|
deadpool-postgres = { version = "0.14" }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
tauri-plugin-keyring-store = { version = "0.2.0", default-features = false }
|
tauri-plugin-keyring-store = { version = "0.2.0", default-features = false }
|
||||||
|
|||||||
+21
-9
@@ -38,21 +38,33 @@ pub fn run() {
|
|||||||
// another provider is already installed).
|
// another provider is already installed).
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
let store = Store::open("gridline.db").expect("failed to open db");
|
|
||||||
let store_ref = StdMutex::new(store);
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_keyring_store::init())
|
.plugin(tauri_plugin_keyring_store::init())
|
||||||
.manage(AppState {
|
|
||||||
db_store: store_ref,
|
|
||||||
pool_manager: tokio::sync::Mutex::new(ConnectionPoolManager::new()),
|
|
||||||
ssh_manager: StdMutex::new(SshTunnelManager::new(Arc::new(Ssh2Backend))),
|
|
||||||
cancel_registry: crate::cancel::CancelRegistry::default(),
|
|
||||||
})
|
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
// Open the local store under the OS app-data directory. When the
|
||||||
|
// app is launched from Finder/LaunchServices the working directory
|
||||||
|
// is `/`, so a relative "gridline.db" path panics ("failed to
|
||||||
|
// open db", exit 101) before the UI ever starts. The demo DB uses
|
||||||
|
// the same directory (see commands/demo.rs).
|
||||||
|
let data_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| format!("failed to resolve app data dir: {e}"))?;
|
||||||
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.map_err(|e| format!("failed to create app data dir: {e}"))?;
|
||||||
|
let store =
|
||||||
|
Store::open(&data_dir.join("gridline.db").to_string_lossy()).expect("failed to open db");
|
||||||
|
|
||||||
|
app.manage(AppState {
|
||||||
|
db_store: StdMutex::new(store),
|
||||||
|
pool_manager: tokio::sync::Mutex::new(ConnectionPoolManager::new()),
|
||||||
|
ssh_manager: StdMutex::new(SshTunnelManager::new(Arc::new(Ssh2Backend))),
|
||||||
|
cancel_registry: crate::cancel::CancelRegistry::default(),
|
||||||
|
});
|
||||||
|
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
demo::ensure_demo_db(app.handle(), &state.db_store)
|
demo::ensure_demo_db(app.handle(), &state.db_store)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Gridline",
|
"productName": "Gridline",
|
||||||
"version": "0.7.8",
|
"version": "0.7.10",
|
||||||
"identifier": "com.adrianbonpin.gridline",
|
"identifier": "com.adrianbonpin.gridline",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"resources": ["resources/pg_tools/*", "resources/mysql_tools/*"],
|
"resources": ["resources/pg_tools/*", "resources/mysql_tools/*"],
|
||||||
|
"macOS": {
|
||||||
|
"signingIdentity": "-",
|
||||||
|
"hardenedRuntime": false
|
||||||
|
},
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
@@ -200,4 +200,41 @@ describe("ConnectionCardMenu", () => {
|
|||||||
await userEvent.keyboard("{Escape}");
|
await userEvent.keyboard("{Escape}");
|
||||||
expect(screen.queryByText("Manage")).not.toBeInTheDocument();
|
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,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
Copy,
|
Copy,
|
||||||
|
EyeOff,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
import type { Connection } from "../../lib/types";
|
import type { Connection } from "../../lib/types";
|
||||||
import { useConnectionStore } from "../../stores/connectionStore";
|
import { useConnectionStore } from "../../stores/connectionStore";
|
||||||
import { buildConfigFromConnection } from "./ConnectionCard";
|
import { buildConfigFromConnection } from "./ConnectionCard";
|
||||||
|
import { buildConnectionUrl } from "../../lib/connectionString";
|
||||||
import { useConnectionStatus } from "./useConnectionStatus";
|
import { useConnectionStatus } from "./useConnectionStatus";
|
||||||
|
|
||||||
interface ConnectionCardMenuProps {
|
interface ConnectionCardMenuProps {
|
||||||
@@ -82,6 +84,19 @@ export function ConnectionCardMenu({
|
|||||||
setManageOpen(false);
|
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 =
|
const statusLabel =
|
||||||
state === "checking"
|
state === "checking"
|
||||||
? "Testing…"
|
? "Testing…"
|
||||||
@@ -169,6 +184,28 @@ export function ConnectionCardMenu({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setManageOpen((m) => !m)}
|
onClick={() => setManageOpen((m) => !m)}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||||
|
|
||||||
describe("tauri bundle config (v0.7.8)", () => {
|
describe("tauri bundle config (v0.7.10)", () => {
|
||||||
it("declares bundled pg_tools resources", () => {
|
it("declares bundled pg_tools resources", () => {
|
||||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||||
});
|
});
|
||||||
it("version is 0.7.8", () => {
|
it("version is 0.7.10", () => {
|
||||||
expect(tauriConf.version).toBe("0.7.8");
|
expect(tauriConf.version).toBe("0.7.10");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
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", () => {
|
describe("parseConnectionString", () => {
|
||||||
it("parses a PostgreSQL URL", () => {
|
it("parses a PostgreSQL URL", () => {
|
||||||
@@ -150,3 +169,75 @@ describe("detectProviderFromHost", () => {
|
|||||||
expect(detectProviderFromHost("prod.example.com")).toBeNull();
|
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 {
|
export interface ParsedConnectionString {
|
||||||
db_type: DbType;
|
db_type: DbType;
|
||||||
@@ -89,3 +89,49 @@ export function detectProviderFromHost(host: string): "supabase" | "neon" | null
|
|||||||
if (h.endsWith(".neon.tech")) return "neon";
|
if (h.endsWith(".neon.tech")) return "neon";
|
||||||
return null;
|
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 agents from "../../AGENTS.md?raw";
|
||||||
import readme from "../../README.md?raw";
|
import readme from "../../README.md?raw";
|
||||||
|
|
||||||
describe("v0.7.8 docs coverage", () => {
|
describe("v0.7.10 docs coverage", () => {
|
||||||
it("AGENTS.md marks inline cell editing complete", () => {
|
it("AGENTS.md marks inline cell editing complete", () => {
|
||||||
expect(agents).toContain("Inline cell editing");
|
expect(agents).toContain("Inline cell editing");
|
||||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||||
@@ -24,8 +24,8 @@ describe("v0.7.8 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||||
});
|
});
|
||||||
it("README declares v0.7.8", () => {
|
it("README declares v0.7.10", () => {
|
||||||
expect(readme).toContain("0.7.8");
|
expect(readme).toContain("0.7.10");
|
||||||
});
|
});
|
||||||
it("AGENTS.md marks schema CRUD complete", () => {
|
it("AGENTS.md marks schema CRUD complete", () => {
|
||||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||||
@@ -64,9 +64,9 @@ describe("v0.7.8 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
||||||
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
||||||
});
|
});
|
||||||
it("README links to v0.7.8 assets in both download tables", () => {
|
it("README links to v0.7.10 assets in both download tables", () => {
|
||||||
expect(readme).toContain("releases/download/v0.7.8/");
|
expect(readme).toContain("releases/download/v0.7.10/");
|
||||||
expect(readme).toContain("Gridline_0.7.8_aarch64.dmg");
|
expect(readme).toContain("Gridline_0.7.10_aarch64.dmg");
|
||||||
expect(readme).toContain("Gridline-0.7.8-1.x86_64.rpm");
|
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";
|
import pkg from "../../package.json";
|
||||||
|
|
||||||
describe("version", () => {
|
describe("version", () => {
|
||||||
it("declares v0.7.8 across the app shell", () => {
|
it("declares v0.7.10 across the app shell", () => {
|
||||||
expect(pkg.version).toBe("0.7.8");
|
expect(pkg.version).toBe("0.7.10");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user