supascan 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bun-version +1 -0
- package/.github/workflows/release-github.yml +70 -0
- package/.github/workflows/release-npm.yml +45 -0
- package/.github/workflows/tests.yml +36 -0
- package/LICENCE +22 -0
- package/README.md +115 -0
- package/apps/cli/build.ts +37 -0
- package/apps/cli/package.json +29 -0
- package/apps/cli/src/commands/analyze.ts +213 -0
- package/apps/cli/src/commands/dump.ts +68 -0
- package/apps/cli/src/commands/rpc.ts +67 -0
- package/apps/cli/src/context.ts +96 -0
- package/apps/cli/src/embedded-report.ts +1 -0
- package/apps/cli/src/formatters/console.ts +39 -0
- package/apps/cli/src/formatters/events.ts +95 -0
- package/apps/cli/src/index.ts +105 -0
- package/apps/cli/src/types.ts +9 -0
- package/apps/cli/src/utils/args.ts +46 -0
- package/apps/cli/src/utils/browser.ts +29 -0
- package/apps/cli/src/utils/files.ts +12 -0
- package/apps/cli/src/version.ts +3 -0
- package/apps/web/build.ts +68 -0
- package/apps/web/dev.ts +5 -0
- package/apps/web/index.html +75 -0
- package/apps/web/package.json +23 -0
- package/apps/web/src/App.tsx +129 -0
- package/apps/web/src/components/QueryBuilder.tsx +174 -0
- package/apps/web/src/components/QueryWindow.tsx +133 -0
- package/apps/web/src/components/RPCExecutor.tsx +176 -0
- package/apps/web/src/components/SchemaBrowser.tsx +269 -0
- package/apps/web/src/components/SmartTable.tsx +129 -0
- package/apps/web/src/components/TargetConfig.tsx +130 -0
- package/apps/web/src/components/TargetSummary.tsx +105 -0
- package/apps/web/src/hooks/useAnalysis.ts +54 -0
- package/apps/web/src/hooks/useNotification.ts +28 -0
- package/apps/web/src/hooks/useRPC.ts +53 -0
- package/apps/web/src/hooks/useSupabase.ts +46 -0
- package/apps/web/src/hooks/useTableQuery.ts +148 -0
- package/apps/web/src/index.tsx +18 -0
- package/apps/web/src/types.ts +16 -0
- package/apps/web/src/utils/hash.ts +27 -0
- package/context.test.ts +93 -0
- package/package.json +48 -0
- package/package.publish.json +18 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/analyzer.ts +212 -0
- package/packages/core/src/extractor.ts +233 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/supabase.ts +316 -0
- package/packages/core/src/types/analyzer.types.ts +72 -0
- package/packages/core/src/types/event.types.ts +4 -0
- package/packages/core/src/types/events.types.ts +5 -0
- package/packages/core/src/types/extractor.types.ts +54 -0
- package/packages/core/src/types/result.types.ts +17 -0
- package/packages/core/src/types/supabase.types.ts +98 -0
- package/tsconfig.json +23 -0
- package/turbo.json +19 -0
- package/utils.test.ts +68 -0
- package/version.ts +3 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNotification } from "../hooks/useNotification";
|
|
3
|
+
import { useTableQuery } from "../hooks/useTableQuery";
|
|
4
|
+
import type { SupabaseClient } from "../types";
|
|
5
|
+
import { SmartTable } from "./SmartTable";
|
|
6
|
+
|
|
7
|
+
interface QueryBuilderProps {
|
|
8
|
+
client: SupabaseClient | null;
|
|
9
|
+
schema: string;
|
|
10
|
+
table: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Operation = "select" | "insert" | "update" | "delete";
|
|
14
|
+
|
|
15
|
+
export function QueryBuilder({ client, schema, table }: QueryBuilderProps) {
|
|
16
|
+
const [operation, setOperation] = useState<Operation>("select");
|
|
17
|
+
const [columns, setColumns] = useState("*");
|
|
18
|
+
const [limit, setLimit] = useState("10");
|
|
19
|
+
const [orderBy, setOrderBy] = useState("");
|
|
20
|
+
const [jsonData, setJsonData] = useState("{}");
|
|
21
|
+
const [filter, setFilter] = useState("");
|
|
22
|
+
|
|
23
|
+
const { state, execute } = useTableQuery(client, schema, table);
|
|
24
|
+
const notify = useNotification();
|
|
25
|
+
|
|
26
|
+
const handleExecute = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const parsedLimit = parseInt(limit);
|
|
29
|
+
const [orderCol, orderDir] = orderBy.trim().split(" ");
|
|
30
|
+
|
|
31
|
+
let data: Record<string, unknown> | undefined;
|
|
32
|
+
if (operation !== "select") {
|
|
33
|
+
try {
|
|
34
|
+
data = JSON.parse(jsonData);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
notify("Invalid JSON data", "error");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await execute({
|
|
42
|
+
operation,
|
|
43
|
+
columns: columns.trim() || "*",
|
|
44
|
+
limit: parsedLimit > 0 ? parsedLimit : undefined,
|
|
45
|
+
orderBy: orderCol || undefined,
|
|
46
|
+
orderDir: orderDir === "desc" ? "desc" : "asc",
|
|
47
|
+
data,
|
|
48
|
+
filter: filter.trim() || undefined,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
notify("Query executed successfully", "success");
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
notify(message, "error");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="h-full flex flex-col">
|
|
60
|
+
<div className="p-4 space-y-3 border-b border-gray-200">
|
|
61
|
+
<select
|
|
62
|
+
value={operation}
|
|
63
|
+
onChange={(e) => setOperation(e.target.value as Operation)}
|
|
64
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono bg-white focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
65
|
+
>
|
|
66
|
+
<option value="select">SELECT</option>
|
|
67
|
+
<option value="insert">INSERT</option>
|
|
68
|
+
<option value="update">UPDATE</option>
|
|
69
|
+
<option value="delete">DELETE</option>
|
|
70
|
+
</select>
|
|
71
|
+
|
|
72
|
+
{operation === "select" && (
|
|
73
|
+
<>
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
value={columns}
|
|
77
|
+
onChange={(e) => setColumns(e.target.value)}
|
|
78
|
+
placeholder="* or column1, column2"
|
|
79
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
80
|
+
/>
|
|
81
|
+
<div className="grid grid-cols-2 gap-2">
|
|
82
|
+
<input
|
|
83
|
+
type="number"
|
|
84
|
+
value={limit}
|
|
85
|
+
onChange={(e) => setLimit(e.target.value)}
|
|
86
|
+
placeholder="Limit"
|
|
87
|
+
className="p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
88
|
+
/>
|
|
89
|
+
<input
|
|
90
|
+
type="text"
|
|
91
|
+
value={orderBy}
|
|
92
|
+
onChange={(e) => setOrderBy(e.target.value)}
|
|
93
|
+
placeholder="column asc/desc"
|
|
94
|
+
className="p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{operation !== "select" && (
|
|
101
|
+
<>
|
|
102
|
+
<textarea
|
|
103
|
+
value={jsonData}
|
|
104
|
+
onChange={(e) => setJsonData(e.target.value)}
|
|
105
|
+
placeholder={
|
|
106
|
+
operation === "insert"
|
|
107
|
+
? '{"column": "value"}'
|
|
108
|
+
: '{"column": "new value"}'
|
|
109
|
+
}
|
|
110
|
+
rows={operation === "update" ? 2 : 3}
|
|
111
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
112
|
+
/>
|
|
113
|
+
{(operation === "update" || operation === "delete") && (
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
value={filter}
|
|
117
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
118
|
+
placeholder="id = 1"
|
|
119
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
<button
|
|
126
|
+
onClick={handleExecute}
|
|
127
|
+
disabled={state.status === "loading" || !client}
|
|
128
|
+
className="w-full px-4 py-2 bg-slate-700 text-white rounded text-sm font-mono hover:bg-slate-800 disabled:opacity-50 transition-colors"
|
|
129
|
+
>
|
|
130
|
+
{state.status === "loading" ? "Executing..." : "Execute Query"}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="flex-1 flex flex-col border-t border-gray-200 min-h-0">
|
|
135
|
+
<div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
|
|
136
|
+
<h4 className="text-xs font-semibold text-gray-700 font-mono">
|
|
137
|
+
Results
|
|
138
|
+
</h4>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex-1 overflow-auto">
|
|
141
|
+
{state.status === "loading" && (
|
|
142
|
+
<div className="p-8 text-center">
|
|
143
|
+
<div className="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
|
|
144
|
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-slate-300 border-t-slate-600"></div>
|
|
145
|
+
Executing query...
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{state.status === "error" && (
|
|
151
|
+
<div className="p-4 bg-red-50 border-b border-red-200 text-xs text-red-700 font-mono">
|
|
152
|
+
Error: {state.error.message}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{state.status === "success" &&
|
|
157
|
+
(state.data.length > 0 ? (
|
|
158
|
+
<SmartTable data={state.data} />
|
|
159
|
+
) : (
|
|
160
|
+
<div className="p-6 text-center text-gray-400 text-sm font-mono">
|
|
161
|
+
No data returned
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
|
|
165
|
+
{state.status === "idle" && (
|
|
166
|
+
<div className="p-6 text-center text-gray-400 text-sm font-mono">
|
|
167
|
+
Query results will appear here...
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface QueryWindowProps {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onFocus: () => void;
|
|
8
|
+
zIndex: number;
|
|
9
|
+
initialPosition?: { x: number; y: number };
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function QueryWindow({
|
|
14
|
+
id,
|
|
15
|
+
title,
|
|
16
|
+
onClose,
|
|
17
|
+
onFocus,
|
|
18
|
+
zIndex,
|
|
19
|
+
initialPosition = { x: 100, y: 100 },
|
|
20
|
+
children,
|
|
21
|
+
}: QueryWindowProps) {
|
|
22
|
+
const [position, setPosition] = useState(initialPosition);
|
|
23
|
+
const [size, setSize] = useState({ width: 800, height: 600 });
|
|
24
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
25
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
26
|
+
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
27
|
+
const windowRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
31
|
+
if (isDragging) {
|
|
32
|
+
setPosition({
|
|
33
|
+
x: Math.max(0, e.clientX - dragStart.x),
|
|
34
|
+
y: Math.max(0, e.clientY - dragStart.y),
|
|
35
|
+
});
|
|
36
|
+
} else if (isResizing) {
|
|
37
|
+
const newWidth = Math.max(400, e.clientX - position.x);
|
|
38
|
+
const newHeight = Math.max(300, e.clientY - position.y);
|
|
39
|
+
setSize({ width: newWidth, height: newHeight });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleMouseUp = () => {
|
|
44
|
+
setIsDragging(false);
|
|
45
|
+
setIsResizing(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (isDragging || isResizing) {
|
|
49
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
50
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
53
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}, [isDragging, isResizing, dragStart, position.x, position.y]);
|
|
57
|
+
|
|
58
|
+
const handleHeaderMouseDown = (e: React.MouseEvent) => {
|
|
59
|
+
if ((e.target as HTMLElement).closest(".window-controls")) return;
|
|
60
|
+
onFocus();
|
|
61
|
+
setIsDragging(true);
|
|
62
|
+
setDragStart({
|
|
63
|
+
x: e.clientX - position.x,
|
|
64
|
+
y: e.clientY - position.y,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleResizeMouseDown = (e: React.MouseEvent) => {
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
onFocus();
|
|
71
|
+
setIsResizing(true);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleWindowClick = () => {
|
|
75
|
+
onFocus();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
ref={windowRef}
|
|
81
|
+
className="fixed bg-white rounded-lg shadow-2xl border border-gray-300 flex flex-col overflow-hidden"
|
|
82
|
+
style={{
|
|
83
|
+
left: `${position.x}px`,
|
|
84
|
+
top: `${position.y}px`,
|
|
85
|
+
width: `${size.width}px`,
|
|
86
|
+
height: `${size.height}px`,
|
|
87
|
+
zIndex: 1000 + zIndex,
|
|
88
|
+
}}
|
|
89
|
+
onClick={handleWindowClick}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
className="bg-gradient-to-r from-slate-700 to-slate-600 px-4 py-3 flex items-center justify-between cursor-move select-none"
|
|
93
|
+
onMouseDown={handleHeaderMouseDown}
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<div className="flex gap-1.5 window-controls">
|
|
97
|
+
<button
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 transition-colors"
|
|
100
|
+
title="Close"
|
|
101
|
+
/>
|
|
102
|
+
<button
|
|
103
|
+
className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 transition-colors opacity-50 cursor-default"
|
|
104
|
+
title="Minimize"
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 transition-colors opacity-50 cursor-default"
|
|
108
|
+
title="Maximize"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
<h3 className="text-sm font-semibold text-white font-mono ml-2 truncate">
|
|
112
|
+
{title}
|
|
113
|
+
</h3>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex-1 overflow-auto">{children}</div>
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize"
|
|
121
|
+
onMouseDown={handleResizeMouseDown}
|
|
122
|
+
>
|
|
123
|
+
<svg
|
|
124
|
+
className="w-4 h-4 text-gray-400"
|
|
125
|
+
fill="currentColor"
|
|
126
|
+
viewBox="0 0 16 16"
|
|
127
|
+
>
|
|
128
|
+
<path d="M16 16V14l-2-2v4h4zm0-6V8l-4-4v4l4 4zm0-6V2l-6-2v4l6 2z" />
|
|
129
|
+
</svg>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { RPCFunction } from "@supascan/core";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useNotification } from "../hooks/useNotification";
|
|
4
|
+
import { useRPC } from "../hooks/useRPC";
|
|
5
|
+
import type { SupabaseClient } from "../types";
|
|
6
|
+
import { SmartTable } from "./SmartTable";
|
|
7
|
+
|
|
8
|
+
interface RPCExecutorProps {
|
|
9
|
+
client: SupabaseClient | null;
|
|
10
|
+
schema: string;
|
|
11
|
+
rpc: RPCFunction;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseParamValue(value: string, type: string): unknown {
|
|
15
|
+
if (value === "") return undefined;
|
|
16
|
+
if (type === "number" || type === "integer") return parseFloat(value);
|
|
17
|
+
if (type === "boolean") return value === "true";
|
|
18
|
+
if (type === "json" || type === "jsonb") return JSON.parse(value);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function RPCExecutor({ client, schema, rpc }: RPCExecutorProps) {
|
|
23
|
+
const [params, setParams] = useState<Record<string, string>>({});
|
|
24
|
+
const { state, execute } = useRPC(client, schema, rpc.name);
|
|
25
|
+
const notify = useNotification();
|
|
26
|
+
|
|
27
|
+
const handleParamChange = (name: string, value: string) => {
|
|
28
|
+
if (value === "") {
|
|
29
|
+
const { [name]: _, ...rest } = params;
|
|
30
|
+
setParams(rest);
|
|
31
|
+
} else {
|
|
32
|
+
setParams({ ...params, [name]: value });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleExecute = async () => {
|
|
37
|
+
try {
|
|
38
|
+
const parsedParams = Object.entries(params).reduce(
|
|
39
|
+
(acc, [key, value]) => {
|
|
40
|
+
const param = rpc.parameters.find((p) => p.name === key);
|
|
41
|
+
if (param) {
|
|
42
|
+
try {
|
|
43
|
+
acc[key] = parseParamValue(value, param.type);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
notify(`Invalid value for parameter "${key}"`, "error");
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return acc;
|
|
50
|
+
},
|
|
51
|
+
{} as Record<string, unknown>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await execute(parsedParams);
|
|
55
|
+
notify(`RPC ${rpc.name} executed successfully`, "success");
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
notify(message, "error");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = state.status === "success" ? state.data : null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="h-full flex flex-col">
|
|
66
|
+
<div className="p-4 space-y-3 border-b border-gray-200">
|
|
67
|
+
{rpc.parameters.length > 0 && (
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
{rpc.parameters.map((param) => (
|
|
70
|
+
<div key={param.name}>
|
|
71
|
+
<label className="block text-xs font-medium text-gray-600 mb-1 font-mono">
|
|
72
|
+
{param.name} ({param.type})
|
|
73
|
+
{param.required && <span className="text-red-600"> *</span>}
|
|
74
|
+
</label>
|
|
75
|
+
{param.type === "boolean" ? (
|
|
76
|
+
<select
|
|
77
|
+
value={params[param.name] || ""}
|
|
78
|
+
onChange={(e) =>
|
|
79
|
+
handleParamChange(param.name, e.target.value)
|
|
80
|
+
}
|
|
81
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono bg-white focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
82
|
+
>
|
|
83
|
+
<option value="">Select...</option>
|
|
84
|
+
<option value="true">true</option>
|
|
85
|
+
<option value="false">false</option>
|
|
86
|
+
</select>
|
|
87
|
+
) : param.type === "json" || param.type === "jsonb" ? (
|
|
88
|
+
<textarea
|
|
89
|
+
value={params[param.name] || ""}
|
|
90
|
+
onChange={(e) =>
|
|
91
|
+
handleParamChange(param.name, e.target.value)
|
|
92
|
+
}
|
|
93
|
+
placeholder="{}"
|
|
94
|
+
rows={2}
|
|
95
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<input
|
|
99
|
+
type={
|
|
100
|
+
param.type === "number" || param.type === "integer"
|
|
101
|
+
? "number"
|
|
102
|
+
: "text"
|
|
103
|
+
}
|
|
104
|
+
value={params[param.name] || ""}
|
|
105
|
+
onChange={(e) =>
|
|
106
|
+
handleParamChange(param.name, e.target.value)
|
|
107
|
+
}
|
|
108
|
+
placeholder={param.description || `Enter ${param.type}`}
|
|
109
|
+
className="w-full p-2 text-sm border border-gray-300 rounded font-mono focus:ring-2 focus:ring-supabase-green focus:border-supabase-green"
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleExecute}
|
|
119
|
+
disabled={state.status === "loading" || !client}
|
|
120
|
+
className="w-full px-4 py-2 bg-slate-700 text-white rounded text-sm font-mono hover:bg-slate-800 transition-colors disabled:opacity-50"
|
|
121
|
+
>
|
|
122
|
+
{state.status === "loading" ? "Executing..." : "Execute RPC"}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="flex-1 flex flex-col border-t border-gray-200 min-h-0">
|
|
127
|
+
<div className="px-4 py-2 bg-gray-50 border-b border-gray-200">
|
|
128
|
+
<h4 className="text-xs font-semibold text-gray-700 font-mono">
|
|
129
|
+
Results
|
|
130
|
+
</h4>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex-1 overflow-auto">
|
|
133
|
+
{state.status === "loading" && (
|
|
134
|
+
<div className="p-8 text-center">
|
|
135
|
+
<div className="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
|
|
136
|
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-slate-300 border-t-slate-600"></div>
|
|
137
|
+
Executing RPC...
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{state.status === "error" && (
|
|
143
|
+
<div className="p-4 bg-red-50 border-b border-red-200 text-xs text-red-700 font-mono">
|
|
144
|
+
Error: {state.error.message}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{state.status === "success" &&
|
|
149
|
+
(Array.isArray(result) && result.length > 0 ? (
|
|
150
|
+
<SmartTable data={result as Record<string, unknown>[]} />
|
|
151
|
+
) : Array.isArray(result) && result.length === 0 ? (
|
|
152
|
+
<div className="p-6 text-center text-gray-400 text-sm font-mono">
|
|
153
|
+
No data returned
|
|
154
|
+
</div>
|
|
155
|
+
) : typeof result === "object" && result !== null ? (
|
|
156
|
+
<div className="p-4">
|
|
157
|
+
<pre className="text-xs font-mono whitespace-pre-wrap">
|
|
158
|
+
{JSON.stringify(result, null, 2)}
|
|
159
|
+
</pre>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<div className="p-4">
|
|
163
|
+
<div className="text-xs font-mono">{String(result)}</div>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
|
|
167
|
+
{state.status === "idle" && (
|
|
168
|
+
<div className="p-6 text-center text-gray-400 text-sm font-mono">
|
|
169
|
+
RPC results will appear here...
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|