Graphql Query Explorer
blockdevopsApollo and GraphiQL style GraphQL query builder and response inspector with schema documentation sidebar, syntax-highlighted query & variables editor, query history presets, execution tracing, and formatted JSON response viewer.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/graphql-query-explorer.json$npx shadcn@latest add https://uipkge.dev/r/react/graphql-query-explorer.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/graphql-query-explorer.json$bunx shadcn@latest add https://uipkge.dev/r/react/graphql-query-explorer.jsonNamed registry:
npx shadcn@latest add @uipkge-react/graphql-query-explorerInstalls to:components/blocks/graphql-query-explorer/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SchemaFieldinterface SchemaField {
name: string
args?: string
returnType: string
description: string
sampleQuery: string
sampleVariables: string
}RootTypeinterface RootType {
name: 'Query' | 'Mutation' | 'Subscription'
color: string
badgeVariant: 'default' | 'secondary' | 'outline'
fields: SchemaField[]
}QueryPresetinterface QueryPreset {
id: string
name: string
operationType: 'query' | 'mutation' | 'subscription'
query: string
variables: string
response: Record<string, unknown>
latency: string
size: string
timestamp: string
}ResolverTraceinterface ResolverTrace {
path: string
parentType: string
fieldName: string
returnType: string
durationMs: number
percentage: number
}npm dependencies
Files installed (4)
components/blocks/graphql-query-explorer/GraphqlQueryExplorer.tsx30.9 kB
'use client' import * as React from 'react' import { AlignLeft, Check, ChevronDown, ChevronUp, Copy, FileCode, Globe, History, Loader2, Play, Zap, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { GraphqlSchemaSidebar } from './GraphqlSchemaSidebar' import { GraphqlTracingPanel } from './GraphqlTracingPanel' import { type QueryPreset, type SchemaField } from './graphql-query-explorer-types' export interface GraphqlQueryExplorerProps { className?: string } const presets: QueryPreset[] = [ { id: 'preset-customer', name: 'GetCustomerProfile', operationType: 'query', query: `query GetCustomerProfile($id: ID!) { customer(id: $id) { id name email avatarUrl status createdAt subscriptions { id status plan renewalDate seats } billingAddress { city country postalCode } } }`, variables: `{\n "id": "cust_8492"\n}`, response: { data: { customer: { id: 'cust_8492', name: 'Sarah Jenkins', email: 'sarah.jenkins@acme-corp.io', avatarUrl: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330', status: 'ACTIVE', createdAt: '2024-03-15T08:22:19Z', subscriptions: [ { id: 'sub_9812', status: 'ACTIVE', plan: 'ENTERPRISE_ANNUAL', renewalDate: '2027-03-15', seats: 48, }, { id: 'sub_4410', status: 'CANCELED', plan: 'STARTER_TRIAL', renewalDate: '2024-04-01', seats: 5, }, ], billingAddress: { city: 'San Francisco', country: 'US', postalCode: '94107', }, }, }, extensions: { tracing: { version: 1, duration: 64120000, }, }, }, latency: '64ms', size: '1.8 kB', timestamp: 'Just now', }, { id: 'preset-users', name: 'ListUsers', operationType: 'query', query: `query ListUsers($limit: Int, $offset: Int) { users(limit: $limit, offset: $offset) { id fullName email role isVerified lastActiveAt } }`, variables: `{\n "limit": 3,\n "offset": 0\n}`, response: { data: { users: [ { id: 'usr_9011', fullName: 'Alex Rivera', email: 'alex.rivera@uipkge.dev', role: 'ADMIN', isVerified: true, lastActiveAt: '2026-08-21T10:45:00Z', }, { id: 'usr_9012', fullName: 'Elena Rostova', email: 'elena.r@uipkge.dev', role: 'DEVELOPER', isVerified: true, lastActiveAt: '2026-08-21T09:12:30Z', }, { id: 'usr_9013', fullName: 'Marcus Chen', email: 'marcus.chen@uipkge.dev', role: 'MEMBER', isVerified: false, lastActiveAt: '2026-08-20T18:04:12Z', }, ], }, }, latency: '38ms', size: '0.9 kB', timestamp: '2 mins ago', }, { id: 'preset-create-order', name: 'CreateOrder', operationType: 'mutation', query: `mutation CreateOrder($input: CreateOrderInput!) { createOrder(input: $input) { order { id status totalAmount currency itemsCount createdAt } clientSecret error { code message } } }`, variables: `{\n "input": {\n "customerId": "cust_8492",\n "items": [\n { "sku": "SKU_PRO_REGISTRY", "quantity": 1, "price": 149.00 }\n ],\n "currency": "USD"\n }\n}`, response: { data: { createOrder: { order: { id: 'ord_88201', status: 'PENDING_PAYMENT', totalAmount: 149.0, currency: 'USD', itemsCount: 1, createdAt: '2026-08-21T12:00:00Z', }, clientSecret: 'pi_3Mtwx2_secret_9941a8', error: null, }, }, }, latency: '112ms', size: '1.1 kB', timestamp: '5 mins ago', }, { id: 'preset-subscription-stream', name: 'OnOrderStatusChanged', operationType: 'subscription', query: `subscription OnOrderStatusChanged($orderId: ID!) { orderStatusUpdated(orderId: $orderId) { orderId previousStatus newStatus updatedAt carrierTracking { carrier trackingNumber estimatedDelivery } } }`, variables: `{\n "orderId": "ord_88201"\n}`, response: { data: { orderStatusUpdated: { orderId: 'ord_88201', previousStatus: 'PROCESSING', newStatus: 'SHIPPED', updatedAt: '2026-08-21T12:05:44Z', carrierTracking: { carrier: 'FedEx Priority', trackingNumber: 'FDX-9982-1049-US', estimatedDelivery: '2026-08-23T16:00:00Z', }, }, }, }, latency: '18ms', size: '0.7 kB', timestamp: '12 mins ago', }, ] function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } function highlightJsonLine(line: string): string { if (!line) return '' const escaped = escapeHtml(line) return escaped.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => { let cls = 'text-warning' if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'text-info font-medium' } else { cls = 'text-success' } } else if (/true|false/.test(match)) { cls = 'text-chart-2 font-semibold' } else if (/null/.test(match)) { cls = 'text-destructive italic' } return `<span class="${cls}">${match}</span>` }, ) } function highlightGraphqlLine(line: string): string { if (!line) return '' const escaped = escapeHtml(line) if (escaped.trim().startsWith('#')) { return `<span class="text-muted-foreground/60 italic">${escaped}</span>` } // Park each emitted <span> behind a letter-only placeholder: the type pass // (`:\s*Type`) otherwise matched the `dark:text-...` inside markup an earlier // pass had already inserted, splitting the class attribute. const parked: string[] = [] const park = (html: string) => { const key = String(parked.length) .split('') .map((d) => String.fromCharCode(97 + Number(d))) .join('') parked.push(html) return `\u0000${key}\u0000` } return escaped .replace(/\b(query|mutation|subscription|fragment|on)\b/g, (m) => park(`<span class="text-info font-semibold">${m}</span>`), ) .replace(/(\$[a-zA-Z0-9_]+)/g, (m) => park(`<span class="text-success font-medium">${m}</span>`)) .replace(/:\s*([A-Za-z0-9_\[\]!]+)/g, (_, t) => ': ' + park(`<span class="text-chart-1 font-mono">${t}</span>`)) .replace(/(@[a-zA-Z0-9_]+)/g, (m) => park(`<span class="text-warning">${m}</span>`)) .replace(/\u0000([a-j]+)\u0000/g, (_, key: string) => { const idx = Number( key .split('') .map((c: string) => String(c.charCodeAt(0) - 97)) .join(''), ) return parked[idx] ?? '' }) } function prettifyGraphql(queryStr: string): string { const lines = queryStr.split('\n') let indentLevel = 0 const formatted: string[] = [] for (const rawLine of lines) { const line = rawLine.trim() if (!line) continue if (line.startsWith('}') || line.startsWith(')')) { indentLevel = Math.max(0, indentLevel - 1) } formatted.push(' '.repeat(indentLevel) + line) if (line.endsWith('{') || line.endsWith('(')) { indentLevel++ } } return formatted.join('\n') } export function GraphqlQueryExplorer({ className }: GraphqlQueryExplorerProps) { const [endpointUrl, setEndpointUrl] = React.useState('https://api.uipkge.dev/graphql') const [editorMode, setEditorMode] = React.useState<'edit' | 'preview'>('edit') const [activeCenterTab, setActiveCenterTab] = React.useState<'variables' | 'headers'>('variables') const [isVariablesCollapsed, setIsVariablesCollapsed] = React.useState(false) const [activeResponseTab, setActiveResponseTab] = React.useState<'response' | 'tracing' | 'headers'>('response') const [isLoading, setIsLoading] = React.useState(false) const [copiedResponse, setCopiedResponse] = React.useState(false) const [showHistoryDropdown, setShowHistoryDropdown] = React.useState(false) const [activeQuery, setActiveQuery] = React.useState(presets[0].query) const [activeVariables, setActiveVariables] = React.useState(presets[0].variables) const [activeHeaders, setActiveHeaders] = React.useState( `{\n "Authorization": "Bearer uipkge_live_9f81a7",\n "X-Tenant-ID": "tenant_prod_eu"\n}`, ) const [responseJson, setResponseJson] = React.useState<Record<string, unknown>>(presets[0].response) const [responseStatus, setResponseStatus] = React.useState('200 OK') const [responseLatency, setResponseLatency] = React.useState('64ms') const [responseSize, setResponseSize] = React.useState('1.8 kB') const queryLines = React.useMemo(() => activeQuery.split('\n'), [activeQuery]) const variableLines = React.useMemo(() => activeVariables.split('\n'), [activeVariables]) const headersLines = React.useMemo(() => activeHeaders.split('\n'), [activeHeaders]) const formattedResponseString = React.useMemo(() => JSON.stringify(responseJson, null, 2), [responseJson]) const responseLines = React.useMemo(() => formattedResponseString.split('\n'), [formattedResponseString]) const detectedOperation = React.useMemo(() => { const q = activeQuery.trim() if (q.startsWith('mutation')) return { type: 'mutation', color: 'bg-warning/10 text-warning border-warning/30' } if (q.startsWith('subscription')) return { type: 'subscription', color: 'bg-chart-1/10 text-chart-1 border-chart-1/30', } return { type: 'query', color: 'bg-info/10 text-info border-info/30' } }, [activeQuery]) const isVariablesValidJson = React.useMemo(() => { try { JSON.parse(activeVariables) return true } catch { return false } }, [activeVariables]) const handlePrettify = () => { setActiveQuery(prettifyGraphql(activeQuery)) if (isVariablesValidJson) { try { setActiveVariables(JSON.stringify(JSON.parse(activeVariables), null, 2)) } catch { // Keep as-is } } } const runQuery = () => { if (isLoading) return setIsLoading(true) setTimeout(() => { const randMs = Math.floor(Math.random() * 40) + 35 setResponseLatency(`${randMs}ms`) const trimmed = activeQuery.trim() if (trimmed.includes('createOrder') || trimmed.includes('mutation')) { setResponseJson(presets[2].response) setResponseSize(presets[2].size) setResponseStatus('200 OK') } else if (trimmed.includes('orderStatusUpdated') || trimmed.includes('subscription')) { setResponseJson(presets[3].response) setResponseSize(presets[3].size) setResponseStatus('200 OK · SSE Stream') } else if (trimmed.includes('users') || trimmed.includes('ListUsers')) { setResponseJson(presets[1].response) setResponseSize(presets[1].size) setResponseStatus('200 OK') } else { setResponseJson(presets[0].response) setResponseSize(presets[0].size) setResponseStatus('200 OK') } setIsLoading(false) }, 260) } const loadPreset = (preset: QueryPreset) => { setActiveQuery(preset.query) setActiveVariables(preset.variables) setResponseJson(preset.response) setResponseLatency(preset.latency) setResponseSize(preset.size) setShowHistoryDropdown(false) } const loadFieldQuery = (field: SchemaField) => { setActiveQuery(field.sampleQuery) setActiveVariables(field.sampleVariables) } const copyResponse = () => { navigator.clipboard.writeText(formattedResponseString) setCopiedResponse(true) setTimeout(() => { setCopiedResponse(false) }, 2000) } return ( <Card data-slot="graphql-query-explorer" className={cn( 'border-border bg-card text-card-foreground flex min-h-[720px] flex-col overflow-hidden rounded-xl border shadow-xs', className, )} > {/* TOP TOOLBAR */} <header className="border-border bg-muted/40 flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3"> {/* Left: Endpoint & Method & Schema Badge */} <div className="flex flex-wrap items-center gap-2.5"> <Badge variant="outline" className="border-border bg-background font-mono text-xs font-semibold tracking-wider" > POST </Badge> <div className="border-border bg-background flex h-8 items-center gap-2 rounded-md border px-3 text-xs shadow-xs"> <Globe className="text-muted-foreground size-3.5" /> <input value={endpointUrl} onChange={(e) => setEndpointUrl(e.target.value)} type="text" className="text-foreground w-64 bg-transparent font-mono text-xs focus:outline-none" placeholder="https://api.example.com/graphql" /> </div> <Badge variant="secondary" className="gap-1.5 py-1 font-mono text-xs font-normal"> <span className="bg-success size-1.5 animate-pulse rounded-full" /> Schema v2.4 · 48 Types </Badge> </div> {/* Right: Action Buttons */} <div className="flex flex-wrap items-center gap-2"> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" title="Format query (Shift + Option + F)" onClick={handlePrettify} > <AlignLeft className="size-3.5" /> Prettify </Button> {/* History Menu Toggle */} <div className="relative"> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={() => setShowHistoryDropdown(!showHistoryDropdown)} > <History className="size-3.5" /> History <Badge variant="secondary" className="ml-0.5 h-4 px-1 font-mono text-xs"> {presets.length} </Badge> </Button> {/* History Dropdown Card */} {showHistoryDropdown && ( <div className="border-border bg-popover text-popover-foreground absolute right-0 z-50 mt-2 w-80 rounded-lg border p-2 text-xs shadow-md"> <div className="border-border text-muted-foreground flex items-center justify-between gap-x-2 border-b px-2 py-1.5 font-medium"> <span>Recent Executions</span> <span className="font-mono text-xs">Saved Stubs</span> </div> <div className="mt-1 max-h-60 space-y-1 overflow-y-auto"> {presets.map((preset) => ( <button key={preset.id} className="hover:bg-muted focus:bg-muted flex w-full flex-col gap-0.5 rounded-md p-2 text-left transition-colors" onClick={() => loadPreset(preset)} > <div className="flex items-center justify-between gap-x-2"> <span className="text-foreground font-mono font-medium">{preset.name}</span> <Badge variant="outline" className="font-mono text-xs font-normal"> {preset.latency} </Badge> </div> <div className="text-muted-foreground flex flex-wrap items-center gap-2 font-mono text-xs"> <span className="text-primary font-semibold uppercase">{preset.operationType}</span> <span>·</span> <span>{preset.timestamp}</span> </div> </button> ))} </div> </div> )} </div> {/* Run Query Primary Action */} <Button variant="default" size="sm" className="bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-2 text-xs font-semibold shadow-xs" disabled={isLoading} onClick={runQuery} > {isLoading ? <Loader2 className="size-3.5 animate-spin" /> : <Play className="size-3.5 fill-current" />} Run Query <kbd className="border-primary-foreground/30 bg-primary-foreground/15 hidden rounded border px-1 py-0.5 font-mono text-xs font-normal sm:inline-block"> ⌘↵ </kbd> </Button> </div> </header> {/* 3-COLUMN STUDIO LAYOUT */} <div className="divide-border grid flex-1 grid-cols-1 divide-y overflow-hidden lg:grid-cols-12 lg:divide-x lg:divide-y-0"> {/* COLUMN 1: SCHEMA DOCUMENTATION SIDEBAR (lg:col-span-3) */} <GraphqlSchemaSidebar onLoadField={loadFieldQuery} /> {/* COLUMN 2: CENTER QUERY & VARIABLES EDITOR (lg:col-span-5) */} <main className="bg-card flex flex-col overflow-hidden lg:col-span-5"> {/* Query Toolbar */} <div className="border-border bg-muted/20 flex items-center justify-between gap-x-2 border-b px-3 py-2"> <div className="flex flex-wrap items-center gap-2"> <FileCode className="text-primary size-4" /> <span className="text-foreground font-mono text-xs font-semibold">Query.graphql</span> <Badge variant="outline" className={cn('font-mono text-xs uppercase', detectedOperation.color)}> {detectedOperation.type} </Badge> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground font-mono text-xs"> {queryLines.length} lines · {activeQuery.length} chars </span> <div className="border-border bg-muted/60 flex items-center rounded-md border p-0.5"> <button className={cn( 'min-h-6 rounded px-2 py-0.5 text-xs font-medium transition-colors', editorMode === 'edit' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setEditorMode('edit')} > Edit </button> <button className={cn( 'min-h-6 rounded px-2 py-0.5 text-xs font-medium transition-colors', editorMode === 'preview' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setEditorMode('preview')} > Preview </button> </div> </div> </div> {/* Query Editor Body with Line Numbers */} <div className="relative flex min-h-[260px] flex-1 overflow-hidden"> {/* Line Numbers Gutter */} <div className="border-border/60 bg-muted/20 text-muted-foreground/50 w-10 shrink-0 overflow-hidden border-r py-3 pr-2 text-right font-mono text-xs leading-relaxed select-none"> {queryLines.map((_, idx) => ( <div key={idx}>{idx + 1}</div> ))} </div> {/* Code Area: Editable or Syntax Preview */} <div className="bg-background/50 flex-1 overflow-auto"> {editorMode === 'edit' ? ( <textarea value={activeQuery} onChange={(e) => setActiveQuery(e.target.value)} spellCheck={false} className="text-foreground h-full w-full resize-none bg-transparent p-3 font-mono text-xs leading-relaxed focus:outline-none" placeholder="# Write GraphQL query or mutation here..." /> ) : ( <div className="space-y-0.5 p-3 font-mono text-xs leading-relaxed"> {queryLines.map((line, idx) => ( <div key={idx} className="whitespace-pre" dangerouslySetInnerHTML={{ __html: highlightGraphqlLine(line) }} /> ))} </div> )} </div> </div> {/* COLLAPSIBLE BOTTOM PANE: VARIABLES / HEADERS */} <div className="border-border bg-muted/10 flex flex-col border-t"> {/* Pane Header */} <div className="border-border bg-muted/30 flex items-center justify-between gap-x-2 border-b px-3 py-1.5 text-xs"> <div className="flex items-center gap-3"> <button className={cn( 'font-mono font-medium transition-colors', activeCenterTab === 'variables' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveCenterTab('variables')} > {'{ }'} Query Variables </button> <span className="text-border">|</span> <button className={cn( 'font-mono font-medium transition-colors', activeCenterTab === 'headers' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveCenterTab('headers')} > HTTP Headers (2) </button> </div> <div className="flex flex-wrap items-center gap-2"> {activeCenterTab === 'variables' && ( <Badge variant="outline" className={cn( 'font-mono text-xs', isVariablesValidJson ? 'border-success/30 text-success' : 'border-destructive/30 text-destructive', )} > {isVariablesValidJson ? 'Valid JSON' : 'Invalid JSON'} </Badge> )} <button className="text-muted-foreground hover:text-foreground min-h-6 p-0.5 transition-colors" title={isVariablesCollapsed ? 'Expand pane' : 'Collapse pane'} onClick={() => setIsVariablesCollapsed(!isVariablesCollapsed)} > {isVariablesCollapsed ? <ChevronUp className="size-3.5" /> : <ChevronDown className="size-3.5" />} </button> </div> </div> {/* Pane Content */} {!isVariablesCollapsed && ( <div className="bg-background/50 relative flex h-36 min-h-[140px] overflow-hidden"> {activeCenterTab === 'variables' ? ( <> <div className="border-border/60 bg-muted/20 text-muted-foreground/50 w-8 shrink-0 overflow-hidden border-r py-2 pr-2 text-right font-mono text-xs leading-relaxed select-none"> {variableLines.map((_, idx) => ( <div key={idx}>{idx + 1}</div> ))} </div> <textarea value={activeVariables} onChange={(e) => setActiveVariables(e.target.value)} spellCheck={false} className="text-foreground h-full w-full resize-none bg-transparent p-2 font-mono text-xs leading-relaxed focus:outline-none" placeholder="{}" /> </> ) : ( <> <div className="border-border/60 bg-muted/20 text-muted-foreground/50 w-8 shrink-0 overflow-hidden border-r py-2 pr-2 text-right font-mono text-xs leading-relaxed select-none"> {headersLines.map((_, idx) => ( <div key={idx}>{idx + 1}</div> ))} </div> <textarea value={activeHeaders} onChange={(e) => setActiveHeaders(e.target.value)} spellCheck={false} className="text-foreground h-full w-full resize-none bg-transparent p-2 font-mono text-xs leading-relaxed focus:outline-none" /> </> )} </div> )} </div> </main> {/* COLUMN 3: RIGHT RESPONSE PANEL (lg:col-span-4) */} <section className="bg-muted/10 flex flex-col overflow-hidden lg:col-span-4"> {/* Response Header */} <div className="border-border bg-muted/30 flex items-center justify-between gap-x-2 border-b px-3 py-2"> <div className="flex flex-wrap items-center gap-2"> <Badge variant="outline" className="border-success/30 bg-success/10 text-success gap-1.5 font-mono text-xs font-semibold" > <span className="bg-success size-1.5 rounded-full" /> {responseStatus} </Badge> <span className="text-muted-foreground flex items-center gap-1 font-mono text-xs"> <Zap className="text-warning size-3" /> {responseLatency} </span> <span className="text-muted-foreground font-mono text-xs">{responseSize}</span> </div> <Button variant="ghost" size="sm" className="h-7 gap-1 px-2 text-xs" onClick={copyResponse}> {copiedResponse ? <Check className="text-success size-3" /> : <Copy className="size-3" />} <span>{copiedResponse ? 'Copied' : 'Copy'}</span> </Button> </div> {/* Response View Tabs */} <div className="border-border bg-muted/20 flex items-center gap-1 border-b px-3 py-1"> <button className={cn( 'min-h-6 rounded px-2 py-1 font-mono text-xs font-medium transition-colors', activeResponseTab === 'response' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveResponseTab('response')} > Response JSON </button> <button className={cn( 'min-h-6 rounded px-2 py-1 font-mono text-xs font-medium transition-colors', activeResponseTab === 'tracing' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveResponseTab('tracing')} > Tracing </button> <button className={cn( 'min-h-6 rounded px-2 py-1 font-mono text-xs font-medium transition-colors', activeResponseTab === 'headers' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setActiveResponseTab('headers')} > Headers </button> </div> {/* Loading Indicator */} {isLoading ? ( <div className="flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center"> <Loader2 className="text-primary size-6 animate-spin" /> <div className="space-y-1"> <p className="text-foreground font-mono text-xs font-medium">Executing GraphQL Operation...</p> <p className="text-muted-foreground font-mono text-xs">{endpointUrl}</p> </div> </div> ) : activeResponseTab === 'response' ? ( /* Response Body: Tab 1 (JSON Response) */ <div className="relative flex flex-1 overflow-auto bg-neutral-950 text-neutral-100 dark:bg-neutral-950"> {/* Gutter */} <div className="w-10 shrink-0 overflow-hidden border-r border-neutral-800 bg-neutral-900/50 py-3 pr-2 text-right font-mono text-xs leading-relaxed text-neutral-500 select-none"> {responseLines.map((_, idx) => ( <div key={idx}>{idx + 1}</div> ))} </div> {/* Colorized JSON Code */} <div className="flex-1 overflow-auto p-3 font-mono text-xs leading-relaxed"> {responseLines.map((line, idx) => ( <div key={idx} className="font-mono whitespace-pre" dangerouslySetInnerHTML={{ __html: highlightJsonLine(line) }} /> ))} </div> </div> ) : activeResponseTab === 'tracing' ? ( <GraphqlTracingPanel latency={responseLatency} /> ) : ( /* Response Body: Tab 3 (HTTP Response Headers) */ <div className="flex-1 space-y-2 overflow-y-auto p-3 font-mono text-xs"> <div className="border-border bg-card space-y-2 rounded-lg border p-3"> <div className="border-border/50 flex justify-between border-b pb-1.5"> <span className="text-muted-foreground">content-type:</span> <span className="text-foreground font-medium">application/graphql-response+json; charset=utf-8</span> </div> <div className="border-border/50 flex justify-between border-b pb-1.5"> <span className="text-muted-foreground">cache-control:</span> <span className="text-foreground font-medium">max-age=0, private, must-revalidate</span> </div> <div className="border-border/50 flex justify-between border-b pb-1.5"> <span className="text-muted-foreground">x-request-id:</span> <span className="text-foreground font-medium">req_01hx8921a9vnm8</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">server-timing:</span> <span className="text-foreground font-medium">graphql;dur=64.1</span> </div> </div> </div> )} </section> </div> </Card> ) } export default GraphqlQueryExplorercomponents/blocks/graphql-query-explorer/GraphqlSchemaSidebar.tsx11.7 kB
'use client' import * as React from 'react' import { ChevronDown, ChevronRight, Database, Search } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { type RootType, type SchemaField } from './graphql-query-explorer-types' const schemaRoots: RootType[] = [ { name: 'Query', color: 'text-info', badgeVariant: 'default', fields: [ { name: 'customer', args: 'id: ID!', returnType: 'Customer', description: 'Fetches verified customer account, active SaaS subscriptions, and verified billing profile.', sampleQuery: `query GetCustomerProfile($id: ID!) {\n customer(id: $id) {\n id\n name\n email\n subscriptions { id status plan }\n }\n}`, sampleVariables: `{\n "id": "cust_8492"\n}`, }, { name: 'users', args: 'limit: Int, offset: Int', returnType: '[User!]!', description: 'Returns paginated workspace members with security roles and activity status flags.', sampleQuery: `query ListUsers($limit: Int, $offset: Int) {\n users(limit: $limit, offset: $offset) {\n id\n fullName\n email\n role\n }\n}`, sampleVariables: `{\n "limit": 10,\n "offset": 0\n}`, }, { name: 'userById', args: 'id: ID!', returnType: 'User', description: 'Look up specific team member by their unique UUID identifier.', sampleQuery: `query GetUser($id: ID!) {\n userById(id: $id) {\n id\n fullName\n email\n role\n isVerified\n }\n}`, sampleVariables: `{\n "id": "usr_9011"\n}`, }, { name: 'products', args: 'category: String, inStock: Boolean', returnType: '[Product!]!', description: 'Catalog items inventory with real-time stock levels and tiered price book.', sampleQuery: `query GetProducts($category: String) {\n products(category: $category) {\n id\n title\n price\n inventoryCount\n }\n}`, sampleVariables: `{\n "category": "developer-tools"\n}`, }, { name: 'orders', args: 'status: OrderStatus, limit: Int', returnType: '[Order!]!', description: 'Historical and in-flight purchase orders with settlement metadata.', sampleQuery: `query GetOrders($status: OrderStatus) {\n orders(status: $status) {\n id\n status\n totalAmount\n createdAt\n }\n}`, sampleVariables: `{\n "status": "COMPLETED"\n}`, }, { name: 'organization', args: 'slug: String!', returnType: 'Organization', description: 'Retrieve tenant workspace configurations, member limits, and billing tier.', sampleQuery: `query GetOrg($slug: String!) {\n organization(slug: $slug) {\n id\n name\n slug\n memberCount\n }\n}`, sampleVariables: `{\n "slug": "acme-corp"\n}`, }, ], }, { name: 'Mutation', color: 'text-warning', badgeVariant: 'secondary', fields: [ { name: 'createOrder', args: 'input: CreateOrderInput!', returnType: 'OrderPayload!', description: 'Creates new purchase order, holds inventory reservations, and emits payment intent.', sampleQuery: `mutation CreateOrder($input: CreateOrderInput!) {\n createOrder(input: $input) {\n order { id status totalAmount }\n clientSecret\n }\n}`, sampleVariables: `{\n "input": {\n "customerId": "cust_8492",\n "items": [{ "sku": "SKU_01", "quantity": 1, "price": 149.00 }]\n }\n}`, }, { name: 'updateCustomer', args: 'id: ID!, input: CustomerInput!', returnType: 'Customer!', description: 'Updates customer profile attributes and notification preferences.', sampleQuery: `mutation UpdateCustomer($id: ID!, $input: CustomerInput!) {\n updateCustomer(id: $id, input: $input) {\n id\n name\n email\n }\n}`, sampleVariables: `{\n "id": "cust_8492",\n "input": { "name": "Sarah Jenkins" }\n}`, }, { name: 'cancelSubscription', args: 'id: ID!, reason: String', returnType: 'SubscriptionResult!', description: 'Immediately pauses or schedules cancellation for recurring billing plan.', sampleQuery: `mutation CancelSub($id: ID!, $reason: String) {\n cancelSubscription(id: $id, reason: $reason) {\n id\n status\n effectiveDate\n }\n}`, sampleVariables: `{\n "id": "sub_4410",\n "reason": "Upgraded to Enterprise annual"\n}`, }, { name: 'rotateApiKey', args: 'keyId: ID!', returnType: 'ApiKeyRotationPayload!', description: 'Invalidates existing API secret and provisions new machine token.', sampleQuery: `mutation RotateKey($keyId: ID!) {\n rotateApiKey(keyId: $keyId) {\n keyId\n secretToken\n expiresAt\n }\n}`, sampleVariables: `{\n "keyId": "key_live_9941"\n}`, }, ], }, { name: 'Subscription', color: 'text-chart-1', badgeVariant: 'outline', fields: [ { name: 'orderStatusUpdated', args: 'orderId: ID!', returnType: 'OrderStatusEvent!', description: 'Live push stream delivering order state transitions and logistics milestones.', sampleQuery: `subscription WatchOrder($orderId: ID!) {\n orderStatusUpdated(orderId: $orderId) {\n orderId\n newStatus\n updatedAt\n }\n}`, sampleVariables: `{\n "orderId": "ord_88201"\n}`, }, { name: 'userActivityStream', args: 'channelId: ID!', returnType: 'ActivityEvent!', description: 'Workspace real-time collaboration telemetry and presence updates.', sampleQuery: `subscription ActivityStream($channelId: ID!) {\n userActivityStream(channelId: $channelId) {\n userId\n action\n timestamp\n }\n}`, sampleVariables: `{\n "channelId": "chan_engineering"\n}`, }, ], }, ] export function GraphqlSchemaSidebar({ onLoadField }: { onLoadField: (field: SchemaField) => void }) { const [schemaSearch, setSchemaSearch] = React.useState('') const [selectedField, setSelectedField] = React.useState<SchemaField | null>(null) const [expandedRoots, setExpandedRoots] = React.useState<Record<string, boolean>>({ Query: true, Mutation: true, Subscription: true, }) const filteredSchemaRoots = React.useMemo(() => { const query = schemaSearch.trim().toLowerCase() if (!query) return schemaRoots return schemaRoots .map((root) => { const matchingFields = root.fields.filter( (f) => f.name.toLowerCase().includes(query) || f.returnType.toLowerCase().includes(query) || (f.args && f.args.toLowerCase().includes(query)) || f.description.toLowerCase().includes(query), ) return { ...root, fields: matchingFields, } }) .filter((root) => root.fields.length > 0) }, [schemaSearch]) const toggleRoot = (name: string) => { setExpandedRoots((prev) => ({ ...prev, [name]: !prev[name] })) } const handleSelectField = (field: SchemaField) => { setSelectedField(field) onLoadField(field) } return ( <> {/* COLUMN 1: SCHEMA DOCUMENTATION SIDEBAR (lg:col-span-3) */} <aside className="bg-muted/15 flex flex-col overflow-hidden lg:col-span-3"> {/* Sidebar Header */} <div className="border-border bg-muted/30 flex items-center justify-between gap-x-2 border-b px-3 py-2.5"> <div className="flex items-center gap-1.5 text-xs font-semibold"> <Database className="text-primary size-3.5" /> <span>Schema Explorer</span> </div> <Badge variant="outline" className="font-mono text-xs"> v2.4 </Badge> </div> {/* Search Types Input */} <div className="border-border border-b p-2.5"> <div className="relative"> <Search className="text-muted-foreground absolute top-2.5 left-2.5 size-3.5" /> <Input value={schemaSearch} onChange={(e) => setSchemaSearch(e.target.value)} type="text" placeholder="Search types & fields..." className="h-8 pl-8 font-mono text-xs" /> </div> </div> {/* Root Types & Field Tree */} <div className="flex-1 space-y-3 overflow-y-auto p-2.5"> {filteredSchemaRoots.map((root) => ( <div key={root.name} className="space-y-1"> {/* Root Type Toggle */} <button className="hover:bg-muted/60 flex w-full items-center justify-between rounded-md px-2 py-1.5 text-left text-xs font-semibold transition-colors" onClick={() => toggleRoot(root.name)} > <div className="flex items-center gap-1.5"> {expandedRoots[root.name] ? ( <ChevronDown className="text-muted-foreground size-3.5" /> ) : ( <ChevronRight className="text-muted-foreground size-3.5" /> )} <span className={root.color}>{root.name}</span> </div> <Badge variant="secondary" className="font-mono text-xs font-normal"> {root.fields.length} </Badge> </button> {/* Fields List */} {expandedRoots[root.name] && ( <div className="border-border/70 ml-2 space-y-0.5 border-l pl-2"> {root.fields.map((field) => ( <button key={field.name} className={cn( 'hover:bg-muted focus:bg-muted group flex w-full flex-col gap-0.5 rounded px-2 py-1.5 text-left transition-colors', selectedField?.name === field.name && 'bg-muted/90 ring-border ring-1', )} onClick={() => handleSelectField(field)} > <div className="flex items-center justify-between gap-x-2"> <span className="text-foreground group-hover:text-primary font-mono text-xs font-medium transition-colors"> {field.name} </span> <span className="text-muted-foreground font-mono text-xs">{field.returnType}</span> </div> {field.args && ( <div className="text-muted-foreground truncate font-mono text-xs">({field.args})</div> )} </button> ))} </div> )} </div> ))} </div> {/* Selected Field Documentation Preview */} {selectedField && ( <div className="border-border bg-card/60 border-t p-3 text-xs"> <div className="flex items-center justify-between gap-x-2"> <span className="text-foreground font-mono font-semibold">{selectedField.name}</span> <Badge variant="secondary" className="font-mono text-xs"> {selectedField.returnType} </Badge> </div> <p className="text-muted-foreground mt-1.5 text-xs leading-normal">{selectedField.description}</p> {selectedField.args && ( <div className="border-border bg-muted/40 mt-2 rounded border p-1.5 font-mono text-xs"> <span className="text-muted-foreground">Args: </span> <span className="text-foreground font-medium">{selectedField.args}</span> </div> )} </div> )} </aside> </> ) }components/blocks/graphql-query-explorer/GraphqlTracingPanel.tsx2.5 kB
'use client' import { Badge } from '@/components/ui/badge' import { type ResolverTrace } from './graphql-query-explorer-types' const traces: ResolverTrace[] = [ { path: 'Query.customer', parentType: 'Query', fieldName: 'customer', returnType: 'Customer', durationMs: 48.2, percentage: 75, }, { path: 'Customer.subscriptions', parentType: 'Customer', fieldName: 'subscriptions', returnType: '[Subscription!]!', durationMs: 14.9, percentage: 23, }, { path: 'Customer.billingAddress', parentType: 'Customer', fieldName: 'billingAddress', returnType: 'BillingAddress', durationMs: 1.0, percentage: 2, }, ] export function GraphqlTracingPanel({ latency }: { latency: string }) { return ( <> /* Response Body: Tab 2 (Tracing Waterfall) */ <div className="flex-1 space-y-3 overflow-y-auto p-4"> <div className="border-border bg-card flex items-center justify-between gap-x-2 rounded-lg border p-3"> <div> <span className="text-muted-foreground text-xs">Total Duration</span> <p className="text-foreground font-mono text-base font-bold">{latency}</p> </div> <Badge variant="secondary" className="font-mono text-xs"> Apollo Tracing v1 </Badge> </div> <div className="space-y-2"> <span className="text-muted-foreground font-mono text-xs font-medium">Resolver Execution Times</span> <div className="space-y-2"> {traces.map((trace) => ( <div key={trace.path} className="border-border bg-card space-y-1.5 rounded-lg border p-2.5 text-xs"> <div className="flex items-center justify-between gap-x-2"> <span className="text-foreground font-mono font-semibold">{trace.path}</span> <span className="text-muted-foreground font-mono">{trace.durationMs}ms</span> </div> <div className="bg-muted h-1.5 w-full overflow-hidden rounded-full"> <div className="bg-primary h-full rounded-full" style={{ width: `${trace.percentage}%` }} /> </div> <div className="text-muted-foreground flex justify-between font-mono text-xs"> <span>Type: {trace.returnType}</span> <span>{trace.percentage}% of query</span> </div> </div> ))} </div> </div> </div> </> ) }components/blocks/graphql-query-explorer/graphql-query-explorer-types.ts0.7 kB
export interface SchemaField { name: string args?: string returnType: string description: string sampleQuery: string sampleVariables: string } export interface RootType { name: 'Query' | 'Mutation' | 'Subscription' color: string badgeVariant: 'default' | 'secondary' | 'outline' fields: SchemaField[] } export interface QueryPreset { id: string name: string operationType: 'query' | 'mutation' | 'subscription' query: string variables: string response: Record<string, unknown> latency: string size: string timestamp: string } export interface ResolverTrace { path: string parentType: string fieldName: string returnType: string durationMs: number percentage: number }
Raw manifest:https://uipkge.dev/r/react/graphql-query-explorer.json