@kyro-cms/admin 0.3.2 → 0.3.4
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/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -6,11 +6,29 @@ import React, {
|
|
|
6
6
|
Suspense,
|
|
7
7
|
lazy,
|
|
8
8
|
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
Book,
|
|
11
|
+
Send,
|
|
12
|
+
Trash2,
|
|
13
|
+
Copy,
|
|
14
|
+
RefreshCw,
|
|
15
|
+
Settings,
|
|
16
|
+
Maximize2,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
Search,
|
|
20
|
+
Type,
|
|
21
|
+
Activity,
|
|
22
|
+
Zap,
|
|
23
|
+
Info,
|
|
24
|
+
X
|
|
25
|
+
} from "./ui/icons";
|
|
9
26
|
|
|
10
27
|
interface GraphQLPlaygroundProps {
|
|
11
28
|
endpoint?: string;
|
|
12
29
|
initialQuery?: string;
|
|
13
30
|
initialVariables?: string;
|
|
31
|
+
initialShowDocs?: boolean;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
interface QueryTab {
|
|
@@ -35,13 +53,13 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
|
35
53
|
|
|
36
54
|
# 1. Introspection - Discover the schema
|
|
37
55
|
{
|
|
38
|
-
|
|
56
|
+
__schema {
|
|
39
57
|
types {
|
|
40
58
|
name
|
|
59
|
+
kind
|
|
41
60
|
fields {
|
|
42
61
|
name
|
|
43
|
-
type
|
|
44
|
-
required
|
|
62
|
+
type { name kind }
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
}
|
|
@@ -52,7 +70,6 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
|
52
70
|
collections {
|
|
53
71
|
slug
|
|
54
72
|
name
|
|
55
|
-
fields
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -60,27 +77,47 @@ const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
|
|
|
60
77
|
{
|
|
61
78
|
ping
|
|
62
79
|
}
|
|
80
|
+
`;
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
// --- Docs Types ---
|
|
83
|
+
interface TypeInfo {
|
|
84
|
+
name: string;
|
|
85
|
+
kind: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
fields?: FieldInfo[];
|
|
88
|
+
inputFields?: FieldInfo[];
|
|
89
|
+
enumValues?: { name: string; description?: string; isDeprecated: boolean }[];
|
|
90
|
+
isDeprecated?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface FieldInfo {
|
|
94
|
+
name: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
type: { name?: string; kind?: string; ofType?: Record<string, unknown> };
|
|
97
|
+
args: ArgInfo[];
|
|
98
|
+
isDeprecated?: boolean;
|
|
99
|
+
deprecationReason?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ArgInfo {
|
|
103
|
+
name: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
type: { name?: string; kind?: string; ofType?: Record<string, unknown> };
|
|
106
|
+
defaultValue?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SchemaInfo {
|
|
110
|
+
queryType: { name: string };
|
|
111
|
+
mutationType?: { name: string };
|
|
112
|
+
subscriptionType?: { name: string };
|
|
113
|
+
types: TypeInfo[];
|
|
77
114
|
}
|
|
78
|
-
`;
|
|
79
115
|
|
|
80
116
|
export function GraphQLPlayground({
|
|
81
117
|
endpoint = "/api/graphql",
|
|
82
118
|
initialQuery,
|
|
83
119
|
initialVariables,
|
|
120
|
+
initialShowDocs = false,
|
|
84
121
|
}: GraphQLPlaygroundProps) {
|
|
85
122
|
const [token, setToken] = useState<string>("");
|
|
86
123
|
const [isConnected, setIsConnected] = useState(false);
|
|
@@ -100,91 +137,102 @@ export function GraphQLPlayground({
|
|
|
100
137
|
const [activeTab, setActiveTab] = useState<"query" | "variables" | "headers">(
|
|
101
138
|
"query",
|
|
102
139
|
);
|
|
103
|
-
const [isDark, setIsDark] = useState(false);
|
|
104
140
|
const [isMounted, setIsMounted] = useState(false);
|
|
141
|
+
const [showDocs, setShowDocs] = useState(initialShowDocs);
|
|
142
|
+
const [schema, setSchema] = useState<SchemaInfo | null>(null);
|
|
143
|
+
const [loadingSchema, setLoadingSchema] = useState(false);
|
|
144
|
+
const [selectedType, setSelectedType] = useState<TypeInfo | null>(null);
|
|
105
145
|
|
|
106
146
|
useEffect(() => {
|
|
107
147
|
setIsMounted(true);
|
|
108
|
-
setIsDark(document.documentElement.classList.contains("dark"));
|
|
109
|
-
|
|
110
|
-
const storedToken = localStorage.getItem("graphql_auth_token");
|
|
111
|
-
if (storedToken) {
|
|
112
|
-
setToken(storedToken);
|
|
113
|
-
setIsConnected(true);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const observer = new MutationObserver(() => {
|
|
117
|
-
setIsDark(document.documentElement.classList.contains("dark"));
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
observer.observe(document.documentElement, {
|
|
121
|
-
attributes: true,
|
|
122
|
-
attributeFilter: ["class"],
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return () => observer.disconnect();
|
|
126
148
|
}, []);
|
|
127
149
|
|
|
128
|
-
const currentTab =
|
|
150
|
+
const currentTab = useMemo(
|
|
151
|
+
() => tabs.find((t) => t.id === currentTabId) || tabs[0],
|
|
152
|
+
[tabs, currentTabId],
|
|
153
|
+
);
|
|
129
154
|
|
|
130
|
-
const
|
|
131
|
-
(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
const fetchSchema = useCallback(async () => {
|
|
156
|
+
setLoadingSchema(true);
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(endpoint, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
query: `
|
|
163
|
+
{
|
|
164
|
+
__schema {
|
|
165
|
+
queryType { name }
|
|
166
|
+
mutationType { name }
|
|
167
|
+
subscriptionType { name }
|
|
168
|
+
types {
|
|
169
|
+
name
|
|
170
|
+
kind
|
|
171
|
+
description
|
|
172
|
+
fields {
|
|
173
|
+
name
|
|
174
|
+
description
|
|
175
|
+
type { name kind ofType { name kind ofType { name kind } } }
|
|
176
|
+
args {
|
|
177
|
+
name
|
|
178
|
+
description
|
|
179
|
+
type { name kind ofType { name kind } }
|
|
180
|
+
defaultValue
|
|
181
|
+
}
|
|
182
|
+
isDeprecated
|
|
183
|
+
deprecationReason
|
|
184
|
+
}
|
|
185
|
+
inputFields {
|
|
186
|
+
name
|
|
187
|
+
description
|
|
188
|
+
type { name kind ofType { name kind } }
|
|
189
|
+
defaultValue
|
|
190
|
+
}
|
|
191
|
+
enumValues {
|
|
192
|
+
name
|
|
193
|
+
description
|
|
194
|
+
isDeprecated
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
`,
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
if (data.data && data.data.__schema) {
|
|
204
|
+
setSchema(data.data.__schema);
|
|
135
205
|
setIsConnected(true);
|
|
136
|
-
setTabs((prev) =>
|
|
137
|
-
prev.map((tab) => ({
|
|
138
|
-
...tab,
|
|
139
|
-
headers: JSON.stringify(
|
|
140
|
-
{ Authorization: `Bearer ${token.trim()}` },
|
|
141
|
-
null,
|
|
142
|
-
2,
|
|
143
|
-
),
|
|
144
|
-
})),
|
|
145
|
-
);
|
|
146
206
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
setToken("");
|
|
154
|
-
setIsConnected(false);
|
|
155
|
-
}, []);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("Failed to fetch schema", err);
|
|
209
|
+
} finally {
|
|
210
|
+
setLoadingSchema(false);
|
|
211
|
+
}
|
|
212
|
+
}, [endpoint]);
|
|
156
213
|
|
|
157
|
-
|
|
158
|
-
if (!
|
|
159
|
-
|
|
160
|
-
return;
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (showDocs && !schema) {
|
|
216
|
+
fetchSchema();
|
|
161
217
|
}
|
|
218
|
+
}, [showDocs, schema, fetchSchema]);
|
|
162
219
|
|
|
220
|
+
const handleRun = async () => {
|
|
163
221
|
setIsLoading(true);
|
|
164
222
|
setError(null);
|
|
165
|
-
|
|
166
223
|
try {
|
|
167
|
-
|
|
168
|
-
if (currentTab.variables.trim()) {
|
|
169
|
-
try {
|
|
170
|
-
variables = JSON.parse(currentTab.variables);
|
|
171
|
-
} catch {
|
|
172
|
-
setError("Invalid JSON in variables");
|
|
173
|
-
setIsLoading(false);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let headers: Record<string, string> = {
|
|
224
|
+
const headers: Record<string, string> = {
|
|
179
225
|
"Content-Type": "application/json",
|
|
180
226
|
};
|
|
181
|
-
if (
|
|
227
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
228
|
+
|
|
229
|
+
// Add custom headers from tab
|
|
230
|
+
if (currentTab.headers) {
|
|
182
231
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return;
|
|
232
|
+
const customHeaders = JSON.parse(currentTab.headers);
|
|
233
|
+
Object.assign(headers, customHeaders);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.warn("Invalid custom headers JSON");
|
|
188
236
|
}
|
|
189
237
|
}
|
|
190
238
|
|
|
@@ -193,433 +241,245 @@ export function GraphQLPlayground({
|
|
|
193
241
|
headers,
|
|
194
242
|
body: JSON.stringify({
|
|
195
243
|
query: currentTab.query,
|
|
196
|
-
variables,
|
|
244
|
+
variables: currentTab.variables ? JSON.parse(currentTab.variables) : {},
|
|
197
245
|
}),
|
|
198
246
|
});
|
|
199
247
|
|
|
200
248
|
const data = await res.json();
|
|
201
249
|
setResponse(JSON.stringify(data, null, 2));
|
|
202
|
-
|
|
203
|
-
|
|
250
|
+
if (data.errors) setError("Query returned errors");
|
|
251
|
+
} catch (err: unknown) {
|
|
252
|
+
const message = err instanceof Error ? err.message : "Request failed";
|
|
253
|
+
setError(message || "Request failed");
|
|
254
|
+
setResponse(JSON.stringify({ error: message }, null, 2));
|
|
204
255
|
} finally {
|
|
205
256
|
setIsLoading(false);
|
|
206
257
|
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
id:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const newTabs = tabs.filter((t) => t.id !== tabId);
|
|
229
|
-
setTabs(newTabs);
|
|
230
|
-
if (currentTabId === tabId) {
|
|
231
|
-
setCurrentTabId(newTabs[0].id);
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
[tabs, currentTabId],
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const updateTab = useCallback(
|
|
238
|
-
(field: keyof QueryTab, value: string) => {
|
|
239
|
-
setTabs((prev) =>
|
|
240
|
-
prev.map((tab) =>
|
|
241
|
-
tab.id === currentTabId ? { ...tab, [field]: value } : tab,
|
|
242
|
-
),
|
|
243
|
-
);
|
|
244
|
-
},
|
|
245
|
-
[currentTabId],
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// Keyboard shortcut: Ctrl+Enter to run
|
|
249
|
-
useEffect(() => {
|
|
250
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
251
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
252
|
-
e.preventDefault();
|
|
253
|
-
executeQuery();
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
258
|
-
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
259
|
-
}, [executeQuery]);
|
|
260
|
-
|
|
261
|
-
const theme = isDark ? aura : githubLight;
|
|
262
|
-
|
|
263
|
-
const extensions = useMemo(() => [javascript({ jsx: true })], []);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const updateTab = (key: keyof QueryTab, value: string) => {
|
|
261
|
+
setTabs((prev) =>
|
|
262
|
+
prev.map((t) => (t.id === currentTabId ? { ...t, [key]: value } : t)),
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const extensions = [javascript()];
|
|
267
|
+
const theme = aura;
|
|
268
|
+
|
|
269
|
+
const renderType = (type: Record<string, unknown>): string => {
|
|
270
|
+
if (!type) return "Unknown";
|
|
271
|
+
if (type.name) return type.name;
|
|
272
|
+
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);
|
|
276
|
+
}
|
|
277
|
+
return "Unknown";
|
|
278
|
+
};
|
|
264
279
|
|
|
265
280
|
return (
|
|
266
|
-
<div className="h-full flex flex-col bg-[var(--kyro-
|
|
281
|
+
<div className="h-full flex flex-col bg-[var(--kyro-bg)] overflow-hidden rounded-lg border border-[var(--kyro-border)]">
|
|
267
282
|
{/* Header */}
|
|
268
|
-
<div className="flex items-center justify-between px-4 py-3
|
|
283
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
269
284
|
<div className="flex items-center gap-4">
|
|
270
285
|
<div className="flex items-center gap-2">
|
|
271
|
-
<
|
|
272
|
-
className="w-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
stroke="currentColor"
|
|
276
|
-
>
|
|
277
|
-
<path
|
|
278
|
-
strokeLinecap="round"
|
|
279
|
-
strokeLinejoin="round"
|
|
280
|
-
strokeWidth="2"
|
|
281
|
-
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
282
|
-
/>
|
|
283
|
-
</svg>
|
|
284
|
-
<span className="font-bold text-sm text-[var(--kyro-text-primary)]">
|
|
285
|
-
GraphQL
|
|
286
|
-
</span>
|
|
286
|
+
<div className="w-8 h-8 rounded-lg bg-pink-500/10 flex items-center justify-center text-pink-500">
|
|
287
|
+
<Zap className="w-4 h-4" />
|
|
288
|
+
</div>
|
|
289
|
+
<span className="font-bold text-sm tracking-tight">GraphQL Playground</span>
|
|
287
290
|
</div>
|
|
288
|
-
<div className="
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
/>
|
|
294
|
-
<span className="text-xs text-[var(--kyro-text-secondary)]">
|
|
295
|
-
{isConnected ? "Connected" : "Not connected"}
|
|
291
|
+
<div className="h-4 w-px bg-[var(--kyro-border)]" />
|
|
292
|
+
<div className="flex items-center gap-1">
|
|
293
|
+
<div className={`w-2 h-2 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"} animate-pulse`} />
|
|
294
|
+
<span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">
|
|
295
|
+
{isConnected ? "Connected" : "Disconnected"}
|
|
296
296
|
</span>
|
|
297
297
|
</div>
|
|
298
298
|
</div>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<button type="button"
|
|
307
|
-
onClick={clearToken}
|
|
308
|
-
className="text-xs text-pink-500 hover:text-pink-600 font-medium"
|
|
299
|
+
<div className="flex items-center gap-2">
|
|
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
|
+
}`}
|
|
309
306
|
>
|
|
310
|
-
|
|
307
|
+
<Book className="w-3.5 h-3.5" />
|
|
308
|
+
Documentation
|
|
309
|
+
</button>
|
|
310
|
+
<button
|
|
311
|
+
onClick={handleRun}
|
|
312
|
+
disabled={isLoading}
|
|
313
|
+
className="kyro-btn kyro-btn-md kyro-btn-primary flex items-center gap-2"
|
|
314
|
+
>
|
|
315
|
+
<Send className="w-3.5 h-3.5" />
|
|
316
|
+
{isLoading ? "Running..." : "Run Query"}
|
|
311
317
|
</button>
|
|
312
318
|
</div>
|
|
313
319
|
</div>
|
|
314
320
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<div className=
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
disabled={!token.trim()}
|
|
329
|
-
className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold text-sm hover:bg-pink-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
330
|
-
>
|
|
331
|
-
Connect
|
|
332
|
-
</button>
|
|
333
|
-
</form>
|
|
334
|
-
<p className="text-xs text-[var(--kyro-text-muted)] mt-2">
|
|
335
|
-
Get your token from the Auth section or login endpoint
|
|
336
|
-
</p>
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
339
|
-
|
|
340
|
-
{/* Tabs */}
|
|
341
|
-
<div className="flex items-center gap-1 px-2 py-2 bg-[var(--kyro-surface-accent)] border-b border-[var(--kyro-border)] overflow-x-auto">
|
|
342
|
-
{tabs.map((tab) => (
|
|
343
|
-
<div
|
|
344
|
-
key={tab.id}
|
|
345
|
-
className={`group flex items-center gap-2 px-3 py-1.5 rounded-t cursor-pointer transition-colors ${
|
|
346
|
-
currentTabId === tab.id
|
|
347
|
-
? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
|
|
348
|
-
: "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)]"
|
|
349
|
-
}`}
|
|
350
|
-
onClick={() => setCurrentTabId(tab.id)}
|
|
351
|
-
>
|
|
352
|
-
<span className="text-xs font-medium">{tab.name}</span>
|
|
353
|
-
{tabs.length > 1 && (
|
|
354
|
-
<button type="button"
|
|
355
|
-
onClick={(e) => closeTab(tab.id, e)}
|
|
356
|
-
className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
|
|
321
|
+
<div className="flex-1 flex overflow-hidden">
|
|
322
|
+
{/* Left: Editor and Schema */}
|
|
323
|
+
<div className={`flex flex-col border-r border-[var(--kyro-border)] transition-all duration-300 ${showDocs ? "w-1/2" : "w-1/2"}`}>
|
|
324
|
+
{/* Tabs for Query/Vars/Headers */}
|
|
325
|
+
<div className="flex border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
326
|
+
{(["query", "variables", "headers"] as const).map((tab) => (
|
|
327
|
+
<button
|
|
328
|
+
key={tab}
|
|
329
|
+
onClick={() => setActiveTab(tab)}
|
|
330
|
+
className={`px-4 py-2 text-xs font-bold tracking-widest transition-all ${activeTab === tab
|
|
331
|
+
? "text-pink-500 border-b-2 border-pink-500"
|
|
332
|
+
: "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]"
|
|
333
|
+
}`}
|
|
357
334
|
>
|
|
358
|
-
|
|
359
|
-
className="w-3 h-3"
|
|
360
|
-
fill="none"
|
|
361
|
-
stroke="currentColor"
|
|
362
|
-
viewBox="0 0 24 24"
|
|
363
|
-
>
|
|
364
|
-
<path
|
|
365
|
-
strokeLinecap="round"
|
|
366
|
-
strokeLinejoin="round"
|
|
367
|
-
strokeWidth="2"
|
|
368
|
-
d="M6 18L18 6M6 6l12 12"
|
|
369
|
-
/>
|
|
370
|
-
</svg>
|
|
335
|
+
{tab}
|
|
371
336
|
</button>
|
|
372
|
-
)}
|
|
337
|
+
))}
|
|
373
338
|
</div>
|
|
374
|
-
))}
|
|
375
|
-
<button type="button"
|
|
376
|
-
onClick={addNewTab}
|
|
377
|
-
className="p-1.5 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded"
|
|
378
|
-
>
|
|
379
|
-
<svg
|
|
380
|
-
className="w-4 h-4"
|
|
381
|
-
fill="none"
|
|
382
|
-
stroke="currentColor"
|
|
383
|
-
viewBox="0 0 24 24"
|
|
384
|
-
>
|
|
385
|
-
<path
|
|
386
|
-
strokeLinecap="round"
|
|
387
|
-
strokeLinejoin="round"
|
|
388
|
-
strokeWidth="2"
|
|
389
|
-
d="M12 4v16m8-8H4"
|
|
390
|
-
/>
|
|
391
|
-
</svg>
|
|
392
|
-
</button>
|
|
393
|
-
</div>
|
|
394
339
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<button type="button"
|
|
412
|
-
onClick={() => setActiveTab("variables")}
|
|
413
|
-
className={`px-3 py-1 text-xs font-bold rounded transition-colors ${
|
|
414
|
-
activeTab === "variables"
|
|
415
|
-
? "bg-pink-500 text-white"
|
|
416
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
417
|
-
}`}
|
|
418
|
-
>
|
|
419
|
-
Variables
|
|
420
|
-
</button>
|
|
421
|
-
<button type="button"
|
|
422
|
-
onClick={() => setActiveTab("headers")}
|
|
423
|
-
className={`px-3 py-1 text-xs font-bold rounded transition-colors ${
|
|
424
|
-
activeTab === "headers"
|
|
425
|
-
? "bg-pink-500 text-white"
|
|
426
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
427
|
-
}`}
|
|
428
|
-
>
|
|
429
|
-
Headers
|
|
430
|
-
</button>
|
|
431
|
-
|
|
432
|
-
<div className="flex-1" />
|
|
433
|
-
|
|
434
|
-
<button type="button"
|
|
435
|
-
onClick={executeQuery}
|
|
436
|
-
disabled={isLoading}
|
|
437
|
-
className="flex items-center gap-2 px-4 py-1.5 bg-pink-500 text-white rounded font-bold text-xs hover:bg-pink-600 transition-colors disabled:opacity-50"
|
|
438
|
-
>
|
|
439
|
-
{isLoading ? (
|
|
440
|
-
<svg
|
|
441
|
-
className="w-4 h-4 animate-spin"
|
|
442
|
-
fill="none"
|
|
443
|
-
viewBox="0 0 24 24"
|
|
444
|
-
>
|
|
445
|
-
<circle
|
|
446
|
-
className="opacity-25"
|
|
447
|
-
cx="12"
|
|
448
|
-
cy="12"
|
|
449
|
-
r="10"
|
|
450
|
-
stroke="currentColor"
|
|
451
|
-
strokeWidth="4"
|
|
452
|
-
/>
|
|
453
|
-
<path
|
|
454
|
-
className="opacity-75"
|
|
455
|
-
fill="currentColor"
|
|
456
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
457
|
-
/>
|
|
458
|
-
</svg>
|
|
459
|
-
) : (
|
|
460
|
-
<svg
|
|
461
|
-
className="w-4 h-4"
|
|
462
|
-
fill="none"
|
|
463
|
-
stroke="currentColor"
|
|
464
|
-
viewBox="0 0 24 24"
|
|
465
|
-
>
|
|
466
|
-
<path
|
|
467
|
-
strokeLinecap="round"
|
|
468
|
-
strokeLinejoin="round"
|
|
469
|
-
strokeWidth="2"
|
|
470
|
-
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
|
471
|
-
/>
|
|
472
|
-
<path
|
|
473
|
-
strokeLinecap="round"
|
|
474
|
-
strokeLinejoin="round"
|
|
475
|
-
strokeWidth="2"
|
|
476
|
-
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
477
|
-
/>
|
|
478
|
-
</svg>
|
|
340
|
+
<div className="flex-1 overflow-hidden relative bg-[var(--kyro-bg)]">
|
|
341
|
+
<Suspense fallback={<div className="p-4 text-xs">Loading editor...</div>}>
|
|
342
|
+
{isMounted && (
|
|
343
|
+
<CodeMirrorEditor
|
|
344
|
+
value={activeTab === "query" ? currentTab.query : activeTab === "variables" ? currentTab.variables : currentTab.headers}
|
|
345
|
+
height="100%"
|
|
346
|
+
extensions={extensions}
|
|
347
|
+
theme={theme}
|
|
348
|
+
onChange={(val) => updateTab(activeTab, val)}
|
|
349
|
+
basicSetup={true}
|
|
350
|
+
style={{
|
|
351
|
+
height: "100%",
|
|
352
|
+
fontSize: "13px",
|
|
353
|
+
fontFamily: "'Fira Code', monospace",
|
|
354
|
+
}}
|
|
355
|
+
/>
|
|
479
356
|
)}
|
|
480
|
-
|
|
481
|
-
<kbd className="ml-1 px-1 py-0.5 bg-white/20 rounded text-[10px]">
|
|
482
|
-
⌘↵
|
|
483
|
-
</kbd>
|
|
484
|
-
</button>
|
|
485
|
-
</div>
|
|
486
|
-
|
|
487
|
-
{/* Editor Content */}
|
|
488
|
-
<div className="flex-1 overflow-auto">
|
|
489
|
-
{activeTab === "query" && (
|
|
490
|
-
<Suspense
|
|
491
|
-
fallback={
|
|
492
|
-
<textarea
|
|
493
|
-
value={currentTab.query}
|
|
494
|
-
onChange={(e) => updateTab("query", e.target.value)}
|
|
495
|
-
className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
|
|
496
|
-
placeholder="Enter your GraphQL query..."
|
|
497
|
-
spellCheck={false}
|
|
498
|
-
/>
|
|
499
|
-
}
|
|
500
|
-
>
|
|
501
|
-
{isMounted && (
|
|
502
|
-
<CodeMirrorEditor
|
|
503
|
-
value={currentTab.query}
|
|
504
|
-
height="100%"
|
|
505
|
-
extensions={extensions}
|
|
506
|
-
theme={theme}
|
|
507
|
-
onChange={(value) => updateTab("query", value)}
|
|
508
|
-
basicSetup={true}
|
|
509
|
-
style={{
|
|
510
|
-
height: "100%",
|
|
511
|
-
minHeight: "300px",
|
|
512
|
-
fontSize: "13px",
|
|
513
|
-
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
|
514
|
-
}}
|
|
515
|
-
/>
|
|
516
|
-
)}
|
|
517
|
-
</Suspense>
|
|
518
|
-
)}
|
|
519
|
-
{activeTab === "variables" && (
|
|
520
|
-
<Suspense
|
|
521
|
-
fallback={
|
|
522
|
-
<textarea
|
|
523
|
-
value={currentTab.variables}
|
|
524
|
-
onChange={(e) => updateTab("variables", e.target.value)}
|
|
525
|
-
className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
|
|
526
|
-
placeholder='{ "key": "value" }'
|
|
527
|
-
spellCheck={false}
|
|
528
|
-
/>
|
|
529
|
-
}
|
|
530
|
-
>
|
|
531
|
-
{isMounted && (
|
|
532
|
-
<CodeMirrorEditor
|
|
533
|
-
value={currentTab.variables}
|
|
534
|
-
height="100%"
|
|
535
|
-
extensions={extensions}
|
|
536
|
-
theme={theme}
|
|
537
|
-
onChange={(value) => updateTab("variables", value)}
|
|
538
|
-
basicSetup={true}
|
|
539
|
-
style={{
|
|
540
|
-
height: "100%",
|
|
541
|
-
minHeight: "300px",
|
|
542
|
-
fontSize: "13px",
|
|
543
|
-
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
|
544
|
-
}}
|
|
545
|
-
/>
|
|
546
|
-
)}
|
|
547
|
-
</Suspense>
|
|
548
|
-
)}
|
|
549
|
-
{activeTab === "headers" && (
|
|
550
|
-
<Suspense
|
|
551
|
-
fallback={
|
|
552
|
-
<textarea
|
|
553
|
-
value={currentTab.headers}
|
|
554
|
-
onChange={(e) => updateTab("headers", e.target.value)}
|
|
555
|
-
className="w-full h-full min-h-[300px] p-4 bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] font-mono text-sm resize-none focus:outline-none"
|
|
556
|
-
placeholder='{ "Authorization": "Bearer your-token" }'
|
|
557
|
-
spellCheck={false}
|
|
558
|
-
/>
|
|
559
|
-
}
|
|
560
|
-
>
|
|
561
|
-
{isMounted && (
|
|
562
|
-
<CodeMirrorEditor
|
|
563
|
-
value={currentTab.headers}
|
|
564
|
-
height="100%"
|
|
565
|
-
extensions={extensions}
|
|
566
|
-
theme={theme}
|
|
567
|
-
onChange={(value) => updateTab("headers", value)}
|
|
568
|
-
basicSetup={true}
|
|
569
|
-
style={{
|
|
570
|
-
height: "100%",
|
|
571
|
-
minHeight: "300px",
|
|
572
|
-
fontSize: "13px",
|
|
573
|
-
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
|
574
|
-
}}
|
|
575
|
-
/>
|
|
576
|
-
)}
|
|
577
|
-
</Suspense>
|
|
578
|
-
)}
|
|
357
|
+
</Suspense>
|
|
579
358
|
</div>
|
|
580
359
|
</div>
|
|
581
360
|
|
|
582
|
-
{/* Right: Response */}
|
|
583
|
-
<div className="flex-1 flex flex-col">
|
|
584
|
-
|
|
585
|
-
<
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
</span>
|
|
592
|
-
)}
|
|
593
|
-
</div>
|
|
594
|
-
<div className="flex-1 overflow-auto p-4">
|
|
595
|
-
{isLoading ? (
|
|
596
|
-
<div className="flex items-center justify-center h-full">
|
|
597
|
-
<div className="animate-spin w-8 h-8 border-2 border-pink-500 border-t-transparent rounded-full" />
|
|
361
|
+
{/* Center/Right: Response or Docs */}
|
|
362
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
363
|
+
{showDocs ? (
|
|
364
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
|
|
365
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--kyro-border)]">
|
|
366
|
+
<span className="text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)]">Schema Explorer</span>
|
|
367
|
+
<button onClick={() => setShowDocs(false)} className="text-[var(--kyro-text-muted)] hover:text-red-500">
|
|
368
|
+
<X className="w-4 h-4" />
|
|
369
|
+
</button>
|
|
598
370
|
</div>
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
371
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
372
|
+
{loadingSchema ? (
|
|
373
|
+
<div className="flex items-center justify-center h-40">
|
|
374
|
+
<RefreshCw className="w-6 h-6 animate-spin text-pink-500" />
|
|
375
|
+
</div>
|
|
376
|
+
) : schema ? (
|
|
377
|
+
<div className="space-y-6">
|
|
378
|
+
{selectedType ? (
|
|
379
|
+
<div className="space-y-4">
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => setSelectedType(null)}
|
|
382
|
+
className="flex items-center gap-1 text-xs text-pink-500 font-bold hover:underline"
|
|
383
|
+
>
|
|
384
|
+
← Back to types
|
|
385
|
+
</button>
|
|
386
|
+
<div>
|
|
387
|
+
<h3 className="text-lg font-bold text-[var(--kyro-text-primary)]">{selectedType.name}</h3>
|
|
388
|
+
<p className="text-xs text-[var(--kyro-text-muted)] italic">{selectedType.kind}</p>
|
|
389
|
+
{selectedType.description && (
|
|
390
|
+
<p className="mt-2 text-sm text-[var(--kyro-text-secondary)] leading-relaxed">{selectedType.description}</p>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
{selectedType.fields && (
|
|
394
|
+
<div className="space-y-3">
|
|
395
|
+
<h4 className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] pt-4">Fields</h4>
|
|
396
|
+
{selectedType.fields.map(f => (
|
|
397
|
+
<div key={f.name} className="p-3 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)]">
|
|
398
|
+
<div className="flex items-center justify-between gap-2">
|
|
399
|
+
<span className="font-bold text-sm text-[var(--kyro-text-primary)]">{f.name}</span>
|
|
400
|
+
<span className="text-[10px] font-mono text-pink-500 bg-pink-500/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
|
|
401
|
+
</div>
|
|
402
|
+
{f.description && <p className="text-xs text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
|
|
403
|
+
{f.args && f.args.length > 0 && (
|
|
404
|
+
<div className="mt-2 pl-4 border-l-2 border-[var(--kyro-border)] space-y-1">
|
|
405
|
+
{f.args.map(a => (
|
|
406
|
+
<div key={a.name} className="text-[10px]">
|
|
407
|
+
<span className="text-[var(--kyro-text-muted)]">{a.name}:</span> <span className="text-pink-400">{renderType(a.type)}</span>
|
|
408
|
+
</div>
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
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 (
|
|
424
|
+
<button
|
|
425
|
+
key={t}
|
|
426
|
+
onClick={() => setSelectedType(found)}
|
|
427
|
+
className="flex items-center justify-between p-4 bg-[var(--kyro-surface-accent)] rounded-xl border border-[var(--kyro-border)] hover:border-pink-500 transition-all text-left group"
|
|
428
|
+
>
|
|
429
|
+
<div>
|
|
430
|
+
<span className="text-xs font-bold text-[var(--kyro-text-muted)] block">{t}</span>
|
|
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" />
|
|
434
|
+
</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
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
) : (
|
|
455
|
+
<p className="text-xs text-[var(--kyro-text-muted)]">No schema loaded.</p>
|
|
456
|
+
)}
|
|
620
457
|
</div>
|
|
621
|
-
|
|
622
|
-
|
|
458
|
+
</div>
|
|
459
|
+
) : (
|
|
460
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
461
|
+
<div className="flex items-center px-4 py-2 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
|
|
462
|
+
<span className="text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)]">Response</span>
|
|
463
|
+
</div>
|
|
464
|
+
<div className="flex-1 overflow-auto p-4 bg-[var(--kyro-bg-secondary)]">
|
|
465
|
+
{isLoading ? (
|
|
466
|
+
<div className="flex flex-col items-center justify-center h-full gap-4">
|
|
467
|
+
<RefreshCw className="w-8 h-8 animate-spin text-pink-500" />
|
|
468
|
+
<span className="text-xs font-bold text-[var(--kyro-text-muted)]">Running Query...</span>
|
|
469
|
+
</div>
|
|
470
|
+
) : response ? (
|
|
471
|
+
<pre className="text-[13px] font-mono text-[var(--kyro-text-primary)] whitespace-pre-wrap selection:bg-pink-500/20">
|
|
472
|
+
{response}
|
|
473
|
+
</pre>
|
|
474
|
+
) : (
|
|
475
|
+
<div className="flex flex-col items-center justify-center h-full opacity-30">
|
|
476
|
+
<Activity className="w-16 h-16 mb-4" />
|
|
477
|
+
<p className="text-sm font-bold">Query output will appear here</p>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
623
483
|
</div>
|
|
624
484
|
</div>
|
|
625
485
|
</div>
|