@djangocfg/ui-tools 2.1.287 → 2.1.290
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/README.md +14 -3
- package/dist/DocsLayout-IKH7BLSU.cjs +3464 -0
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/DocsLayout-JPXFUKAR.mjs +3457 -0
- package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
- package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
- package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
- package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
- package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
- package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
- package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
- package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
- package/dist/chunk-EFWOJPA6.mjs.map +1 -0
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -14
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +93 -157
- package/src/tools/OpenapiViewer/README.md +114 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +331 -53
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +40 -11
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +19 -2
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +38 -21
- package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +168 -50
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +3 -1
- package/src/tools/OpenapiViewer/hooks/useDocsUrlSync.ts +119 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +164 -74
- package/src/tools/OpenapiViewer/types.ts +46 -1
- package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
- package/src/tools/OpenapiViewer/utils/index.ts +3 -0
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
- package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
- package/src/tools/OpenapiViewer/utils/scrollParent.ts +68 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
- package/src/tools/PrettyCode/index.tsx +13 -0
- package/src/tools/PrettyCode/lazy.tsx +5 -0
- package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
- package/dist/DocsLayout-BCVU6TTX.cjs +0 -2027
- package/dist/DocsLayout-BCVU6TTX.cjs.map +0 -1
- package/dist/DocsLayout-ERETJLLV.mjs +0 -2020
- package/dist/DocsLayout-ERETJLLV.mjs.map +0 -1
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
- package/dist/chunk-IULI4XII.cjs.map +0 -1
- package/dist/chunk-VZGQC3NG.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -268
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -211
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Search } from 'lucide-react';
|
|
4
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Combobox,
|
|
8
|
-
Input,
|
|
9
|
-
Tooltip,
|
|
10
|
-
TooltipContent,
|
|
11
|
-
TooltipTrigger,
|
|
12
|
-
} from '@djangocfg/ui-core/components';
|
|
13
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
|
-
|
|
15
|
-
import type { ApiEndpoint, OpenApiInfo, SchemaSource } from '../../types';
|
|
16
|
-
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
17
|
-
import { MethodBadge, ScrollArea } from '../shared/ui';
|
|
18
|
-
import { endpointAnchor } from './anchor';
|
|
19
|
-
import { longestCommonPrefix, sidebarLabel, sidebarTooltip } from './sidebarLabel';
|
|
20
|
-
|
|
21
|
-
type Group = {
|
|
22
|
-
category: string;
|
|
23
|
-
endpoints: ApiEndpoint[];
|
|
24
|
-
/** Longest ``/``-aligned prefix shared by every endpoint in this group.
|
|
25
|
-
* Stripped from labels that fall back to showing the path. */
|
|
26
|
-
commonPrefix: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const METHOD_ORDER: Record<string, number> = {
|
|
30
|
-
GET: 0, POST: 1, PUT: 2, PATCH: 3, DELETE: 4,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function groupEndpoints(list: ApiEndpoint[]): Group[] {
|
|
34
|
-
const map = new Map<string, ApiEndpoint[]>();
|
|
35
|
-
for (const ep of list) {
|
|
36
|
-
const arr = map.get(ep.category) ?? [];
|
|
37
|
-
arr.push(ep);
|
|
38
|
-
map.set(ep.category, arr);
|
|
39
|
-
}
|
|
40
|
-
const groups: Group[] = Array.from(map.entries()).map(([category, endpoints]) => ({
|
|
41
|
-
category,
|
|
42
|
-
endpoints: [...endpoints].sort((a, b) => {
|
|
43
|
-
const byPath = a.path.localeCompare(b.path);
|
|
44
|
-
if (byPath !== 0) return byPath;
|
|
45
|
-
return (METHOD_ORDER[a.method] ?? 99) - (METHOD_ORDER[b.method] ?? 99);
|
|
46
|
-
}),
|
|
47
|
-
commonPrefix: longestCommonPrefix(endpoints.map((e) => e.path)),
|
|
48
|
-
}));
|
|
49
|
-
// Alphabetical, but "Other" sinks to the bottom.
|
|
50
|
-
groups.sort((a, b) => {
|
|
51
|
-
if (a.category === 'Other') return 1;
|
|
52
|
-
if (b.category === 'Other') return -1;
|
|
53
|
-
return a.category.localeCompare(b.category);
|
|
54
|
-
});
|
|
55
|
-
return groups;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface DocsSidebarProps {
|
|
59
|
-
info: OpenApiInfo | null;
|
|
60
|
-
endpoints: ApiEndpoint[];
|
|
61
|
-
schemas: SchemaSource[];
|
|
62
|
-
currentSchemaId: string | null;
|
|
63
|
-
onSchemaChange: (id: string) => void;
|
|
64
|
-
activeEndpointId: string | null;
|
|
65
|
-
selectedVersion: string;
|
|
66
|
-
onNavigate: (anchor: string) => void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function DocsSidebar({
|
|
70
|
-
info,
|
|
71
|
-
endpoints,
|
|
72
|
-
schemas,
|
|
73
|
-
currentSchemaId,
|
|
74
|
-
onSchemaChange,
|
|
75
|
-
activeEndpointId,
|
|
76
|
-
selectedVersion,
|
|
77
|
-
onNavigate,
|
|
78
|
-
}: DocsSidebarProps) {
|
|
79
|
-
const [search, setSearch] = useState('');
|
|
80
|
-
const [debounced, setDebounced] = useState('');
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
const id = setTimeout(() => setDebounced(search), 120);
|
|
84
|
-
return () => clearTimeout(id);
|
|
85
|
-
}, [search]);
|
|
86
|
-
|
|
87
|
-
const filteredGroups = useMemo(() => {
|
|
88
|
-
let list = deduplicateEndpoints(endpoints, selectedVersion);
|
|
89
|
-
if (debounced) {
|
|
90
|
-
const q = debounced.toLowerCase();
|
|
91
|
-
list = list.filter((e) =>
|
|
92
|
-
e.summary.toLowerCase().includes(q) ||
|
|
93
|
-
e.name.toLowerCase().includes(q) ||
|
|
94
|
-
e.description.toLowerCase().includes(q) ||
|
|
95
|
-
e.path.toLowerCase().includes(q),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return groupEndpoints(list);
|
|
99
|
-
}, [endpoints, debounced, selectedVersion]);
|
|
100
|
-
|
|
101
|
-
const schemaOptions = useMemo(
|
|
102
|
-
() => schemas.map((s) => ({ value: s.id, label: s.name })),
|
|
103
|
-
[schemas],
|
|
104
|
-
);
|
|
105
|
-
const hasMultipleSchemas = schemas.length > 1;
|
|
106
|
-
const apiTitle = info?.title ?? 'API Reference';
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<aside className="flex flex-col min-h-0 border-r bg-muted/10">
|
|
110
|
-
{/* Brand row */}
|
|
111
|
-
<div className="shrink-0 border-b px-4 h-12 flex items-center gap-2">
|
|
112
|
-
<span className="text-[13px] font-semibold text-foreground truncate">
|
|
113
|
-
{apiTitle}
|
|
114
|
-
</span>
|
|
115
|
-
{info?.version && (
|
|
116
|
-
<span className="font-mono text-[10px] text-muted-foreground/70 shrink-0">
|
|
117
|
-
v{info.version}
|
|
118
|
-
</span>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{/* Controls */}
|
|
123
|
-
<div className="shrink-0 border-b px-3 py-3 space-y-2">
|
|
124
|
-
{hasMultipleSchemas && (
|
|
125
|
-
<Combobox
|
|
126
|
-
options={schemaOptions}
|
|
127
|
-
value={currentSchemaId ?? ''}
|
|
128
|
-
onValueChange={(id) => id && onSchemaChange(id)}
|
|
129
|
-
placeholder="Select API"
|
|
130
|
-
searchPlaceholder="Search APIs…"
|
|
131
|
-
emptyText="No APIs found"
|
|
132
|
-
className="w-full h-8 text-xs"
|
|
133
|
-
/>
|
|
134
|
-
)}
|
|
135
|
-
<div className="relative">
|
|
136
|
-
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" />
|
|
137
|
-
<Input
|
|
138
|
-
placeholder="Search endpoints…"
|
|
139
|
-
value={search}
|
|
140
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
|
|
141
|
-
className="pl-8 h-8 text-xs"
|
|
142
|
-
/>
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<ScrollArea>
|
|
147
|
-
{filteredGroups.length === 0 ? (
|
|
148
|
-
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
149
|
-
{debounced
|
|
150
|
-
? `No endpoints match "${debounced}"`
|
|
151
|
-
: 'No endpoints in this schema'}
|
|
152
|
-
</div>
|
|
153
|
-
) : (
|
|
154
|
-
<nav className="py-2">
|
|
155
|
-
{filteredGroups.map((group) => (
|
|
156
|
-
<div key={group.category} className="mb-4 last:mb-2">
|
|
157
|
-
<div className="px-4 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none">
|
|
158
|
-
{group.category}
|
|
159
|
-
</div>
|
|
160
|
-
<div>
|
|
161
|
-
{group.endpoints.map((ep) => {
|
|
162
|
-
const anchor = endpointAnchor(ep);
|
|
163
|
-
const isActive = activeEndpointId === anchor;
|
|
164
|
-
const label = sidebarLabel(ep, group.commonPrefix);
|
|
165
|
-
const tooltip = sidebarTooltip(ep);
|
|
166
|
-
// Summary → sans-serif (reads like an outline).
|
|
167
|
-
// Path-tail fallback → mono (reads like code).
|
|
168
|
-
const useMono = !ep.summary;
|
|
169
|
-
return (
|
|
170
|
-
<Tooltip key={`${ep.method}-${ep.path}`} delayDuration={350}>
|
|
171
|
-
<TooltipTrigger asChild>
|
|
172
|
-
<button
|
|
173
|
-
onClick={() => onNavigate(anchor)}
|
|
174
|
-
aria-current={isActive ? 'location' : undefined}
|
|
175
|
-
className={cn(
|
|
176
|
-
'relative group w-full text-left flex items-center gap-2 pl-4 pr-3 py-1.5 transition-colors',
|
|
177
|
-
isActive
|
|
178
|
-
? 'bg-primary/10 text-foreground'
|
|
179
|
-
: 'hover:bg-muted/40 text-foreground/75 hover:text-foreground',
|
|
180
|
-
)}
|
|
181
|
-
>
|
|
182
|
-
{isActive && (
|
|
183
|
-
<span className="absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" />
|
|
184
|
-
)}
|
|
185
|
-
<MethodBadge method={ep.method} />
|
|
186
|
-
<span
|
|
187
|
-
className={cn(
|
|
188
|
-
'truncate leading-tight flex-1 min-w-0',
|
|
189
|
-
useMono ? 'font-mono text-[11px]' : 'text-[12px]',
|
|
190
|
-
isActive && 'text-foreground font-medium',
|
|
191
|
-
)}
|
|
192
|
-
>
|
|
193
|
-
{label}
|
|
194
|
-
</span>
|
|
195
|
-
</button>
|
|
196
|
-
</TooltipTrigger>
|
|
197
|
-
<TooltipContent side="right" align="center" className="font-mono text-[11px]">
|
|
198
|
-
{tooltip}
|
|
199
|
-
</TooltipContent>
|
|
200
|
-
</Tooltip>
|
|
201
|
-
);
|
|
202
|
-
})}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
))}
|
|
206
|
-
</nav>
|
|
207
|
-
)}
|
|
208
|
-
</ScrollArea>
|
|
209
|
-
</aside>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Loader2, Send, Terminal, WifiOff } from 'lucide-react';
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
|
-
|
|
6
|
-
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
import JsonTree from '../../../JsonTree';
|
|
9
|
-
import { usePlaygroundContext } from '../../context/PlaygroundContext';
|
|
10
|
-
import { EmptyState, ScrollArea, StatusBadge } from './ui';
|
|
11
|
-
|
|
12
|
-
// ─── JsonTree config (static, no re-creation on render) ──────────────────────
|
|
13
|
-
|
|
14
|
-
const JSON_TREE_CONFIG = {
|
|
15
|
-
maxAutoExpandDepth: 2,
|
|
16
|
-
maxAutoExpandArrayItems: 10,
|
|
17
|
-
maxAutoExpandObjectKeys: 5,
|
|
18
|
-
maxStringLength: 200,
|
|
19
|
-
collectionLimit: 50,
|
|
20
|
-
showCollectionInfo: true,
|
|
21
|
-
showExpandControls: true,
|
|
22
|
-
showActionButtons: false,
|
|
23
|
-
preserveKeyOrder: true,
|
|
24
|
-
className: 'border-0 rounded-none',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
// ─── ResponsePanel ────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function ResponsePanel() {
|
|
30
|
-
const { state } = usePlaygroundContext();
|
|
31
|
-
const { response, loading, selectedEndpoint } = state;
|
|
32
|
-
|
|
33
|
-
// ── Normalise response data ───────────────────────────────────────────────
|
|
34
|
-
// Always try to parse as JSON first so JsonTree gets an object, not a string.
|
|
35
|
-
// Falls back to raw text for non-JSON responses (HTML errors, plain text, etc.)
|
|
36
|
-
const { treeData, rawText } = useMemo(() => {
|
|
37
|
-
const d = response?.data;
|
|
38
|
-
if (d == null) return { treeData: null, rawText: '' };
|
|
39
|
-
|
|
40
|
-
if (typeof d === 'string') {
|
|
41
|
-
try {
|
|
42
|
-
return { treeData: JSON.parse(d), rawText: d };
|
|
43
|
-
} catch {
|
|
44
|
-
return { treeData: null, rawText: d };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { treeData: d, rawText: JSON.stringify(d, null, 2) };
|
|
49
|
-
}, [response?.data]);
|
|
50
|
-
|
|
51
|
-
// ── Derived ───────────────────────────────────────────────────────────────
|
|
52
|
-
const sizeKb = rawText ? `${(rawText.length / 1024).toFixed(1)} KB` : '';
|
|
53
|
-
const duration = response?.duration != null ? `${response.duration}ms` : '';
|
|
54
|
-
const hasError = Boolean(response?.error);
|
|
55
|
-
const hasStatus = response?.status != null;
|
|
56
|
-
const hasCopy = Boolean(rawText);
|
|
57
|
-
|
|
58
|
-
// ── Early returns ─────────────────────────────────────────────────────────
|
|
59
|
-
if (loading) {
|
|
60
|
-
return (
|
|
61
|
-
<div className="flex items-center justify-center h-full gap-2">
|
|
62
|
-
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
63
|
-
<span className="text-xs text-muted-foreground">Sending…</span>
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!selectedEndpoint) return <EmptyState icon={Terminal} text="Response will appear here" />;
|
|
69
|
-
if (!response) return <EmptyState icon={Send} text='Press "Send Request" to see the response' />;
|
|
70
|
-
|
|
71
|
-
// Pure network error (no HTTP response at all — CORS, offline, timeout)
|
|
72
|
-
if (hasError && !hasStatus) {
|
|
73
|
-
return (
|
|
74
|
-
<EmptyState
|
|
75
|
-
icon={WifiOff}
|
|
76
|
-
text={response.error!}
|
|
77
|
-
className="text-destructive [&_svg]:text-destructive"
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ── Render ────────────────────────────────────────────────────────────────
|
|
83
|
-
return (
|
|
84
|
-
<>
|
|
85
|
-
{/* Status bar */}
|
|
86
|
-
<div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-3 bg-muted/20">
|
|
87
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
88
|
-
{hasStatus && <StatusBadge status={response.status!} />}
|
|
89
|
-
{response.statusText && (
|
|
90
|
-
<span className="text-xs text-muted-foreground truncate">{response.statusText}</span>
|
|
91
|
-
)}
|
|
92
|
-
{sizeKb && (
|
|
93
|
-
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{sizeKb}</span>
|
|
94
|
-
)}
|
|
95
|
-
{duration && (
|
|
96
|
-
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{duration}</span>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
{hasCopy && (
|
|
100
|
-
<CopyButton value={rawText} variant="ghost" size="sm" className="h-6 px-2 text-[10px] text-muted-foreground shrink-0">
|
|
101
|
-
Copy
|
|
102
|
-
</CopyButton>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
{/* HTTP-level error body (4xx/5xx — has status but also error flag) */}
|
|
107
|
-
{hasError && (
|
|
108
|
-
<div className="shrink-0 mx-4 mt-3 rounded border border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
109
|
-
<p className="text-xs text-destructive">{response.error}</p>
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{/* Body */}
|
|
114
|
-
<ScrollArea>
|
|
115
|
-
{treeData != null ? (
|
|
116
|
-
<JsonTree title="Response Body" data={treeData} config={JSON_TREE_CONFIG} />
|
|
117
|
-
) : rawText ? (
|
|
118
|
-
<pre className="p-4 text-[11px] font-mono text-foreground/70 whitespace-pre-wrap break-all leading-relaxed">
|
|
119
|
-
{rawText}
|
|
120
|
-
</pre>
|
|
121
|
-
) : (
|
|
122
|
-
<div className="py-10 text-center text-xs text-muted-foreground">Empty response body</div>
|
|
123
|
-
)}
|
|
124
|
-
</ScrollArea>
|
|
125
|
-
</>
|
|
126
|
-
);
|
|
127
|
-
}
|