@brainfish-ai/devdoc 0.1.21
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/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import {
|
|
6
|
+
Play,
|
|
7
|
+
ArrowClockwise,
|
|
8
|
+
Copy,
|
|
9
|
+
Check,
|
|
10
|
+
Spinner,
|
|
11
|
+
X,
|
|
12
|
+
FileText,
|
|
13
|
+
BracketsCurly,
|
|
14
|
+
Key,
|
|
15
|
+
CaretRight,
|
|
16
|
+
CaretDown,
|
|
17
|
+
MagnifyingGlass,
|
|
18
|
+
Lightning,
|
|
19
|
+
ArrowsClockwise
|
|
20
|
+
} from '@phosphor-icons/react'
|
|
21
|
+
import { Button } from '@/components/ui/button'
|
|
22
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
23
|
+
import Editor, { loader } from '@monaco-editor/react'
|
|
24
|
+
|
|
25
|
+
// Configure Monaco to use CDN
|
|
26
|
+
loader.config({
|
|
27
|
+
paths: {
|
|
28
|
+
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Types
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export interface GraphQLOperationItem {
|
|
37
|
+
id: string
|
|
38
|
+
name: string
|
|
39
|
+
description?: string | null
|
|
40
|
+
operationType: 'query' | 'mutation' | 'subscription'
|
|
41
|
+
query: string
|
|
42
|
+
exampleVariables?: Record<string, unknown>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface GraphQLPlaygroundProps {
|
|
46
|
+
/** GraphQL endpoint URL */
|
|
47
|
+
endpoint: string
|
|
48
|
+
/** Initial query to display */
|
|
49
|
+
defaultQuery?: string
|
|
50
|
+
/** Headers to include with requests */
|
|
51
|
+
headers?: Record<string, string>
|
|
52
|
+
/** Operations from parsed schema */
|
|
53
|
+
operations?: GraphQLOperationItem[]
|
|
54
|
+
/** Selected operation ID (controlled from outside) */
|
|
55
|
+
selectedOperationId?: string
|
|
56
|
+
/** Hide internal schema explorer (when using main sidebar) */
|
|
57
|
+
hideExplorer?: boolean
|
|
58
|
+
/** Custom class name */
|
|
59
|
+
className?: string
|
|
60
|
+
/** Theme */
|
|
61
|
+
theme?: 'light' | 'dark'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface GraphQLResponse {
|
|
65
|
+
data?: unknown
|
|
66
|
+
errors?: Array<{
|
|
67
|
+
message: string
|
|
68
|
+
locations?: Array<{ line: number; column: number }>
|
|
69
|
+
path?: Array<string | number>
|
|
70
|
+
}>
|
|
71
|
+
extensions?: Record<string, unknown>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type TabType = 'variables' | 'headers'
|
|
75
|
+
|
|
76
|
+
const STORAGE_KEY = 'brainfish-graphql-playground'
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Helpers
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
function loadStoredState(): { query?: string; variables?: string; headers?: string } | null {
|
|
83
|
+
if (typeof window === 'undefined') return null
|
|
84
|
+
try {
|
|
85
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
86
|
+
if (stored) return JSON.parse(stored)
|
|
87
|
+
} catch { /* ignore */ }
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function saveStoredState(state: { query: string; variables: string; headers: string }): void {
|
|
92
|
+
if (typeof window === 'undefined') return
|
|
93
|
+
try {
|
|
94
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
95
|
+
} catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Schema Explorer Sidebar
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
interface SchemaExplorerProps {
|
|
103
|
+
operations: GraphQLOperationItem[]
|
|
104
|
+
onSelectOperation: (operation: GraphQLOperationItem) => void
|
|
105
|
+
selectedOperationId?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SchemaExplorer({ operations, onSelectOperation, selectedOperationId }: SchemaExplorerProps) {
|
|
109
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
110
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
111
|
+
query: true,
|
|
112
|
+
mutation: true,
|
|
113
|
+
subscription: true,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Group operations by type
|
|
117
|
+
const queries = operations.filter(op => op.operationType === 'query')
|
|
118
|
+
const mutations = operations.filter(op => op.operationType === 'mutation')
|
|
119
|
+
const subscriptions = operations.filter(op => op.operationType === 'subscription')
|
|
120
|
+
|
|
121
|
+
// Filter by search
|
|
122
|
+
const filterOps = (ops: GraphQLOperationItem[]) => {
|
|
123
|
+
if (!searchQuery.trim()) return ops
|
|
124
|
+
const query = searchQuery.toLowerCase()
|
|
125
|
+
return ops.filter(op =>
|
|
126
|
+
op.name.toLowerCase().includes(query) ||
|
|
127
|
+
op.description?.toLowerCase().includes(query)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const filteredQueries = filterOps(queries)
|
|
132
|
+
const filteredMutations = filterOps(mutations)
|
|
133
|
+
const filteredSubscriptions = filterOps(subscriptions)
|
|
134
|
+
|
|
135
|
+
const toggleSection = (section: string) => {
|
|
136
|
+
setExpandedSections(prev => ({
|
|
137
|
+
...prev,
|
|
138
|
+
[section]: !prev[section],
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const renderSection = (
|
|
143
|
+
title: string,
|
|
144
|
+
icon: React.ReactNode,
|
|
145
|
+
ops: GraphQLOperationItem[],
|
|
146
|
+
sectionKey: string,
|
|
147
|
+
iconColor: string
|
|
148
|
+
) => {
|
|
149
|
+
if (ops.length === 0) return null
|
|
150
|
+
const isExpanded = expandedSections[sectionKey]
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="mb-2">
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => toggleSection(sectionKey)}
|
|
156
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground hover:bg-muted/50 transition-colors"
|
|
157
|
+
>
|
|
158
|
+
{isExpanded ? (
|
|
159
|
+
<CaretDown className="w-3 h-3" />
|
|
160
|
+
) : (
|
|
161
|
+
<CaretRight className="w-3 h-3" />
|
|
162
|
+
)}
|
|
163
|
+
<span className={iconColor}>{icon}</span>
|
|
164
|
+
<span>{title}</span>
|
|
165
|
+
<span className="ml-auto text-[10px] font-normal opacity-60">
|
|
166
|
+
{ops.length}
|
|
167
|
+
</span>
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{isExpanded && (
|
|
171
|
+
<div className="ml-2">
|
|
172
|
+
{ops.map((op) => (
|
|
173
|
+
<button
|
|
174
|
+
key={op.id}
|
|
175
|
+
onClick={() => onSelectOperation(op)}
|
|
176
|
+
className={cn(
|
|
177
|
+
'w-full flex items-start gap-2 px-3 py-2 text-left',
|
|
178
|
+
'hover:bg-muted/50 transition-colors rounded-sm',
|
|
179
|
+
'border-l-2',
|
|
180
|
+
selectedOperationId === op.id
|
|
181
|
+
? 'bg-muted/70 border-primary'
|
|
182
|
+
: 'border-transparent'
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<div className="flex-1 min-w-0">
|
|
186
|
+
<div className="text-sm font-medium truncate">
|
|
187
|
+
{op.name}
|
|
188
|
+
</div>
|
|
189
|
+
{op.description && (
|
|
190
|
+
<div className="text-xs text-muted-foreground truncate mt-0.5">
|
|
191
|
+
{op.description}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</button>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const hasOperations = operations.length > 0
|
|
204
|
+
const hasFilteredResults = filteredQueries.length > 0 || filteredMutations.length > 0 || filteredSubscriptions.length > 0
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="flex flex-col h-full border-r border-border bg-muted/20">
|
|
208
|
+
{/* Search */}
|
|
209
|
+
<div className="p-2 border-b border-border">
|
|
210
|
+
<div className="relative">
|
|
211
|
+
<MagnifyingGlass className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
212
|
+
<input
|
|
213
|
+
type="text"
|
|
214
|
+
value={searchQuery}
|
|
215
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
216
|
+
placeholder="Search operations..."
|
|
217
|
+
className={cn(
|
|
218
|
+
'w-full pl-8 pr-3 py-1.5 text-sm rounded-md',
|
|
219
|
+
'bg-background border border-border',
|
|
220
|
+
'focus:outline-none focus:ring-1 focus:ring-primary',
|
|
221
|
+
'placeholder:text-muted-foreground'
|
|
222
|
+
)}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Operations list */}
|
|
228
|
+
<div className="flex-1 overflow-y-auto py-2">
|
|
229
|
+
{!hasOperations ? (
|
|
230
|
+
<div className="flex flex-col items-center justify-center h-full px-4 text-center">
|
|
231
|
+
<FileText className="w-8 h-8 text-muted-foreground/50 mb-2" />
|
|
232
|
+
<p className="text-sm text-muted-foreground">
|
|
233
|
+
No schema loaded
|
|
234
|
+
</p>
|
|
235
|
+
<p className="text-xs text-muted-foreground/60 mt-1">
|
|
236
|
+
Operations will appear here
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
) : !hasFilteredResults ? (
|
|
240
|
+
<div className="flex flex-col items-center justify-center h-32 px-4 text-center">
|
|
241
|
+
<p className="text-sm text-muted-foreground">
|
|
242
|
+
No matching operations
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
) : (
|
|
246
|
+
<>
|
|
247
|
+
{renderSection(
|
|
248
|
+
'Queries',
|
|
249
|
+
<MagnifyingGlass className="w-3.5 h-3.5" weight="bold" />,
|
|
250
|
+
filteredQueries,
|
|
251
|
+
'query',
|
|
252
|
+
'text-blue-500'
|
|
253
|
+
)}
|
|
254
|
+
{renderSection(
|
|
255
|
+
'Mutations',
|
|
256
|
+
<Lightning className="w-3.5 h-3.5" weight="bold" />,
|
|
257
|
+
filteredMutations,
|
|
258
|
+
'mutation',
|
|
259
|
+
'text-orange-500'
|
|
260
|
+
)}
|
|
261
|
+
{renderSection(
|
|
262
|
+
'Subscriptions',
|
|
263
|
+
<ArrowsClockwise className="w-3.5 h-3.5" weight="bold" />,
|
|
264
|
+
filteredSubscriptions,
|
|
265
|
+
'subscription',
|
|
266
|
+
'text-purple-500'
|
|
267
|
+
)}
|
|
268
|
+
</>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Main Component
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
export function GraphQLPlayground({
|
|
280
|
+
endpoint,
|
|
281
|
+
defaultQuery = `# Welcome to the GraphQL Playground!
|
|
282
|
+
#
|
|
283
|
+
# Select an operation from the sidebar, or
|
|
284
|
+
# type your query below and press Ctrl+Enter
|
|
285
|
+
|
|
286
|
+
query {
|
|
287
|
+
__typename
|
|
288
|
+
}
|
|
289
|
+
`,
|
|
290
|
+
headers: defaultHeaders = {},
|
|
291
|
+
operations = [],
|
|
292
|
+
selectedOperationId: externalSelectedId,
|
|
293
|
+
hideExplorer = false,
|
|
294
|
+
className,
|
|
295
|
+
theme = 'dark',
|
|
296
|
+
}: GraphQLPlaygroundProps) {
|
|
297
|
+
const [query, setQuery] = useState(defaultQuery)
|
|
298
|
+
const [variables, setVariables] = useState('{}')
|
|
299
|
+
const [customHeaders, setCustomHeaders] = useState(
|
|
300
|
+
Object.keys(defaultHeaders).length > 0
|
|
301
|
+
? JSON.stringify(defaultHeaders, null, 2)
|
|
302
|
+
: '{}'
|
|
303
|
+
)
|
|
304
|
+
const [response, setResponse] = useState<GraphQLResponse | null>(null)
|
|
305
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
306
|
+
const [error, setError] = useState<string | null>(null)
|
|
307
|
+
const [copied, setCopied] = useState(false)
|
|
308
|
+
const [activeTab, setActiveTab] = useState<TabType>('variables')
|
|
309
|
+
const [responseTime, setResponseTime] = useState<number | null>(null)
|
|
310
|
+
const [internalSelectedId, setInternalSelectedId] = useState<string | undefined>()
|
|
311
|
+
|
|
312
|
+
// Use external selection if provided, otherwise use internal
|
|
313
|
+
const selectedOperationId = externalSelectedId ?? internalSelectedId
|
|
314
|
+
|
|
315
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
316
|
+
const hasLoadedRef = useRef(false)
|
|
317
|
+
|
|
318
|
+
// Load from localStorage on mount
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
const stored = loadStoredState()
|
|
321
|
+
if (stored) {
|
|
322
|
+
if (stored.query) setQuery(stored.query)
|
|
323
|
+
if (stored.variables) setVariables(stored.variables)
|
|
324
|
+
if (stored.headers) setCustomHeaders(stored.headers)
|
|
325
|
+
}
|
|
326
|
+
hasLoadedRef.current = true
|
|
327
|
+
}, [])
|
|
328
|
+
|
|
329
|
+
// Save to localStorage when values change
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!hasLoadedRef.current) return
|
|
332
|
+
const timeoutId = setTimeout(() => {
|
|
333
|
+
saveStoredState({ query, variables, headers: customHeaders })
|
|
334
|
+
}, 500)
|
|
335
|
+
return () => clearTimeout(timeoutId)
|
|
336
|
+
}, [query, variables, customHeaders])
|
|
337
|
+
|
|
338
|
+
// Handle operation selection from sidebar (internal)
|
|
339
|
+
const handleSelectOperation = useCallback((operation: GraphQLOperationItem) => {
|
|
340
|
+
setQuery(operation.query)
|
|
341
|
+
setInternalSelectedId(operation.id)
|
|
342
|
+
|
|
343
|
+
// Set example variables if available
|
|
344
|
+
if (operation.exampleVariables && Object.keys(operation.exampleVariables).length > 0) {
|
|
345
|
+
setVariables(JSON.stringify(operation.exampleVariables, null, 2))
|
|
346
|
+
} else {
|
|
347
|
+
setVariables('{}')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Clear previous response
|
|
351
|
+
setResponse(null)
|
|
352
|
+
setError(null)
|
|
353
|
+
setResponseTime(null)
|
|
354
|
+
}, [])
|
|
355
|
+
|
|
356
|
+
// Sync with external selection (from main sidebar)
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (externalSelectedId && operations.length > 0) {
|
|
359
|
+
const op = operations.find(o => o.id === externalSelectedId)
|
|
360
|
+
if (op) {
|
|
361
|
+
setQuery(op.query)
|
|
362
|
+
if (op.exampleVariables && Object.keys(op.exampleVariables).length > 0) {
|
|
363
|
+
setVariables(JSON.stringify(op.exampleVariables, null, 2))
|
|
364
|
+
} else {
|
|
365
|
+
setVariables('{}')
|
|
366
|
+
}
|
|
367
|
+
setResponse(null)
|
|
368
|
+
setError(null)
|
|
369
|
+
setResponseTime(null)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}, [externalSelectedId, operations])
|
|
373
|
+
|
|
374
|
+
// Execute GraphQL query
|
|
375
|
+
const executeQuery = useCallback(async () => {
|
|
376
|
+
setIsLoading(true)
|
|
377
|
+
setError(null)
|
|
378
|
+
setResponse(null)
|
|
379
|
+
setResponseTime(null)
|
|
380
|
+
|
|
381
|
+
if (abortControllerRef.current) {
|
|
382
|
+
abortControllerRef.current.abort()
|
|
383
|
+
}
|
|
384
|
+
abortControllerRef.current = new AbortController()
|
|
385
|
+
|
|
386
|
+
const startTime = performance.now()
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
let parsedVariables = {}
|
|
390
|
+
try {
|
|
391
|
+
parsedVariables = variables.trim() && variables.trim() !== '{}'
|
|
392
|
+
? JSON.parse(variables)
|
|
393
|
+
: {}
|
|
394
|
+
} catch {
|
|
395
|
+
setError('Invalid JSON in variables')
|
|
396
|
+
setIsLoading(false)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let parsedHeaders: Record<string, string> = {}
|
|
401
|
+
try {
|
|
402
|
+
parsedHeaders = customHeaders.trim() && customHeaders.trim() !== '{}'
|
|
403
|
+
? JSON.parse(customHeaders)
|
|
404
|
+
: {}
|
|
405
|
+
} catch {
|
|
406
|
+
setError('Invalid JSON in headers')
|
|
407
|
+
setIsLoading(false)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const res = await fetch(endpoint, {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: {
|
|
414
|
+
'Content-Type': 'application/json',
|
|
415
|
+
Accept: 'application/json',
|
|
416
|
+
...defaultHeaders,
|
|
417
|
+
...parsedHeaders,
|
|
418
|
+
},
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
query,
|
|
421
|
+
variables: parsedVariables,
|
|
422
|
+
}),
|
|
423
|
+
signal: abortControllerRef.current.signal,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const endTime = performance.now()
|
|
427
|
+
setResponseTime(Math.round(endTime - startTime))
|
|
428
|
+
|
|
429
|
+
const data = await res.json()
|
|
430
|
+
setResponse(data)
|
|
431
|
+
} catch (err) {
|
|
432
|
+
if (err instanceof Error && err.name === 'AbortError') return
|
|
433
|
+
setError(err instanceof Error ? err.message : 'Request failed')
|
|
434
|
+
} finally {
|
|
435
|
+
setIsLoading(false)
|
|
436
|
+
}
|
|
437
|
+
}, [query, variables, customHeaders, endpoint, defaultHeaders])
|
|
438
|
+
|
|
439
|
+
const handleCancel = useCallback(() => {
|
|
440
|
+
if (abortControllerRef.current) {
|
|
441
|
+
abortControllerRef.current.abort()
|
|
442
|
+
abortControllerRef.current = null
|
|
443
|
+
}
|
|
444
|
+
setIsLoading(false)
|
|
445
|
+
}, [])
|
|
446
|
+
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
449
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
450
|
+
e.preventDefault()
|
|
451
|
+
if (!isLoading) executeQuery()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
455
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
456
|
+
}, [executeQuery, isLoading])
|
|
457
|
+
|
|
458
|
+
const copyResponse = useCallback(() => {
|
|
459
|
+
if (response) {
|
|
460
|
+
navigator.clipboard.writeText(JSON.stringify(response, null, 2))
|
|
461
|
+
setCopied(true)
|
|
462
|
+
setTimeout(() => setCopied(false), 2000)
|
|
463
|
+
}
|
|
464
|
+
}, [response])
|
|
465
|
+
|
|
466
|
+
const handleReset = useCallback(() => {
|
|
467
|
+
setQuery(defaultQuery)
|
|
468
|
+
setVariables('{}')
|
|
469
|
+
setCustomHeaders(
|
|
470
|
+
Object.keys(defaultHeaders).length > 0
|
|
471
|
+
? JSON.stringify(defaultHeaders, null, 2)
|
|
472
|
+
: '{}'
|
|
473
|
+
)
|
|
474
|
+
setResponse(null)
|
|
475
|
+
setError(null)
|
|
476
|
+
setResponseTime(null)
|
|
477
|
+
setInternalSelectedId(undefined)
|
|
478
|
+
localStorage.removeItem(STORAGE_KEY)
|
|
479
|
+
}, [defaultQuery, defaultHeaders])
|
|
480
|
+
|
|
481
|
+
const monacoTheme = theme === 'dark' ? 'vs-dark' : 'vs'
|
|
482
|
+
const hasErrors = response?.errors && response.errors.length > 0
|
|
483
|
+
const statusColor = hasErrors ? 'text-red-400' : response ? 'text-emerald-400' : 'text-zinc-500'
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<TooltipProvider>
|
|
487
|
+
<div className={cn('flex flex-col h-full bg-background', className)}>
|
|
488
|
+
{/* Toolbar */}
|
|
489
|
+
<div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-border bg-muted/50">
|
|
490
|
+
<div className="flex items-center gap-2">
|
|
491
|
+
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-md bg-background border border-border">
|
|
492
|
+
<span className="text-xs font-medium text-emerald-500">POST</span>
|
|
493
|
+
<span className="text-xs text-muted-foreground font-mono truncate max-w-[200px] md:max-w-[400px]">
|
|
494
|
+
{endpoint}
|
|
495
|
+
</span>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<div className="flex items-center gap-2">
|
|
500
|
+
<Button
|
|
501
|
+
onClick={isLoading ? handleCancel : executeQuery}
|
|
502
|
+
variant={isLoading ? 'destructive' : 'default'}
|
|
503
|
+
size="sm"
|
|
504
|
+
className="px-4"
|
|
505
|
+
>
|
|
506
|
+
{isLoading ? (
|
|
507
|
+
<>
|
|
508
|
+
<X className="h-4 w-4 mr-2" weight="bold" />
|
|
509
|
+
Cancel
|
|
510
|
+
</>
|
|
511
|
+
) : (
|
|
512
|
+
<>
|
|
513
|
+
<Play className="h-4 w-4 mr-2" weight="fill" />
|
|
514
|
+
Run
|
|
515
|
+
</>
|
|
516
|
+
)}
|
|
517
|
+
</Button>
|
|
518
|
+
|
|
519
|
+
<Tooltip>
|
|
520
|
+
<TooltipTrigger asChild>
|
|
521
|
+
<Button variant="outline" size="icon" onClick={handleReset}>
|
|
522
|
+
<ArrowClockwise className="h-4 w-4" weight="bold" />
|
|
523
|
+
</Button>
|
|
524
|
+
</TooltipTrigger>
|
|
525
|
+
<TooltipContent>Reset to defaults</TooltipContent>
|
|
526
|
+
</Tooltip>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
{/* Main Content */}
|
|
531
|
+
<div className="flex-1 flex overflow-hidden">
|
|
532
|
+
{/* Left Sidebar - Schema Explorer (only shown when not using main sidebar) */}
|
|
533
|
+
{!hideExplorer && operations.length > 0 && (
|
|
534
|
+
<div className="w-64 flex-shrink-0 hidden md:block">
|
|
535
|
+
<SchemaExplorer
|
|
536
|
+
operations={operations}
|
|
537
|
+
onSelectOperation={handleSelectOperation}
|
|
538
|
+
selectedOperationId={selectedOperationId}
|
|
539
|
+
/>
|
|
540
|
+
</div>
|
|
541
|
+
)}
|
|
542
|
+
|
|
543
|
+
{/* Center & Right - Query/Response */}
|
|
544
|
+
<div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
|
|
545
|
+
{/* Query editor */}
|
|
546
|
+
<div className="flex-1 flex flex-col border-b lg:border-b-0 lg:border-r border-border min-h-[200px] lg:min-h-0">
|
|
547
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
548
|
+
<div className="flex items-center px-3 py-2 border-b border-border bg-muted/30">
|
|
549
|
+
<FileText className="h-4 w-4 mr-2 text-muted-foreground" />
|
|
550
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
551
|
+
Query
|
|
552
|
+
</span>
|
|
553
|
+
<span className="ml-auto text-xs text-muted-foreground">
|
|
554
|
+
Ctrl+Enter to run
|
|
555
|
+
</span>
|
|
556
|
+
</div>
|
|
557
|
+
<div className="flex-1 min-h-0">
|
|
558
|
+
<Editor
|
|
559
|
+
height="100%"
|
|
560
|
+
language="graphql"
|
|
561
|
+
value={query}
|
|
562
|
+
onChange={(value) => {
|
|
563
|
+
setQuery(value || '')
|
|
564
|
+
setInternalSelectedId(undefined) // Clear internal selection when manually editing
|
|
565
|
+
}}
|
|
566
|
+
theme={monacoTheme}
|
|
567
|
+
options={{
|
|
568
|
+
minimap: { enabled: false },
|
|
569
|
+
scrollBeyondLastLine: false,
|
|
570
|
+
fontSize: 13,
|
|
571
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
|
|
572
|
+
lineNumbers: 'on',
|
|
573
|
+
renderLineHighlight: 'line',
|
|
574
|
+
scrollbar: { vertical: 'auto', horizontal: 'auto' },
|
|
575
|
+
padding: { top: 12, bottom: 12 },
|
|
576
|
+
wordWrap: 'on',
|
|
577
|
+
automaticLayout: true,
|
|
578
|
+
tabSize: 2,
|
|
579
|
+
}}
|
|
580
|
+
loading={
|
|
581
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
582
|
+
<Spinner className="w-5 h-5 animate-spin mr-2" />
|
|
583
|
+
Loading editor...
|
|
584
|
+
</div>
|
|
585
|
+
}
|
|
586
|
+
/>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Variables/Headers tabs */}
|
|
591
|
+
<div className="border-t border-border h-[180px] flex flex-col">
|
|
592
|
+
<div className="flex border-b border-border bg-muted/30">
|
|
593
|
+
<button
|
|
594
|
+
onClick={() => setActiveTab('variables')}
|
|
595
|
+
className={cn(
|
|
596
|
+
'flex items-center gap-1.5 px-4 py-2 text-xs font-medium uppercase tracking-wider transition-colors',
|
|
597
|
+
activeTab === 'variables'
|
|
598
|
+
? 'text-primary border-b-2 border-primary -mb-px bg-background'
|
|
599
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
600
|
+
)}
|
|
601
|
+
>
|
|
602
|
+
<BracketsCurly className="h-3.5 w-3.5" />
|
|
603
|
+
Variables
|
|
604
|
+
</button>
|
|
605
|
+
<button
|
|
606
|
+
onClick={() => setActiveTab('headers')}
|
|
607
|
+
className={cn(
|
|
608
|
+
'flex items-center gap-1.5 px-4 py-2 text-xs font-medium uppercase tracking-wider transition-colors',
|
|
609
|
+
activeTab === 'headers'
|
|
610
|
+
? 'text-primary border-b-2 border-primary -mb-px bg-background'
|
|
611
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
612
|
+
)}
|
|
613
|
+
>
|
|
614
|
+
<Key className="h-3.5 w-3.5" />
|
|
615
|
+
Headers
|
|
616
|
+
</button>
|
|
617
|
+
</div>
|
|
618
|
+
<div className="flex-1 min-h-0">
|
|
619
|
+
<Editor
|
|
620
|
+
height="100%"
|
|
621
|
+
language="json"
|
|
622
|
+
value={activeTab === 'variables' ? variables : customHeaders}
|
|
623
|
+
onChange={(value) =>
|
|
624
|
+
activeTab === 'variables'
|
|
625
|
+
? setVariables(value || '{}')
|
|
626
|
+
: setCustomHeaders(value || '{}')
|
|
627
|
+
}
|
|
628
|
+
theme={monacoTheme}
|
|
629
|
+
options={{
|
|
630
|
+
minimap: { enabled: false },
|
|
631
|
+
scrollBeyondLastLine: false,
|
|
632
|
+
fontSize: 13,
|
|
633
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
|
|
634
|
+
lineNumbers: 'off',
|
|
635
|
+
renderLineHighlight: 'none',
|
|
636
|
+
scrollbar: { vertical: 'auto', horizontal: 'auto' },
|
|
637
|
+
padding: { top: 8, bottom: 8 },
|
|
638
|
+
wordWrap: 'on',
|
|
639
|
+
automaticLayout: true,
|
|
640
|
+
tabSize: 2,
|
|
641
|
+
folding: false,
|
|
642
|
+
}}
|
|
643
|
+
loading={
|
|
644
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
645
|
+
Loading...
|
|
646
|
+
</div>
|
|
647
|
+
}
|
|
648
|
+
/>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
{/* Response */}
|
|
654
|
+
<div className="flex-1 flex flex-col min-h-[200px] lg:min-h-0">
|
|
655
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border bg-muted/30">
|
|
656
|
+
<div className="flex items-center gap-3">
|
|
657
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
658
|
+
Response
|
|
659
|
+
</span>
|
|
660
|
+
{responseTime !== null && (
|
|
661
|
+
<span className="text-xs text-muted-foreground">
|
|
662
|
+
{responseTime}ms
|
|
663
|
+
</span>
|
|
664
|
+
)}
|
|
665
|
+
{response && (
|
|
666
|
+
<span className={cn('text-xs font-medium', statusColor)}>
|
|
667
|
+
{hasErrors ? 'Error' : 'Success'}
|
|
668
|
+
</span>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
{response && (
|
|
672
|
+
<Tooltip>
|
|
673
|
+
<TooltipTrigger asChild>
|
|
674
|
+
<Button variant="ghost" size="sm" onClick={copyResponse} className="h-7 px-2">
|
|
675
|
+
{copied ? (
|
|
676
|
+
<>
|
|
677
|
+
<Check className="h-3.5 w-3.5 mr-1 text-emerald-500" />
|
|
678
|
+
<span className="text-xs">Copied</span>
|
|
679
|
+
</>
|
|
680
|
+
) : (
|
|
681
|
+
<>
|
|
682
|
+
<Copy className="h-3.5 w-3.5 mr-1" />
|
|
683
|
+
<span className="text-xs">Copy</span>
|
|
684
|
+
</>
|
|
685
|
+
)}
|
|
686
|
+
</Button>
|
|
687
|
+
</TooltipTrigger>
|
|
688
|
+
<TooltipContent>Copy response</TooltipContent>
|
|
689
|
+
</Tooltip>
|
|
690
|
+
)}
|
|
691
|
+
</div>
|
|
692
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
693
|
+
{isLoading ? (
|
|
694
|
+
<div className="flex items-center justify-center h-full">
|
|
695
|
+
<Spinner className="w-6 h-6 animate-spin text-primary" />
|
|
696
|
+
<span className="ml-2 text-sm text-muted-foreground">Executing query...</span>
|
|
697
|
+
</div>
|
|
698
|
+
) : error ? (
|
|
699
|
+
<div className="p-4">
|
|
700
|
+
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
|
|
701
|
+
<p className="text-destructive text-sm font-mono">{error}</p>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
) : response ? (
|
|
705
|
+
<Editor
|
|
706
|
+
height="100%"
|
|
707
|
+
language="json"
|
|
708
|
+
value={JSON.stringify(response, null, 2)}
|
|
709
|
+
theme={monacoTheme}
|
|
710
|
+
options={{
|
|
711
|
+
readOnly: true,
|
|
712
|
+
minimap: { enabled: false },
|
|
713
|
+
scrollBeyondLastLine: false,
|
|
714
|
+
fontSize: 13,
|
|
715
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace",
|
|
716
|
+
lineNumbers: 'on',
|
|
717
|
+
renderLineHighlight: 'none',
|
|
718
|
+
scrollbar: { vertical: 'auto', horizontal: 'auto' },
|
|
719
|
+
padding: { top: 12, bottom: 12 },
|
|
720
|
+
wordWrap: 'on',
|
|
721
|
+
automaticLayout: true,
|
|
722
|
+
folding: true,
|
|
723
|
+
}}
|
|
724
|
+
/>
|
|
725
|
+
) : (
|
|
726
|
+
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
|
727
|
+
<Play className="h-12 w-12 mb-3 opacity-20" />
|
|
728
|
+
<p className="text-sm">Run a query to see results</p>
|
|
729
|
+
<p className="text-xs mt-1 opacity-60">Press Ctrl+Enter or click Run</p>
|
|
730
|
+
</div>
|
|
731
|
+
)}
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
</TooltipProvider>
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function GraphQLPlaygroundWithExplorer(props: GraphQLPlaygroundProps) {
|
|
742
|
+
return <GraphQLPlayground {...props} />
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export default GraphQLPlayground
|