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:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseConnectionString, looksLikeConnectionString } from "./connectionString";
|
||||
import { parseConnectionString, looksLikeConnectionString, detectProviderFromHost } from "./connectionString";
|
||||
|
||||
describe("parseConnectionString", () => {
|
||||
it("parses a PostgreSQL URL", () => {
|
||||
@@ -38,15 +38,27 @@ describe("parseConnectionString", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a SQLite file URL", () => {
|
||||
it("parses a SQLite file URL — path maps to host (v0.7.0 fix)", () => {
|
||||
const result = parseConnectionString("sqlite:///path/to/db.sqlite");
|
||||
expect(result).toEqual({
|
||||
db_type: "sqlite",
|
||||
host: "localhost",
|
||||
host: "/path/to/db.sqlite",
|
||||
port: null,
|
||||
username: null,
|
||||
password: null,
|
||||
database: "/path/to/db.sqlite",
|
||||
database: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a SQLite file: URL — path maps to host", () => {
|
||||
const result = parseConnectionString("file:///Users/me/data.db");
|
||||
expect(result).toEqual({
|
||||
db_type: "sqlite",
|
||||
host: "/Users/me/data.db",
|
||||
port: null,
|
||||
username: null,
|
||||
password: null,
|
||||
database: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,3 +136,17 @@ describe("looksLikeConnectionString", () => {
|
||||
expect(looksLikeConnectionString("production database")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectProviderFromHost", () => {
|
||||
it("detects Supabase by host suffix", () => {
|
||||
expect(detectProviderFromHost("db.abcdefghijklmnopqrst.supabase.co")).toBe("supabase");
|
||||
expect(detectProviderFromHost("aws-us-east-1.pooler.supabase.com")).toBeNull();
|
||||
});
|
||||
it("detects NeonDB by host suffix", () => {
|
||||
expect(detectProviderFromHost("ep-cool-darkness-a1b2c3d4-pooler.us-east-2.aws.neon.tech")).toBe("neon");
|
||||
});
|
||||
it("returns null for plain Postgres hosts", () => {
|
||||
expect(detectProviderFromHost("localhost")).toBeNull();
|
||||
expect(detectProviderFromHost("prod.example.com")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,14 +40,24 @@ export function parseConnectionString(input: string): ParsedConnectionString | n
|
||||
const db_type = DB_PROTOCOLS[url.protocol];
|
||||
if (!db_type) return null;
|
||||
|
||||
const host = url.hostname || "localhost";
|
||||
const port = url.port ? Number(url.port) : (DEFAULT_PORTS[db_type] ?? null);
|
||||
const username = url.username || null;
|
||||
const password = url.password || null;
|
||||
const pathname = url.pathname;
|
||||
const database = db_type === "sqlite"
|
||||
? (pathname || null)
|
||||
: (pathname.replace(/^\//, "") || null);
|
||||
|
||||
let host: string;
|
||||
let database: string | null;
|
||||
if (db_type === "sqlite") {
|
||||
// SQLite: the file path lives in the URL pathname. Map it to `host`
|
||||
// (the field the backend opens) and leave `database` null. `url.pathname`
|
||||
// always starts with "/"; the fallback covers `sqlite://path` (no
|
||||
// pathname) where the URL's hostname holds the relative path.
|
||||
host = pathname || url.hostname || "";
|
||||
database = null;
|
||||
} else {
|
||||
host = url.hostname || "localhost";
|
||||
database = pathname.replace(/^\//, "") || null;
|
||||
}
|
||||
|
||||
return {
|
||||
db_type,
|
||||
@@ -69,3 +79,13 @@ export function looksLikeConnectionString(input: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Detect a managed-PostgreSQL provider from a connection host, purely for
|
||||
* UI highlighting. Returns `"supabase"` / `"neon"` / `null`. db_type is
|
||||
* unaffected (both presets persist as `postgresql`). */
|
||||
export function detectProviderFromHost(host: string): "supabase" | "neon" | null {
|
||||
const h = host.toLowerCase();
|
||||
if (h.endsWith(".supabase.co")) return "supabase";
|
||||
if (h.endsWith(".neon.tech")) return "neon";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { DB_CAPABILITIES, getCapabilities, type DbCapabilities } from "./dbCapabilities";
|
||||
|
||||
describe("dbCapabilities", () => {
|
||||
it("gives PostgreSQL every capability", () => {
|
||||
const c = DB_CAPABILITIES.postgresql;
|
||||
expect(c).toEqual<DbCapabilities>({
|
||||
explorer: true, queries: true, objects: true, visualizer: true,
|
||||
tools: true, editing: true, import: true, ddl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("gives MySQL explorer/queries/editing/import/ddl but not objects/visualizer/tools", () => {
|
||||
const c = DB_CAPABILITIES.mysql;
|
||||
expect(c.explorer).toBe(true);
|
||||
expect(c.queries).toBe(true);
|
||||
expect(c.editing).toBe(true);
|
||||
expect(c.import).toBe(true);
|
||||
expect(c.ddl).toBe(true);
|
||||
expect(c.objects).toBe(false);
|
||||
expect(c.visualizer).toBe(false);
|
||||
expect(c.tools).toBe(false);
|
||||
});
|
||||
|
||||
it("gives SQLite explorer/queries/visualizer/editing/import/ddl but not objects/tools", () => {
|
||||
const c = DB_CAPABILITIES.sqlite;
|
||||
expect(c.explorer).toBe(true);
|
||||
expect(c.queries).toBe(true);
|
||||
expect(c.visualizer).toBe(true);
|
||||
expect(c.editing).toBe(true);
|
||||
expect(c.import).toBe(true);
|
||||
expect(c.ddl).toBe(true);
|
||||
expect(c.objects).toBe(false);
|
||||
expect(c.tools).toBe(false);
|
||||
});
|
||||
|
||||
it("gives Redis nothing (connection+test only)", () => {
|
||||
expect(DB_CAPABILITIES.redis).toEqual<DbCapabilities>({
|
||||
explorer: false, queries: false, objects: false, visualizer: false,
|
||||
tools: false, editing: false, import: false, ddl: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("getCapabilities returns all-false for an unknown type", () => {
|
||||
const c = getCapabilities("postgres" as never);
|
||||
expect(Object.values(c).every((v) => v === false)).toBe(true);
|
||||
});
|
||||
|
||||
it("getCapabilities returns the matrix entry for known types", () => {
|
||||
expect(getCapabilities("postgresql")).toBe(DB_CAPABILITIES.postgresql);
|
||||
expect(getCapabilities("redis")).toBe(DB_CAPABILITIES.redis);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { DbType } from "./types";
|
||||
|
||||
export interface DbCapabilities {
|
||||
/** Table browser + data grid. */
|
||||
explorer: boolean;
|
||||
/** SQL editor + history/saved. */
|
||||
queries: boolean;
|
||||
/** Functions/triggers/sequences/enums/extensions. */
|
||||
objects: boolean;
|
||||
/** ER schema diagram. */
|
||||
visualizer: boolean;
|
||||
/** Backup / restore / sync. */
|
||||
tools: boolean;
|
||||
/** Inline cell edits + changes queue. */
|
||||
editing: boolean;
|
||||
/** CSV/JSON import. */
|
||||
import: boolean;
|
||||
/** Copy table schema (DDL). */
|
||||
ddl: boolean;
|
||||
}
|
||||
|
||||
const ALL_FALSE: DbCapabilities = {
|
||||
explorer: false, queries: false, objects: false, visualizer: false,
|
||||
tools: false, editing: false, import: false, ddl: false,
|
||||
};
|
||||
|
||||
export const DB_CAPABILITIES: Record<DbType, DbCapabilities> = {
|
||||
postgresql: { ...ALL_FALSE, explorer: true, queries: true, objects: true, visualizer: true, tools: true, editing: true, import: true, ddl: true },
|
||||
mysql: { ...ALL_FALSE, explorer: true, queries: true, editing: true, import: true, ddl: true },
|
||||
sqlite: { ...ALL_FALSE, explorer: true, queries: true, visualizer: true, editing: true, import: true, ddl: true },
|
||||
redis: { ...ALL_FALSE },
|
||||
};
|
||||
|
||||
/** Safe accessor — unknown types get the all-false capability set. */
|
||||
export function getCapabilities(dbType: string): DbCapabilities {
|
||||
return (DB_CAPABILITIES as Record<string, DbCapabilities>)[dbType] ?? ALL_FALSE;
|
||||
}
|
||||
+40
-2
@@ -1,4 +1,4 @@
|
||||
import { siPostgresql, siMysql, siSqlite, siRedis } from "simple-icons";
|
||||
import { siPostgresql, siMysql, siSqlite, siRedis, siSupabase, siNeon } from "simple-icons";
|
||||
import type { DbType } from "./types";
|
||||
|
||||
// Brand colors from simple-icons
|
||||
@@ -56,4 +56,42 @@ export const DB_ICONS: Record<DbType, string> = {
|
||||
mysql: "🐬",
|
||||
redis: "⚡",
|
||||
sqlite: "🗄️",
|
||||
};
|
||||
};
|
||||
|
||||
// ── Managed-PostgreSQL provider icons (Supabase, NeonDB) ──────────
|
||||
// These are NOT DbType values; connections persist as db_type="postgresql".
|
||||
type ProviderIconId = "supabase" | "neon";
|
||||
|
||||
const PROVIDER_ICON_DATA: Record<ProviderIconId, { hex: string; path: string }> = {
|
||||
supabase: { hex: `#${siSupabase.hex}`, path: siSupabase.path },
|
||||
neon: { hex: `#${siNeon.hex}`, path: siNeon.path },
|
||||
};
|
||||
|
||||
export const PROVIDER_LABELS: Record<ProviderIconId, string> = {
|
||||
supabase: "Supabase",
|
||||
neon: "NeonDB",
|
||||
};
|
||||
|
||||
interface ProviderIconProps {
|
||||
id: ProviderIconId;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ProviderIcon({ id, size = 20, className }: ProviderIconProps) {
|
||||
const data = PROVIDER_ICON_DATA[id];
|
||||
if (!data) return <span className="text-lg">❓</span>;
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
fill={data.hex}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={data.path} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
||||
import agents from "../../AGENTS.md?raw";
|
||||
import readme from "../../README.md?raw";
|
||||
|
||||
describe("v0.6.0 docs coverage", () => {
|
||||
describe("v0.7.0 docs coverage", () => {
|
||||
it("AGENTS.md marks inline cell editing complete", () => {
|
||||
expect(agents).toContain("Inline cell editing");
|
||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||
@@ -24,14 +24,15 @@ describe("v0.6.0 docs coverage", () => {
|
||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||
});
|
||||
it("README declares v0.6.0", () => {
|
||||
expect(readme).toContain("0.6.0");
|
||||
it("README declares v0.7.0", () => {
|
||||
expect(readme).toContain("0.7.0");
|
||||
});
|
||||
it("README marks inline editing complete (not Upcoming)", () => {
|
||||
// Key Features lists inline editing as a shipped feature
|
||||
expect(readme).toMatch(/- \*\*Inline cell editing\*\* —/);
|
||||
// comparison-table row for the stage→commit queue carries the ✅ marker
|
||||
expect(readme).toMatch(/Changes queue \(stage → commit\)\s*\|[^|]*❌[^|]*\|[^|]*❌[^|]*\|\s*\*\*✅ Queue → Commit All\*\*/);
|
||||
// comparison-table row for the stage→commit queue carries the Gridline ✅ marker
|
||||
// (5-column table: Feature | DB Pro | Beekeeper | TablePlus | Gridline)
|
||||
expect(readme).toMatch(/Changes queue \(stage → commit\)\s*\|[^|]*❌[^|]*\|[^|]*\|[^|]*\|\s*\*\*✅ Queue → Commit All\*\*/);
|
||||
expect(readme).not.toMatch(/Inline cell editing.*Upcoming/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { PROVIDER_TABS, getProviderById, SETUP_GUIDES } from "./providers";
|
||||
|
||||
describe("providers", () => {
|
||||
it("defines exactly 6 provider tabs in order", () => {
|
||||
expect(PROVIDER_TABS.map((p) => p.id)).toEqual([
|
||||
"postgresql", "mysql", "sqlite", "redis", "supabase", "neon",
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks only supabase and neon as managed presets mapping to postgresql", () => {
|
||||
const pg = getProviderById("postgresql")!;
|
||||
expect(pg.dbType).toBe("postgresql");
|
||||
expect(pg.isManagedPreset).toBe(false);
|
||||
const supa = getProviderById("supabase")!;
|
||||
expect(supa.dbType).toBe("postgresql");
|
||||
expect(supa.isManagedPreset).toBe(true);
|
||||
const neon = getProviderById("neon")!;
|
||||
expect(neon.dbType).toBe("postgresql");
|
||||
expect(neon.isManagedPreset).toBe(true);
|
||||
const redis = getProviderById("redis")!;
|
||||
expect(redis.dbType).toBe("redis");
|
||||
expect(redis.isManagedPreset).toBe(false);
|
||||
});
|
||||
|
||||
it("getProviderById returns undefined for unknown id", () => {
|
||||
expect(getProviderById("nope")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ships setup guides for supabase and neon only, each with steps", () => {
|
||||
expect(SETUP_GUIDES.supabase.steps.length).toBeGreaterThan(0);
|
||||
expect(SETUP_GUIDES.neon.steps.length).toBeGreaterThan(0);
|
||||
expect(SETUP_GUIDES.supabase.sslRequired).toBe(true);
|
||||
expect(SETUP_GUIDES.neon.sslRequired).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { DbType } from "./types";
|
||||
|
||||
export type ProviderId = "postgresql" | "mysql" | "sqlite" | "redis" | "supabase" | "neon";
|
||||
|
||||
export interface ProviderTab {
|
||||
id: ProviderId;
|
||||
label: string;
|
||||
/** The db_type persisted for a connection made via this tab. Supabase/Neon
|
||||
* are managed PostgreSQL, so they persist as `postgresql`. */
|
||||
dbType: DbType;
|
||||
/** True for managed-PostgreSQL presets (Supabase/NeonDB). */
|
||||
isManagedPreset: boolean;
|
||||
}
|
||||
|
||||
export const PROVIDER_TABS: ProviderTab[] = [
|
||||
{ id: "postgresql", label: "PostgreSQL", dbType: "postgresql", isManagedPreset: false },
|
||||
{ id: "mysql", label: "MySQL", dbType: "mysql", isManagedPreset: false },
|
||||
{ id: "sqlite", label: "SQLite", dbType: "sqlite", isManagedPreset: false },
|
||||
{ id: "redis", label: "Redis", dbType: "redis", isManagedPreset: false },
|
||||
{ id: "supabase", label: "Supabase", dbType: "postgresql", isManagedPreset: true },
|
||||
{ id: "neon", label: "NeonDB", dbType: "postgresql", isManagedPreset: true },
|
||||
];
|
||||
|
||||
export function getProviderById(id: string): ProviderTab | undefined {
|
||||
return PROVIDER_TABS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export interface SetupGuideStep {
|
||||
title: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface SetupGuide {
|
||||
/** Short tagline shown on the provider card. */
|
||||
blurb: string;
|
||||
sslRequired: boolean;
|
||||
steps: SetupGuideStep[];
|
||||
}
|
||||
|
||||
/** Static, researched setup instructions. No network calls at runtime.
|
||||
* Content reviewed against the official docs (fetched 2026-08-04, see spec §9). */
|
||||
export const SETUP_GUIDES: Record<"supabase" | "neon", SetupGuide> = {
|
||||
supabase: {
|
||||
blurb: "Managed PostgreSQL with a pooled connection string.",
|
||||
sslRequired: true,
|
||||
steps: [
|
||||
{
|
||||
title: "Open the Supabase Dashboard",
|
||||
detail: "Open your project → click the Connect button at the top of the page.",
|
||||
},
|
||||
{
|
||||
title: "Copy a Postgres connection string",
|
||||
detail:
|
||||
"Direct: postgresql://postgres:[YOUR-PASSWORD]@db.[project-ref].supabase.co:5432/postgres " +
|
||||
"(needs IPv6, or the IPv4 add-on). For IPv4-only networks use the shared pooler session mode: " +
|
||||
"postgres://postgres.[project-ref]:[YOUR-PASSWORD]@aws-[REGION].pooler.supabase.com:5432/postgres.",
|
||||
},
|
||||
{
|
||||
title: "Paste it into Gridline's Connection URI",
|
||||
detail: "It auto-detects as PostgreSQL; the Supabase tab highlights. Save stores the password in your OS keychain.",
|
||||
},
|
||||
{
|
||||
title: "Enable SSL",
|
||||
detail: "Supabase requires SSL — under SSH / SSL set the mode to Require (or Verify Full with the project root cert).",
|
||||
},
|
||||
],
|
||||
},
|
||||
neon: {
|
||||
blurb: "Serverless Postgres with pooled or direct endpoints.",
|
||||
sslRequired: true,
|
||||
steps: [
|
||||
{
|
||||
title: "Open the Neon Console",
|
||||
detail: "On your Project Dashboard, click Connect to open the 'Connect to your database' modal.",
|
||||
},
|
||||
{
|
||||
title: "Pick branch, compute, database, role",
|
||||
detail: "A connection string is built for you. The default is the pooled endpoint (host ends in -pooler…neon.tech).",
|
||||
},
|
||||
{
|
||||
title: "Copy the connection string",
|
||||
detail:
|
||||
"postgresql://[role]:[password]@ep-[compute-id]-pooler.[region].aws.neon.tech/[dbname]?sslmode=require " +
|
||||
"— port 5432 for both pooled and direct. If your client can't parse user:pass@host, fill Host/Port/User/Password manually.",
|
||||
},
|
||||
{
|
||||
title: "Paste it into Gridline's Connection URI",
|
||||
detail: "Auto-detects as PostgreSQL; the NeonDB tab highlights. Save stores the password in your OS keychain.",
|
||||
},
|
||||
{
|
||||
title: "Enable SSL",
|
||||
detail: "Neon requires SSL/TLS (strings ship with sslmode=require) — confirm SSH / SSL → mode Require.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { INPUT_ROUNDING } from "./uiConstants";
|
||||
|
||||
describe("uiConstants", () => {
|
||||
it("exposes a rounded (non-pill) radius class for inputs", () => {
|
||||
expect(INPUT_ROUNDING).toBe("rounded-lg");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/** Shared Tailwind radius class for all form controls (inputs, selects, textareas).
|
||||
* Pill-shaped (`rounded-full`) controls were retired in v0.7.0 in favor of a
|
||||
* rounded rectangle. Import this constant so the radius is defined once. */
|
||||
export const INPUT_ROUNDING = "rounded-lg";
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
describe("version", () => {
|
||||
it("declares v0.6.0 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.6.0");
|
||||
it("declares v0.7.0 across the app shell", () => {
|
||||
expect(pkg.version).toBe("0.7.0");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user