feat: add per-connection read-only mode
Connections default to read-only. SQL editor wraps queries in a read-only transaction so PostgreSQL rejects mutations. Data mutation commands (update_row, insert_row, delete_rows) are blocked at the Rust layer. Toolbar toggle with confirmation dialog lets users switch to read-write. Badges shown in workspace, table viewer, and status bar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
68
src/components/layout/ReadOnlyToggle.tsx
Normal file
68
src/components/layout/ReadOnlyToggle.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { useToggleReadOnly } from "@/hooks/use-read-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Lock, LockOpen } from "lucide-react";
|
||||
|
||||
export function ReadOnlyToggle() {
|
||||
const { activeConnectionId, readOnlyMap, connectedIds } = useAppStore();
|
||||
const toggleMutation = useToggleReadOnly();
|
||||
|
||||
if (!activeConnectionId || !connectedIds.has(activeConnectionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isReadOnly = readOnlyMap[activeConnectionId] ?? true;
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isReadOnly) {
|
||||
const confirmed = window.confirm(
|
||||
"Switch to Read-Write mode?\n\nThis will allow executing INSERT, UPDATE, DELETE, DROP, and other mutating queries."
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
toggleMutation.mutate({
|
||||
connectionId: activeConnectionId,
|
||||
readOnly: !isReadOnly,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-7 gap-1.5 text-xs font-medium ${
|
||||
isReadOnly
|
||||
? "text-yellow-600 dark:text-yellow-500"
|
||||
: "text-green-600 dark:text-green-500"
|
||||
}`}
|
||||
onClick={handleToggle}
|
||||
disabled={toggleMutation.isPending}
|
||||
>
|
||||
{isReadOnly ? (
|
||||
<>
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
Read-Only
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockOpen className="h-3.5 w-3.5" />
|
||||
Read-Write
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{isReadOnly
|
||||
? "Read-Only mode: mutations are blocked. Click to enable writes."
|
||||
: "Read-Write mode: all queries are allowed. Click to restrict to read-only."}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function StatusBar({ rowCount, executionTime }: Props) {
|
||||
const { activeConnectionId, connectedIds, pgVersion } = useAppStore();
|
||||
const { activeConnectionId, connectedIds, readOnlyMap, pgVersion } = useAppStore();
|
||||
const { data: connections } = useConnections();
|
||||
|
||||
const activeConn = connections?.find((c) => c.id === activeConnectionId);
|
||||
@@ -25,6 +25,17 @@ export function StatusBar({ rowCount, executionTime }: Props) {
|
||||
/>
|
||||
{activeConn ? activeConn.name : "No connection"}
|
||||
</span>
|
||||
{isConnected && activeConnectionId && (
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
(readOnlyMap[activeConnectionId] ?? true)
|
||||
? "text-yellow-600 dark:text-yellow-500"
|
||||
: "text-green-600 dark:text-green-500"
|
||||
}`}
|
||||
>
|
||||
{(readOnlyMap[activeConnectionId] ?? true) ? "RO" : "RW"}
|
||||
</span>
|
||||
)}
|
||||
{pgVersion && (
|
||||
<span className="hidden sm:inline">{pgVersion.split(",")[0]?.replace("PostgreSQL ", "PG ")}</span>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
||||
import { ConnectionList } from "@/components/connections/ConnectionList";
|
||||
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
||||
import { ReadOnlyToggle } from "@/components/layout/ReadOnlyToggle";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { Database, Plus } from "lucide-react";
|
||||
import type { ConnectionConfig, Tab } from "@/types";
|
||||
@@ -45,6 +46,10 @@ export function Toolbar() {
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
|
||||
<ReadOnlyToggle />
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user