@kyro-cms/admin 0.9.5 → 0.9.7
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/dist/index.cjs +659 -684
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +54 -51
- package/dist/index.css.map +1 -1
- package/dist/index.js +660 -685
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar.tsx +172 -292
- package/src/components/Admin.tsx +7 -1
- package/src/components/AutoForm.tsx +573 -367
- package/src/components/DetailView.tsx +22 -47
- package/src/components/GraphQLPlayground.tsx +473 -223
- package/src/components/ListView.tsx +1 -1
- package/src/components/MediaGallery.tsx +2 -2
- package/src/components/RestPlayground.tsx +482 -519
- package/src/components/blocks/AccordionBlock.tsx +1 -1
- package/src/components/blocks/ArrayBlock.tsx +1 -1
- package/src/components/blocks/ChildBlocksTree.tsx +6 -6
- package/src/components/blocks/CodeBlock.tsx +1 -1
- package/src/components/blocks/FileBlock.tsx +1 -1
- package/src/components/blocks/HeroBlock.tsx +1 -1
- package/src/components/blocks/ListBlock.tsx +1 -1
- package/src/components/blocks/RelationshipBlock.tsx +1 -1
- package/src/components/blocks/RichTextBlock.tsx +1 -1
- package/src/components/blocks/VideoBlock.tsx +1 -1
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/RichTextField.tsx +3 -1
- package/src/components/ui/SplitButton.tsx +1 -1
- package/src/components/ui/Toast.tsx +2 -1
- package/src/layouts/AdminLayout.astro +16 -1
- package/src/pages/graphql-explorer.astro +7 -51
- package/src/pages/graphql.astro +7 -119
- package/src/pages/index.astro +4 -63
- package/src/pages/rest-playground.astro +3 -29
- package/src/styles/main.css +53 -43
|
@@ -2,26 +2,26 @@ import React, {
|
|
|
2
2
|
useState,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
-
|
|
5
|
+
useRef,
|
|
6
6
|
Suspense,
|
|
7
7
|
lazy,
|
|
8
8
|
} from "react";
|
|
9
9
|
import {
|
|
10
10
|
Book,
|
|
11
|
-
Send,
|
|
12
11
|
Trash2,
|
|
13
12
|
Copy,
|
|
14
13
|
RefreshCw,
|
|
15
|
-
Settings,
|
|
16
|
-
Maximize2,
|
|
17
14
|
ChevronRight,
|
|
18
|
-
ChevronDown,
|
|
19
|
-
Search,
|
|
20
|
-
Type,
|
|
21
15
|
Activity,
|
|
22
16
|
Zap,
|
|
23
17
|
Info,
|
|
24
|
-
X
|
|
18
|
+
X,
|
|
19
|
+
Download,
|
|
20
|
+
Check,
|
|
21
|
+
Code2,
|
|
22
|
+
Play,
|
|
23
|
+
Clock,
|
|
24
|
+
Terminal,
|
|
25
25
|
} from "./ui/icons";
|
|
26
26
|
|
|
27
27
|
interface GraphQLPlaygroundProps {
|
|
@@ -33,25 +33,47 @@ interface GraphQLPlaygroundProps {
|
|
|
33
33
|
|
|
34
34
|
interface QueryTab {
|
|
35
35
|
id: string;
|
|
36
|
-
name: string;
|
|
37
36
|
query: string;
|
|
38
37
|
variables: string;
|
|
39
38
|
headers: string;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
interface HistoryEntry {
|
|
42
|
+
id: string;
|
|
43
|
+
query: string;
|
|
44
|
+
variables: string;
|
|
45
|
+
response: string;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
duration: number;
|
|
48
|
+
statusCode: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
const CodeMirrorEditor = lazy(() =>
|
|
44
52
|
import("@uiw/react-codemirror").then((mod) => ({ default: mod.default })),
|
|
45
53
|
);
|
|
46
54
|
import { javascript } from "@codemirror/lang-javascript";
|
|
47
|
-
import { githubLight } from "@uiw/codemirror-theme-github";
|
|
48
55
|
import { aura } from "@uiw/codemirror-theme-aura";
|
|
49
|
-
|
|
56
|
+
|
|
57
|
+
function prettifyQuery(query: string): string {
|
|
58
|
+
let indent = 0;
|
|
59
|
+
let result = "";
|
|
60
|
+
const lines = query.split("\n");
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (!trimmed) continue;
|
|
64
|
+
if (trimmed.startsWith("}") || trimmed.startsWith("]")) indent = Math.max(0, indent - 1);
|
|
65
|
+
result += " ".repeat(indent) + trimmed + "\n";
|
|
66
|
+
if (trimmed.endsWith("{") || trimmed.endsWith("[")) indent++;
|
|
67
|
+
const opens = (trimmed.match(/\{/g) || []).length;
|
|
68
|
+
const closes = (trimmed.match(/\}/g) || []).length;
|
|
69
|
+
indent += opens - closes;
|
|
70
|
+
}
|
|
71
|
+
return result.trim();
|
|
72
|
+
}
|
|
50
73
|
|
|
51
74
|
const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
52
|
-
#
|
|
75
|
+
# Cmd+Enter to run, Cmd+Shift+P to prettify
|
|
53
76
|
|
|
54
|
-
# 1. Introspection - Discover the schema
|
|
55
77
|
{
|
|
56
78
|
__schema {
|
|
57
79
|
types {
|
|
@@ -65,21 +87,15 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
#
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
# 3. Ping the API
|
|
77
|
-
{
|
|
78
|
-
ping
|
|
79
|
-
}
|
|
90
|
+
# Example: fetch all posts (uncomment to use)
|
|
91
|
+
# {
|
|
92
|
+
# posts(page: 1, limit: 10) {
|
|
93
|
+
# docs { id title slug status }
|
|
94
|
+
# totalDocs
|
|
95
|
+
# }
|
|
96
|
+
# }
|
|
80
97
|
`;
|
|
81
98
|
|
|
82
|
-
// --- Docs Types ---
|
|
83
99
|
interface TypeInfo {
|
|
84
100
|
name: string;
|
|
85
101
|
kind: string;
|
|
@@ -120,38 +136,40 @@ export function GraphQLPlayground({
|
|
|
120
136
|
initialShowDocs = false,
|
|
121
137
|
}: GraphQLPlaygroundProps) {
|
|
122
138
|
const [token, setToken] = useState<string>("");
|
|
139
|
+
const [showToken, setShowToken] = useState(false);
|
|
123
140
|
const [isConnected, setIsConnected] = useState(false);
|
|
124
|
-
const [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
headers: "",
|
|
131
|
-
},
|
|
132
|
-
]);
|
|
133
|
-
const [currentTabId, setCurrentTabId] = useState("default");
|
|
141
|
+
const [tab, setTab] = useState<QueryTab>({
|
|
142
|
+
id: "default",
|
|
143
|
+
query: initialQuery || DEFAULT_QUERY,
|
|
144
|
+
variables: initialVariables || "{}",
|
|
145
|
+
headers: "",
|
|
146
|
+
});
|
|
134
147
|
const [response, setResponse] = useState<string>("");
|
|
135
148
|
const [isLoading, setIsLoading] = useState(false);
|
|
136
149
|
const [error, setError] = useState<string | null>(null);
|
|
137
|
-
const [
|
|
138
|
-
"query",
|
|
139
|
-
);
|
|
150
|
+
const [activeEditorTab, setActiveEditorTab] = useState<"query" | "variables" | "headers">("query");
|
|
140
151
|
const [isMounted, setIsMounted] = useState(false);
|
|
141
152
|
const [showDocs, setShowDocs] = useState(initialShowDocs);
|
|
142
153
|
const [schema, setSchema] = useState<SchemaInfo | null>(null);
|
|
143
154
|
const [loadingSchema, setLoadingSchema] = useState(false);
|
|
144
155
|
const [selectedType, setSelectedType] = useState<TypeInfo | null>(null);
|
|
156
|
+
const [rightTab, setRightTab] = useState<"response" | "docs" | "history">(
|
|
157
|
+
initialShowDocs ? "docs" : "response",
|
|
158
|
+
);
|
|
159
|
+
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
|
160
|
+
const [lastDuration, setLastDuration] = useState<number>(0);
|
|
161
|
+
const [lastStatus, setLastStatus] = useState<number>(0);
|
|
162
|
+
const [copied, setCopied] = useState(false);
|
|
163
|
+
const [splitPos, setSplitPos] = useState(50);
|
|
164
|
+
const [mobilePanel, setMobilePanel] = useState<"editor" | "response">("editor");
|
|
165
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
166
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const cursorPos = useRef({ line: 1, col: 1 });
|
|
145
168
|
|
|
146
169
|
useEffect(() => {
|
|
147
170
|
setIsMounted(true);
|
|
148
171
|
}, []);
|
|
149
172
|
|
|
150
|
-
const currentTab = useMemo(
|
|
151
|
-
() => tabs.find((t) => t.id === currentTabId) || tabs[0],
|
|
152
|
-
[tabs, currentTabId],
|
|
153
|
-
);
|
|
154
|
-
|
|
155
173
|
const fetchSchema = useCallback(async () => {
|
|
156
174
|
setLoadingSchema(true);
|
|
157
175
|
try {
|
|
@@ -212,55 +230,110 @@ export function GraphQLPlayground({
|
|
|
212
230
|
}, [endpoint]);
|
|
213
231
|
|
|
214
232
|
useEffect(() => {
|
|
215
|
-
if (showDocs && !schema)
|
|
216
|
-
fetchSchema();
|
|
217
|
-
}
|
|
233
|
+
if (showDocs && !schema) fetchSchema();
|
|
218
234
|
}, [showDocs, schema, fetchSchema]);
|
|
219
235
|
|
|
220
|
-
const handleRun = async () => {
|
|
236
|
+
const handleRun = useCallback(async () => {
|
|
221
237
|
setIsLoading(true);
|
|
222
238
|
setError(null);
|
|
239
|
+
const startTime = performance.now();
|
|
223
240
|
try {
|
|
224
241
|
const headers: Record<string, string> = {
|
|
225
242
|
"Content-Type": "application/json",
|
|
226
243
|
};
|
|
227
244
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
228
|
-
|
|
229
|
-
// Add custom headers from tab
|
|
230
|
-
if (currentTab.headers) {
|
|
245
|
+
if (tab.headers) {
|
|
231
246
|
try {
|
|
232
|
-
const customHeaders = JSON.parse(
|
|
247
|
+
const customHeaders = JSON.parse(tab.headers);
|
|
233
248
|
Object.assign(headers, customHeaders);
|
|
234
|
-
} catch
|
|
235
|
-
console.warn("Invalid custom headers JSON");
|
|
236
|
-
}
|
|
249
|
+
} catch { /* ignore invalid */ }
|
|
237
250
|
}
|
|
238
251
|
|
|
239
252
|
const res = await fetch(endpoint, {
|
|
240
253
|
method: "POST",
|
|
241
254
|
headers,
|
|
242
255
|
body: JSON.stringify({
|
|
243
|
-
query:
|
|
244
|
-
variables:
|
|
256
|
+
query: tab.query,
|
|
257
|
+
variables: tab.variables ? JSON.parse(tab.variables) : {},
|
|
245
258
|
}),
|
|
246
259
|
});
|
|
247
260
|
|
|
261
|
+
const endTime = performance.now();
|
|
262
|
+
const duration = Math.round(endTime - startTime);
|
|
263
|
+
setLastDuration(duration);
|
|
264
|
+
setLastStatus(res.status);
|
|
265
|
+
|
|
248
266
|
const data = await res.json();
|
|
249
|
-
|
|
267
|
+
const formatted = JSON.stringify(data, null, 2);
|
|
268
|
+
setResponse(formatted);
|
|
250
269
|
if (data.errors) setError("Query returned errors");
|
|
270
|
+
|
|
271
|
+
setHistory((prev) => [
|
|
272
|
+
{
|
|
273
|
+
id: Date.now().toString(),
|
|
274
|
+
query: tab.query,
|
|
275
|
+
variables: tab.variables,
|
|
276
|
+
response: formatted,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
duration,
|
|
279
|
+
statusCode: res.status,
|
|
280
|
+
},
|
|
281
|
+
...prev.slice(0, 49),
|
|
282
|
+
]);
|
|
251
283
|
} catch (err: unknown) {
|
|
252
284
|
const message = err instanceof Error ? err.message : "Request failed";
|
|
253
|
-
setError(message
|
|
285
|
+
setError(message);
|
|
254
286
|
setResponse(JSON.stringify({ error: message }, null, 2));
|
|
255
287
|
} finally {
|
|
256
288
|
setIsLoading(false);
|
|
257
289
|
}
|
|
290
|
+
}, [endpoint, token, tab]);
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
const handler = (e: KeyboardEvent) => {
|
|
294
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
handleRun();
|
|
297
|
+
}
|
|
298
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "p") {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
handlePrettify();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
window.addEventListener("keydown", handler);
|
|
304
|
+
return () => window.removeEventListener("keydown", handler);
|
|
305
|
+
}, [handleRun]);
|
|
306
|
+
|
|
307
|
+
const updateTab = (key: "query" | "variables" | "headers", value: string) => {
|
|
308
|
+
setTab((prev) => ({ ...prev, [key]: value }));
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const handlePrettify = () => {
|
|
312
|
+
const pretty = prettifyQuery(tab.query);
|
|
313
|
+
setTab((prev) => ({ ...prev, query: pretty }));
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const handleCopyResponse = async () => {
|
|
317
|
+
if (response) {
|
|
318
|
+
await navigator.clipboard.writeText(response);
|
|
319
|
+
setCopied(true);
|
|
320
|
+
setTimeout(() => setCopied(false), 2000);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const handleDownloadResponse = () => {
|
|
325
|
+
if (!response) return;
|
|
326
|
+
const blob = new Blob([response], { type: "application/json" });
|
|
327
|
+
const url = URL.createObjectURL(blob);
|
|
328
|
+
const a = document.createElement("a");
|
|
329
|
+
a.href = url;
|
|
330
|
+
a.download = "graphql-response.json";
|
|
331
|
+
a.click();
|
|
332
|
+
URL.revokeObjectURL(url);
|
|
258
333
|
};
|
|
259
334
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
prev.map((t) => (t.id === currentTabId ? { ...t, [key]: value } : t)),
|
|
263
|
-
);
|
|
335
|
+
const handleClearEditor = () => {
|
|
336
|
+
setTab((prev) => ({ ...prev, query: "" }));
|
|
264
337
|
};
|
|
265
338
|
|
|
266
339
|
const extensions = [javascript()];
|
|
@@ -268,218 +341,395 @@ export function GraphQLPlayground({
|
|
|
268
341
|
|
|
269
342
|
const renderType = (type: Record<string, unknown>): string => {
|
|
270
343
|
if (!type) return "Unknown";
|
|
271
|
-
if (type.name) return type.name;
|
|
344
|
+
if (type.name) return type.name as string;
|
|
272
345
|
if (type.ofType) {
|
|
273
|
-
if (type.kind === "NON_NULL") return `${renderType(type.ofType)}!`;
|
|
274
|
-
if (type.kind === "LIST") return `[${renderType(type.ofType)}]`;
|
|
275
|
-
return renderType(type.ofType);
|
|
346
|
+
if (type.kind === "NON_NULL") return `${renderType(type.ofType as Record<string, unknown>)}!`;
|
|
347
|
+
if (type.kind === "LIST") return `[${renderType(type.ofType as Record<string, unknown>)}]`;
|
|
348
|
+
return renderType(type.ofType as Record<string, unknown>);
|
|
276
349
|
}
|
|
277
350
|
return "Unknown";
|
|
278
351
|
};
|
|
279
352
|
|
|
353
|
+
const startDrag = useCallback((e: React.MouseEvent) => {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
setIsDragging(true);
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (!isDragging) return;
|
|
360
|
+
const onMove = (e: MouseEvent) => {
|
|
361
|
+
if (!containerRef.current) return;
|
|
362
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
363
|
+
const pct = ((e.clientX - rect.left) / rect.width) * 100;
|
|
364
|
+
setSplitPos(Math.max(20, Math.min(80, pct)));
|
|
365
|
+
};
|
|
366
|
+
const onUp = () => setIsDragging(false);
|
|
367
|
+
window.addEventListener("mousemove", onMove);
|
|
368
|
+
window.addEventListener("mouseup", onUp);
|
|
369
|
+
return () => {
|
|
370
|
+
window.removeEventListener("mousemove", onMove);
|
|
371
|
+
window.removeEventListener("mouseup", onUp);
|
|
372
|
+
};
|
|
373
|
+
}, [isDragging]);
|
|
374
|
+
|
|
375
|
+
const editorPills = [
|
|
376
|
+
{ key: "query" as const, label: "Query", shortcut: "" },
|
|
377
|
+
{ key: "variables" as const, label: "Variables", shortcut: "" },
|
|
378
|
+
{ key: "headers" as const, label: "Headers", shortcut: "" },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
const rightPills = [
|
|
382
|
+
{ key: "response" as const, label: "Response", badge: lastStatus ? `${lastStatus}ms` : undefined },
|
|
383
|
+
{ key: "docs" as const, label: "Docs" },
|
|
384
|
+
{ key: "history" as const, label: `History${history.length ? ` (${history.length})` : ""}` },
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
const editorValue =
|
|
388
|
+
activeEditorTab === "query"
|
|
389
|
+
? tab.query
|
|
390
|
+
: activeEditorTab === "variables"
|
|
391
|
+
? tab.variables
|
|
392
|
+
: tab.headers;
|
|
393
|
+
|
|
280
394
|
return (
|
|
281
|
-
<div className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
|
|
282
|
-
{/*
|
|
283
|
-
<div className="flex items-center
|
|
284
|
-
<div className="flex items-center gap-
|
|
285
|
-
<div className="flex items-center
|
|
286
|
-
<
|
|
287
|
-
<Zap className="w-4 h-4" />
|
|
288
|
-
</div>
|
|
289
|
-
<span className="font-bold text-sm tracking-tight">GraphQL Playground</span>
|
|
395
|
+
<div ref={containerRef} className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
|
|
396
|
+
{/* Compact top bar */}
|
|
397
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
398
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
399
|
+
<div className="w-7 h-7 rounded-lg bg-[var(--kyro-primary)]/10 flex items-center justify-center text-[var(--kyro-primary)] shrink-0">
|
|
400
|
+
<Zap className="w-3.5 h-3.5" />
|
|
290
401
|
</div>
|
|
291
|
-
<
|
|
402
|
+
<span className="text-xs font-semibold hidden sm:inline">GraphQL</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div className="flex items-center gap-1.5 text-[10px] text-[var(--kyro-text-muted)]">
|
|
405
|
+
<div className={`w-1.5 h-1.5 rounded-full ${isConnected ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-danger)]"}`} />
|
|
406
|
+
<span className="hidden sm:inline">{isConnected ? "Connected" : "Disconnected"}</span>
|
|
407
|
+
</div>
|
|
408
|
+
<div className="h-4 w-px bg-[var(--kyro-border)]" />
|
|
409
|
+
{token ? (
|
|
410
|
+
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] text-[10px] font-mono text-[var(--kyro-text-muted)]">
|
|
411
|
+
<span className="w-2 h-2 rounded-full bg-[var(--kyro-success)]" />
|
|
412
|
+
Bearer ••••{token.slice(-4)}
|
|
413
|
+
<button onClick={() => { setToken(""); setShowToken(false); }} className="ml-1 hover:text-[var(--kyro-danger)]">
|
|
414
|
+
<X className="w-3 h-3" />
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
) : (
|
|
292
418
|
<div className="flex items-center gap-1">
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
|
|
419
|
+
<input
|
|
420
|
+
type={showToken ? "text" : "password"}
|
|
421
|
+
value={token}
|
|
422
|
+
onChange={(e) => setToken(e.target.value)}
|
|
423
|
+
placeholder="Bearer token"
|
|
424
|
+
className="w-24 px-2 py-1 text-[10px] font-mono bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
425
|
+
/>
|
|
426
|
+
<button onClick={() => setShowToken(!showToken)} className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
427
|
+
{showToken ? <X className="w-3 h-3" /> : <Info className="w-3 h-3" />}
|
|
428
|
+
</button>
|
|
297
429
|
</div>
|
|
298
|
-
|
|
299
|
-
<div className="flex items-center gap-
|
|
300
|
-
<button
|
|
301
|
-
onClick={() => setShowDocs(!showDocs)}
|
|
302
|
-
className={`kyro-btn kyro-btn-md flex items-center gap-2 ${showDocs
|
|
303
|
-
? "bg-pink-500 text-white border-pink-500 hover:bg-pink-600 hover:border-pink-600 shadow-[0_0_15px_rgba(236,72,153,0.3)]"
|
|
304
|
-
: "kyro-btn-ghost"
|
|
305
|
-
}`}
|
|
306
|
-
>
|
|
430
|
+
)}
|
|
431
|
+
<div className="ml-auto flex items-center gap-1 flex-wrap justify-end">
|
|
432
|
+
<button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="Schema docs">
|
|
307
433
|
<Book className="w-3.5 h-3.5" />
|
|
308
|
-
Documentation
|
|
309
434
|
</button>
|
|
435
|
+
<button onClick={() => setRightTab("history")} className={`p-1.5 rounded-lg transition-all ${rightTab === "history" ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="History">
|
|
436
|
+
<Clock className="w-3.5 h-3.5" />
|
|
437
|
+
</button>
|
|
438
|
+
<button onClick={handlePrettify} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Prettify (Cmd+Shift+P)">
|
|
439
|
+
<Code2 className="w-3.5 h-3.5" />
|
|
440
|
+
</button>
|
|
441
|
+
<button onClick={handleCopyResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
|
|
442
|
+
{copied ? <Check className="w-3.5 h-3.5 text-[var(--kyro-success)]" /> : <Copy className="w-3.5 h-3.5" />}
|
|
443
|
+
</button>
|
|
444
|
+
<button onClick={handleDownloadResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
|
|
445
|
+
<Download className="w-3.5 h-3.5" />
|
|
446
|
+
</button>
|
|
447
|
+
<button onClick={handleClearEditor} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)]" title="Clear editor">
|
|
448
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
449
|
+
</button>
|
|
450
|
+
<div className="h-4 w-px bg-[var(--kyro-border)] mx-1" />
|
|
310
451
|
<button
|
|
311
452
|
onClick={handleRun}
|
|
312
453
|
disabled={isLoading}
|
|
313
|
-
className="
|
|
454
|
+
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-[var(--kyro-primary)] text-white text-xs font-semibold hover:opacity-90 disabled:opacity-50 transition-all shadow-sm"
|
|
314
455
|
>
|
|
315
|
-
<
|
|
316
|
-
{isLoading ? "
|
|
456
|
+
<Play className="w-3 h-3" />
|
|
457
|
+
{isLoading ? "..." : "Run"}
|
|
317
458
|
</button>
|
|
318
459
|
</div>
|
|
319
460
|
</div>
|
|
320
461
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<
|
|
326
|
-
{(
|
|
462
|
+
{/* Main split area */}
|
|
463
|
+
<div className="flex-1 flex flex-col md:flex-row overflow-hidden relative">
|
|
464
|
+
{/* Mobile panel switcher */}
|
|
465
|
+
<div className="flex md:hidden gap-1 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)] shrink-0">
|
|
466
|
+
<button
|
|
467
|
+
onClick={() => setMobilePanel("editor")}
|
|
468
|
+
className={`flex-1 px-3 py-1.5 text-[10px] font-semibold rounded-md transition-all ${mobilePanel === "editor" ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
469
|
+
>
|
|
470
|
+
Editor
|
|
471
|
+
</button>
|
|
472
|
+
<button
|
|
473
|
+
onClick={() => setMobilePanel("response")}
|
|
474
|
+
className={`flex-1 px-3 py-1.5 text-[10px] font-semibold rounded-md transition-all ${mobilePanel === "response" ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`}
|
|
475
|
+
>
|
|
476
|
+
Output
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{/* Left: editor */}
|
|
481
|
+
<div className={`${mobilePanel === "editor" ? "flex" : "hidden"} md:flex flex-col overflow-hidden border-r border-[var(--kyro-border)] w-full md:w-auto`} style={{ flex: 'none', width: typeof window !== "undefined" && window.innerWidth >= 768 ? `${splitPos}%` : "100%" }}>
|
|
482
|
+
{/* Editor pills */}
|
|
483
|
+
<div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
484
|
+
{editorPills.map((p) => (
|
|
327
485
|
<button
|
|
328
|
-
key={
|
|
329
|
-
onClick={() =>
|
|
330
|
-
className={`px-
|
|
331
|
-
|
|
332
|
-
|
|
486
|
+
key={p.key}
|
|
487
|
+
onClick={() => setActiveEditorTab(p.key)}
|
|
488
|
+
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${activeEditorTab === p.key
|
|
489
|
+
? "bg-[var(--kyro-primary)] text-white"
|
|
490
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
333
491
|
}`}
|
|
334
492
|
>
|
|
335
|
-
{
|
|
493
|
+
{p.label}
|
|
336
494
|
</button>
|
|
337
495
|
))}
|
|
338
496
|
</div>
|
|
339
497
|
|
|
498
|
+
{/* CodeMirror */}
|
|
340
499
|
<div className="flex-1 overflow-hidden relative bg-[var(--kyro-bg)]">
|
|
341
|
-
<Suspense fallback={<div className="p-
|
|
500
|
+
<Suspense fallback={<div className="p-3 text-[10px] text-[var(--kyro-text-muted)]">Loading editor...</div>}>
|
|
342
501
|
{isMounted && (
|
|
343
502
|
<CodeMirrorEditor
|
|
344
|
-
value={
|
|
503
|
+
value={editorValue}
|
|
345
504
|
height="100%"
|
|
346
505
|
extensions={extensions}
|
|
347
506
|
theme={theme}
|
|
348
|
-
onChange={(val) => updateTab(
|
|
349
|
-
basicSetup={true}
|
|
507
|
+
onChange={(val) => updateTab(activeEditorTab, val)}
|
|
508
|
+
basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: true }}
|
|
350
509
|
style={{
|
|
351
510
|
height: "100%",
|
|
352
|
-
fontSize: "
|
|
511
|
+
fontSize: "12px",
|
|
353
512
|
fontFamily: "'Fira Code', monospace",
|
|
354
513
|
}}
|
|
355
514
|
/>
|
|
356
515
|
)}
|
|
357
516
|
</Suspense>
|
|
358
517
|
</div>
|
|
518
|
+
|
|
519
|
+
{/* Status bar */}
|
|
520
|
+
<div className="flex items-center justify-between px-3 py-1 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[9px] text-[var(--kyro-text-muted)] font-mono">
|
|
521
|
+
<span>Ln {cursorPos.current.line}, Col {cursorPos.current.col}</span>
|
|
522
|
+
<span>{tab.query.length} chars</span>
|
|
523
|
+
</div>
|
|
359
524
|
</div>
|
|
360
525
|
|
|
361
|
-
{/*
|
|
362
|
-
<div
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
526
|
+
{/* Drag handle */}
|
|
527
|
+
<div
|
|
528
|
+
className="hidden md:block absolute top-0 bottom-0 z-10 w-1.5 cursor-col-resize group"
|
|
529
|
+
style={{ left: `calc(${splitPos}% - 3px)` }}
|
|
530
|
+
onMouseDown={startDrag}
|
|
531
|
+
>
|
|
532
|
+
<div className="w-0.5 h-full mx-auto bg-transparent group-hover:bg-[var(--kyro-primary)] group-hover:opacity-40 transition-all" />
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
{/* Right panel */}
|
|
536
|
+
<div className={`${mobilePanel === "response" ? "flex" : "hidden"} md:flex flex-1 flex-col overflow-hidden min-w-0 w-full md:w-auto`} style={{ flex: undefined, width: typeof window !== "undefined" && window.innerWidth >= 768 ? `${100 - splitPos}%` : "100%" }}>
|
|
537
|
+
{/* Right pills */}
|
|
538
|
+
<div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
539
|
+
{rightPills.map((p) => (
|
|
540
|
+
<button
|
|
541
|
+
key={p.key}
|
|
542
|
+
onClick={() => { setRightTab(p.key); if (p.key === "docs") setShowDocs(true); }}
|
|
543
|
+
className={`px-2.5 py-1 text-[10px] font-semibold rounded-md transition-all ${rightTab === p.key
|
|
544
|
+
? "bg-[var(--kyro-primary)] text-white"
|
|
545
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
546
|
+
}`}
|
|
547
|
+
>
|
|
548
|
+
{p.label}
|
|
549
|
+
</button>
|
|
550
|
+
))}
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<div className="flex-1 overflow-hidden">
|
|
554
|
+
{rightTab === "docs" ? (
|
|
555
|
+
<div className="h-full flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
|
|
556
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--kyro-border)]">
|
|
557
|
+
<span className="text-[10px] font-semibold text-[var(--kyro-text-secondary)]">Schema Explorer</span>
|
|
558
|
+
<button onClick={() => setShowDocs(false)} className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
559
|
+
<X className="w-3.5 h-3.5" />
|
|
560
|
+
</button>
|
|
561
|
+
</div>
|
|
562
|
+
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
|
563
|
+
{loadingSchema ? (
|
|
564
|
+
<div className="flex items-center justify-center h-32">
|
|
565
|
+
<RefreshCw className="w-5 h-5 animate-spin text-[var(--kyro-primary)]" />
|
|
566
|
+
</div>
|
|
567
|
+
) : schema ? (
|
|
568
|
+
<div className="space-y-4">
|
|
569
|
+
{selectedType ? (
|
|
570
|
+
<div className="space-y-3">
|
|
571
|
+
<button
|
|
572
|
+
onClick={() => setSelectedType(null)}
|
|
573
|
+
className="flex items-center gap-1 text-[10px] text-[var(--kyro-primary)] font-semibold hover:underline"
|
|
574
|
+
>
|
|
575
|
+
← Back to types
|
|
576
|
+
</button>
|
|
577
|
+
<div>
|
|
578
|
+
<h3 className="text-sm font-bold text-[var(--kyro-text-primary)]">{selectedType.name}</h3>
|
|
579
|
+
<p className="text-[10px] text-[var(--kyro-text-muted)] italic">{selectedType.kind}</p>
|
|
580
|
+
{selectedType.description && (
|
|
581
|
+
<p className="mt-1.5 text-[11px] text-[var(--kyro-text-secondary)] leading-relaxed">{selectedType.description}</p>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
{selectedType.fields && (
|
|
585
|
+
<div className="space-y-2">
|
|
586
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Fields</h4>
|
|
587
|
+
{selectedType.fields.map(f => (
|
|
588
|
+
<div key={f.name} className="p-2.5 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)]">
|
|
589
|
+
<div className="flex items-center justify-between gap-2">
|
|
590
|
+
<span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
|
|
591
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
|
|
592
|
+
</div>
|
|
593
|
+
{f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
|
|
594
|
+
{f.args && f.args.length > 0 && (
|
|
595
|
+
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
|
|
596
|
+
{f.args.map(a => (
|
|
597
|
+
<div key={a.name} className="text-[9px]">
|
|
598
|
+
<span className="text-[var(--kyro-text-muted)]">{a.name}:</span>{" "}
|
|
599
|
+
<span className="text-[var(--kyro-primary)]">{renderType(a.type)}</span>
|
|
600
|
+
</div>
|
|
601
|
+
))}
|
|
602
|
+
</div>
|
|
603
|
+
)}
|
|
604
|
+
</div>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
391
607
|
)}
|
|
392
608
|
</div>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
</div>
|
|
409
|
-
))}
|
|
609
|
+
) : (
|
|
610
|
+
<div className="space-y-3">
|
|
611
|
+
<div className="grid grid-cols-2 gap-2">
|
|
612
|
+
{["Query", "Mutation"].map(t => {
|
|
613
|
+
const found = schema.types.find(type => type.name === t);
|
|
614
|
+
if (!found) return null;
|
|
615
|
+
return (
|
|
616
|
+
<button
|
|
617
|
+
key={t}
|
|
618
|
+
onClick={() => setSelectedType(found)}
|
|
619
|
+
className="flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-all text-left group"
|
|
620
|
+
>
|
|
621
|
+
<div>
|
|
622
|
+
<span className="text-[10px] font-semibold text-[var(--kyro-text-muted)] block">{t}</span>
|
|
623
|
+
<span className="text-[11px] font-bold text-[var(--kyro-text-primary)]">Root Operations</span>
|
|
410
624
|
</div>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
625
|
+
<ChevronRight className="w-3.5 h-3.5 text-[var(--kyro-primary)] group-hover:translate-x-1 transition-transform" />
|
|
626
|
+
</button>
|
|
627
|
+
);
|
|
628
|
+
})}
|
|
414
629
|
</div>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
<div className="space-y-4">
|
|
419
|
-
<div className="grid grid-cols-2 gap-2">
|
|
420
|
-
{["Query", "Mutation"].map(t => {
|
|
421
|
-
const found = schema.types.find(type => type.name === t);
|
|
422
|
-
if (!found) return null;
|
|
423
|
-
return (
|
|
630
|
+
<div className="space-y-1.5">
|
|
631
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">All Types</h4>
|
|
632
|
+
{schema.types.filter(t => !t.name.startsWith("__") && t.kind === "OBJECT").map(t => (
|
|
424
633
|
<button
|
|
425
|
-
key={t}
|
|
426
|
-
onClick={() => setSelectedType(
|
|
427
|
-
className="flex items-center justify-between
|
|
634
|
+
key={t.name}
|
|
635
|
+
onClick={() => setSelectedType(t)}
|
|
636
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left"
|
|
428
637
|
>
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
<span className="text-sm font-bold text-[var(--kyro-text-primary)]">Root Operations</span>
|
|
432
|
-
</div>
|
|
433
|
-
<ChevronRight className="w-4 h-4 text-pink-500 group-hover:translate-x-1 transition-transform" />
|
|
638
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{t.name}</span>
|
|
639
|
+
<ChevronRight className="w-3 h-3 opacity-30" />
|
|
434
640
|
</button>
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
</div>
|
|
438
|
-
<div className="space-y-2">
|
|
439
|
-
<h4 className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] pt-4">All Types</h4>
|
|
440
|
-
{schema.types.filter(t => !t.name.startsWith("__") && t.kind === "OBJECT").map(t => (
|
|
441
|
-
<button
|
|
442
|
-
key={t.name}
|
|
443
|
-
onClick={() => setSelectedType(t)}
|
|
444
|
-
className="w-full flex items-center justify-between px-4 py-2 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left"
|
|
445
|
-
>
|
|
446
|
-
<span className="text-sm font-medium text-[var(--kyro-text-primary)]">{t.name}</span>
|
|
447
|
-
<ChevronRight className="w-3.5 h-3.5 opacity-30" />
|
|
448
|
-
</button>
|
|
449
|
-
))}
|
|
641
|
+
))}
|
|
642
|
+
</div>
|
|
450
643
|
</div>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
646
|
+
) : (
|
|
647
|
+
<p className="text-[10px] text-[var(--kyro-text-muted)]">No schema loaded.</p>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
457
650
|
</div>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
651
|
+
) : rightTab === "history" ? (
|
|
652
|
+
<div className="h-full flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
|
|
653
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-[var(--kyro-border)]">
|
|
654
|
+
<span className="text-[10px] font-semibold text-[var(--kyro-text-secondary)]">Query History</span>
|
|
655
|
+
{history.length > 0 && (
|
|
656
|
+
<button onClick={() => setHistory([])} className="text-[9px] text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)]">Clear</button>
|
|
657
|
+
)}
|
|
658
|
+
</div>
|
|
659
|
+
<div className="flex-1 overflow-y-auto">
|
|
660
|
+
{history.length === 0 ? (
|
|
661
|
+
<div className="flex flex-col items-center justify-center h-32 text-[var(--kyro-text-muted)]">
|
|
662
|
+
<Terminal className="w-8 h-8 mb-2 opacity-30" />
|
|
663
|
+
<p className="text-[10px]">No queries executed yet</p>
|
|
664
|
+
</div>
|
|
665
|
+
) : (
|
|
666
|
+
<div className="p-2 space-y-1">
|
|
667
|
+
{history.map((entry) => (
|
|
668
|
+
<button
|
|
669
|
+
key={entry.id}
|
|
670
|
+
onClick={() => {
|
|
671
|
+
setTab((prev) => ({ ...prev, query: entry.query, variables: entry.variables }));
|
|
672
|
+
setResponse(entry.response);
|
|
673
|
+
}}
|
|
674
|
+
className="w-full text-left p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] border border-transparent hover:border-[var(--kyro-border)] transition-all"
|
|
675
|
+
>
|
|
676
|
+
<div className="text-[10px] font-mono text-[var(--kyro-text-primary)] truncate leading-relaxed">
|
|
677
|
+
{entry.query.split("\n").slice(0, 3).join(" ").substring(0, 80)}
|
|
678
|
+
</div>
|
|
679
|
+
<div className="flex items-center gap-2 mt-1 text-[9px] text-[var(--kyro-text-muted)]">
|
|
680
|
+
<span>{new Date(entry.timestamp).toLocaleTimeString()}</span>
|
|
681
|
+
<span>{entry.duration}ms</span>
|
|
682
|
+
<span className={`${entry.statusCode < 400 ? "text-[var(--kyro-success)]" : "text-[var(--kyro-danger)]"}`}>
|
|
683
|
+
{entry.statusCode}
|
|
684
|
+
</span>
|
|
685
|
+
</div>
|
|
686
|
+
</button>
|
|
687
|
+
))}
|
|
688
|
+
</div>
|
|
689
|
+
)}
|
|
690
|
+
</div>
|
|
463
691
|
</div>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
692
|
+
) : (
|
|
693
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
694
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
695
|
+
<span className="text-[10px] font-semibold text-[var(--kyro-text-secondary)]">Response</span>
|
|
696
|
+
{lastDuration > 0 && (
|
|
697
|
+
<span className="text-[9px] font-mono text-[var(--kyro-text-muted)]">
|
|
698
|
+
{lastDuration}ms
|
|
699
|
+
</span>
|
|
700
|
+
)}
|
|
701
|
+
{lastStatus > 0 && (
|
|
702
|
+
<span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${lastStatus < 400 ? "bg-[var(--kyro-success-bg)] text-[var(--kyro-success)]" : "bg-[var(--kyro-danger-bg)] text-[var(--kyro-danger)]"
|
|
703
|
+
}`}>
|
|
704
|
+
{lastStatus}
|
|
705
|
+
</span>
|
|
706
|
+
)}
|
|
707
|
+
</div>
|
|
708
|
+
<div className="flex-1 overflow-auto bg-[var(--kyro-bg-secondary)]">
|
|
709
|
+
{isLoading ? (
|
|
710
|
+
<div className="flex flex-col items-center justify-center h-full gap-3">
|
|
711
|
+
<RefreshCw className="w-6 h-6 animate-spin text-[var(--kyro-primary)]" />
|
|
712
|
+
<span className="text-[10px] font-semibold text-[var(--kyro-text-muted)]">Running Query...</span>
|
|
713
|
+
</div>
|
|
714
|
+
) : response ? (
|
|
715
|
+
<pre className="text-[11px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap p-3 selection:bg-[var(--kyro-primary)]/20">
|
|
716
|
+
{error && (
|
|
717
|
+
<div className="mb-2 p-2 rounded bg-[var(--kyro-danger-bg)] border border-[var(--kyro-danger)]/20 text-[10px] text-[var(--kyro-danger)] font-medium">
|
|
718
|
+
⚠ {error}
|
|
719
|
+
</div>
|
|
720
|
+
)}
|
|
721
|
+
{response}
|
|
722
|
+
</pre>
|
|
723
|
+
) : (
|
|
724
|
+
<div className="flex flex-col items-center justify-center h-full opacity-30">
|
|
725
|
+
<Activity className="w-12 h-12 mb-3" />
|
|
726
|
+
<p className="text-[11px] font-bold">Press Run to execute</p>
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
480
730
|
</div>
|
|
481
|
-
|
|
482
|
-
|
|
731
|
+
)}
|
|
732
|
+
</div>
|
|
483
733
|
</div>
|
|
484
734
|
</div>
|
|
485
735
|
</div>
|