refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useDbViewerStore } from "../../../stores/dbViewerStore";
|
||||
import * as cmd from "../../../lib/commands";
|
||||
import type { RoleInfo } from "../../../lib/types";
|
||||
import { FormRow, FormSectionHeader, inputClass, controlClass } from "./formRow";
|
||||
|
||||
interface RoleMember {
|
||||
name: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
interface RoleAction {
|
||||
op: "create" | "edit" | "drop";
|
||||
login: boolean;
|
||||
superuser: boolean;
|
||||
createdb: boolean;
|
||||
createrole: boolean;
|
||||
inherit: boolean;
|
||||
replication: boolean;
|
||||
bypassrls: boolean;
|
||||
connection_limit: number;
|
||||
valid_until: string;
|
||||
password: string;
|
||||
members: RoleMember[];
|
||||
}
|
||||
|
||||
interface RoleParams {
|
||||
schema: string;
|
||||
name: string;
|
||||
action: RoleAction;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
connectionId: string;
|
||||
mode?: "create" | "edit";
|
||||
params: Record<string, unknown>;
|
||||
onChange: (p: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const OPTIONS: { key: keyof Omit<RoleAction, "op" | "connection_limit" | "valid_until" | "password" | "members">; label: string }[] = [
|
||||
{ key: "login", label: "LOGIN" },
|
||||
{ key: "superuser", label: "SUPERUSER" },
|
||||
{ key: "createdb", label: "CREATEDB" },
|
||||
{ key: "createrole", label: "CREATEROLE" },
|
||||
{ key: "inherit", label: "INHERIT" },
|
||||
{ key: "replication", label: "REPLICATION" },
|
||||
{ key: "bypassrls", label: "BYPASSRLS" },
|
||||
];
|
||||
|
||||
function patchAction(params: RoleParams, patch: Partial<RoleAction>): RoleParams {
|
||||
return { ...params, action: { ...params.action, ...patch } };
|
||||
}
|
||||
|
||||
export function RoleForm({ connectionId, mode = "create", params, onChange }: Props) {
|
||||
const typed = params as unknown as RoleParams;
|
||||
const action = typed.action;
|
||||
|
||||
const [view, setView] = useState<"visual" | "sql">("visual");
|
||||
const [preview, setPreview] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [staging, setStaging] = useState(false);
|
||||
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
||||
|
||||
const setParams = (next: RoleParams) => onChange(next as unknown as Record<string, unknown>);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
cmd
|
||||
.getRoles(connectionId)
|
||||
.then((list) => {
|
||||
if (active) setRoles(list);
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setRoles([]);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [connectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setError(null);
|
||||
cmd
|
||||
.buildObjectDdl(connectionId, "role", params)
|
||||
.then((sqls) => {
|
||||
if (active) setPreview(Array.isArray(sqls) ? sqls.join("\n;\n") : (sqls as string));
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (active) {
|
||||
setPreview("");
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [connectionId, params]);
|
||||
|
||||
const availableRoles = useMemo(
|
||||
() => roles.filter((r) => r.name !== typed.name),
|
||||
[roles, typed.name],
|
||||
);
|
||||
|
||||
const toggleMember = (name: string, checked: boolean) => {
|
||||
const next = checked
|
||||
? [...action.members, { name, admin: false }]
|
||||
: action.members.filter((m) => m.name !== name);
|
||||
setParams(patchAction(typed, { members: next }));
|
||||
};
|
||||
|
||||
const setMemberAdmin = (name: string, admin: boolean) => {
|
||||
const next = action.members.map((m) => (m.name === name ? { ...m, admin } : m));
|
||||
setParams(patchAction(typed, { members: next }));
|
||||
};
|
||||
|
||||
const stage = async () => {
|
||||
setStaging(true);
|
||||
setError(null);
|
||||
try {
|
||||
const sqls = await cmd.buildObjectDdl(connectionId, "role", params);
|
||||
const list = Array.isArray(sqls) ? sqls : [sqls];
|
||||
list.forEach((sql) =>
|
||||
useDbViewerStore.getState().addChange({
|
||||
type: "ddl",
|
||||
sql,
|
||||
description: `${mode === "edit" ? "Edit" : "Create"} role ${typed.name}`,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setStaging(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-transparent">
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-2">
|
||||
<span className="text-[11px] font-semibold text-text-muted uppercase tracking-wider">
|
||||
{mode} role
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex rounded-md border border-border overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Visual"
|
||||
onClick={() => setView("visual")}
|
||||
className={[
|
||||
"px-2 py-0.5 text-xs transition-colors cursor-pointer",
|
||||
view === "visual" ? "bg-surface-raised text-text" : "text-text-muted hover:text-text",
|
||||
].join(" ")}
|
||||
>
|
||||
Visual
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="SQL"
|
||||
onClick={() => setView("sql")}
|
||||
className={[
|
||||
"px-2 py-0.5 text-xs transition-colors cursor-pointer",
|
||||
view === "sql" ? "bg-surface-raised text-text" : "text-text-muted hover:text-text",
|
||||
].join(" ")}
|
||||
>
|
||||
SQL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={stage}
|
||||
disabled={!!error || staging || !typed.name.trim()}
|
||||
className="rounded-lg bg-accent px-3 py-1.5 text-xs font-medium text-white hover:bg-accent-hover disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
Stage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4 space-y-3">
|
||||
{view === "visual" ? (
|
||||
<>
|
||||
<FormRow label="Name">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Role name"
|
||||
value={typed.name}
|
||||
onChange={(e) => setParams({ ...typed, name: e.target.value })}
|
||||
className={inputClass}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<div className="border-b border-border">
|
||||
<FormSectionHeader label="Options" />
|
||||
<div className="px-4 py-2 grid grid-cols-2 gap-2">
|
||||
{OPTIONS.map(({ key, label }) => (
|
||||
<label
|
||||
key={key}
|
||||
className="flex items-center gap-2 text-xs text-text cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={label}
|
||||
checked={!!action[key]}
|
||||
onChange={(e) =>
|
||||
setParams(patchAction(typed, { [key]: e.target.checked } as Partial<RoleAction>))
|
||||
}
|
||||
className="rounded border-border bg-surface text-accent focus:ring-accent"
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormRow label="Connection limit">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="-1 for unlimited"
|
||||
value={action.connection_limit}
|
||||
onChange={(e) =>
|
||||
setParams(
|
||||
patchAction(typed, {
|
||||
connection_limit: e.target.value === "" ? -1 : Number(e.target.value),
|
||||
}),
|
||||
)
|
||||
}
|
||||
className={inputClass}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="Valid until">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={action.valid_until}
|
||||
onChange={(e) => setParams(patchAction(typed, { valid_until: e.target.value }))}
|
||||
className={controlClass}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="Password">
|
||||
<input
|
||||
type="password"
|
||||
placeholder={
|
||||
mode === "edit"
|
||||
? "leave blank to keep current password"
|
||||
: "Password"
|
||||
}
|
||||
value={action.password}
|
||||
onChange={(e) => setParams(patchAction(typed, { password: e.target.value }))}
|
||||
className={inputClass}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<div className="border-b border-border">
|
||||
<FormSectionHeader label="Member of" count={action.members.length} />
|
||||
{availableRoles.length === 0 && (
|
||||
<p className="px-4 py-2 text-xs text-text-muted">No other roles available.</p>
|
||||
)}
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
{availableRoles.map((r) => {
|
||||
const member = action.members.find((m) => m.name === r.name);
|
||||
return (
|
||||
<div key={r.name} className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-text cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!member}
|
||||
onChange={(e) => toggleMember(r.name, e.target.checked)}
|
||||
className="rounded border-border bg-surface text-accent focus:ring-accent"
|
||||
/>
|
||||
<span className="font-mono">{r.name}</span>
|
||||
</label>
|
||||
{member && (
|
||||
<label className="flex items-center gap-1 text-xs text-text-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={member.admin}
|
||||
onChange={(e) => setMemberAdmin(r.name, e.target.checked)}
|
||||
className="rounded border-border bg-surface text-accent focus:ring-accent"
|
||||
/>
|
||||
ADMIN
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<pre className="text-xs leading-6 font-mono whitespace-pre-wrap text-text">{preview}</pre>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user