@djangocfg/ui-tools 2.1.268 → 2.1.270
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PlaygroundLayout-FRKIMYVN.mjs +684 -0
- package/dist/PlaygroundLayout-FRKIMYVN.mjs.map +1 -0
- package/dist/PlaygroundLayout-LIAN63CZ.cjs +691 -0
- package/dist/PlaygroundLayout-LIAN63CZ.cjs.map +1 -0
- package/dist/{PrettyCode.client-OO3KAJSM.mjs → PrettyCode.client-DW5LTG47.mjs} +5 -5
- package/dist/PrettyCode.client-DW5LTG47.mjs.map +1 -0
- package/dist/{PrettyCode.client-V2ZN5DTH.cjs → PrettyCode.client-SGDGQTYT.cjs} +5 -5
- package/dist/PrettyCode.client-SGDGQTYT.cjs.map +1 -0
- package/dist/{chunk-SZ2CZEQZ.mjs → chunk-FX3GCEUL.mjs} +5 -26
- package/dist/chunk-FX3GCEUL.mjs.map +1 -0
- package/dist/{chunk-CRHHUOVJ.cjs → chunk-VAL2LCQD.cjs} +4 -27
- package/dist/chunk-VAL2LCQD.cjs.map +1 -0
- package/dist/index.cjs +8 -8
- package/dist/index.mjs +5 -5
- package/package.json +6 -6
- package/src/tools/OpenapiViewer/README.md +121 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/EndpointList.tsx +221 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/RequestPanel.tsx +231 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/ResponsePanel.tsx +112 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/index.tsx +107 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/ui.tsx +137 -0
- package/src/tools/OpenapiViewer/components/index.ts +0 -9
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +1 -1
- package/src/tools/PrettyCode/PrettyCode.client.tsx +17 -12
- package/dist/PlaygroundLayout-FKXSULJ3.cjs +0 -971
- package/dist/PlaygroundLayout-FKXSULJ3.cjs.map +0 -1
- package/dist/PlaygroundLayout-XMMHPZYP.mjs +0 -964
- package/dist/PlaygroundLayout-XMMHPZYP.mjs.map +0 -1
- package/dist/PrettyCode.client-OO3KAJSM.mjs.map +0 -1
- package/dist/PrettyCode.client-V2ZN5DTH.cjs.map +0 -1
- package/dist/chunk-CRHHUOVJ.cjs.map +0 -1
- package/dist/chunk-SZ2CZEQZ.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +0 -149
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +0 -278
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +0 -91
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +0 -100
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +0 -157
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +0 -253
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +0 -173
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +0 -68
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { AlertCircle, ChevronDown, Code, Database, FileText } from 'lucide-react';
|
|
4
|
-
import React, { useMemo } from 'react';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Badge, Card, CardContent, CardHeader, CardTitle, Collapsible, CollapsibleContent,
|
|
8
|
-
CollapsibleTrigger, CopyButton
|
|
9
|
-
} from '@djangocfg/ui-core/components';
|
|
10
|
-
|
|
11
|
-
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
12
|
-
import { getMethodColor, getStatusColor } from '../utils';
|
|
13
|
-
|
|
14
|
-
export const EndpointInfo: React.FC = () => {
|
|
15
|
-
const { state } = usePlaygroundContext();
|
|
16
|
-
const { selectedEndpoint } = state;
|
|
17
|
-
|
|
18
|
-
// Memoize endpoint JSON for copy
|
|
19
|
-
const endpointJson = useMemo(() => {
|
|
20
|
-
if (!selectedEndpoint) return '';
|
|
21
|
-
return JSON.stringify({
|
|
22
|
-
name: selectedEndpoint.name,
|
|
23
|
-
method: selectedEndpoint.method,
|
|
24
|
-
path: selectedEndpoint.path,
|
|
25
|
-
description: selectedEndpoint.description,
|
|
26
|
-
parameters: selectedEndpoint.parameters,
|
|
27
|
-
requestBody: selectedEndpoint.requestBody,
|
|
28
|
-
responses: selectedEndpoint.responses
|
|
29
|
-
}, null, 2);
|
|
30
|
-
}, [selectedEndpoint]);
|
|
31
|
-
|
|
32
|
-
if (!selectedEndpoint) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const getMethodBadges = (methods: string) => {
|
|
37
|
-
return methods.split(', ').map((method) => (
|
|
38
|
-
<Badge key={method} variant={getMethodColor(method) === 'success' ? 'default' : 'secondary'} className="text-xs">
|
|
39
|
-
{method}
|
|
40
|
-
</Badge>
|
|
41
|
-
));
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-4">
|
|
46
|
-
<div className="flex items-center justify-between">
|
|
47
|
-
<h2 className="text-lg font-semibold text-foreground">Selected Endpoint</h2>
|
|
48
|
-
<CopyButton value={endpointJson} variant="outline" size="sm">
|
|
49
|
-
Copy
|
|
50
|
-
</CopyButton>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<Card>
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle className="flex items-center justify-between text-sm text-foreground">
|
|
56
|
-
<div className="flex items-center space-x-2">
|
|
57
|
-
<Code className="h-4 w-4" />
|
|
58
|
-
<span className="font-medium">{selectedEndpoint.name}</span>
|
|
59
|
-
</div>
|
|
60
|
-
<div className="flex space-x-1">{getMethodBadges(selectedEndpoint.method)}</div>
|
|
61
|
-
</CardTitle>
|
|
62
|
-
</CardHeader>
|
|
63
|
-
<CardContent className="space-y-4">
|
|
64
|
-
<div>
|
|
65
|
-
<p className="text-xs font-mono text-muted-foreground break-all">
|
|
66
|
-
{selectedEndpoint.path}
|
|
67
|
-
</p>
|
|
68
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
69
|
-
{selectedEndpoint.description}
|
|
70
|
-
</p>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{/* Parameters */}
|
|
74
|
-
{selectedEndpoint.parameters && selectedEndpoint.parameters.length > 0 && (
|
|
75
|
-
<Collapsible>
|
|
76
|
-
<CollapsibleTrigger className="flex items-center space-x-2 text-sm font-medium text-foreground">
|
|
77
|
-
<Database className="h-4 w-4" />
|
|
78
|
-
<span>Parameters ({selectedEndpoint.parameters.length})</span>
|
|
79
|
-
<ChevronDown className="h-4 w-4" />
|
|
80
|
-
</CollapsibleTrigger>
|
|
81
|
-
<CollapsibleContent className="mt-2 space-y-2">
|
|
82
|
-
{selectedEndpoint.parameters.map((param, index) => (
|
|
83
|
-
<div key={index} className="flex items-center space-x-2 text-xs">
|
|
84
|
-
<Badge variant={param.required ? 'destructive' : 'secondary'} className="text-xs">
|
|
85
|
-
{param.required ? 'Required' : 'Optional'}
|
|
86
|
-
</Badge>
|
|
87
|
-
<span className="font-mono text-muted-foreground">
|
|
88
|
-
{param.name}: {param.type}
|
|
89
|
-
</span>
|
|
90
|
-
{param.description && (
|
|
91
|
-
<span className="text-muted-foreground">- {param.description}</span>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
))}
|
|
95
|
-
</CollapsibleContent>
|
|
96
|
-
</Collapsible>
|
|
97
|
-
)}
|
|
98
|
-
|
|
99
|
-
{/* Request Body */}
|
|
100
|
-
{selectedEndpoint.requestBody && (
|
|
101
|
-
<Collapsible>
|
|
102
|
-
<CollapsibleTrigger className="flex items-center space-x-2 text-sm font-medium text-foreground">
|
|
103
|
-
<FileText className="h-4 w-4" />
|
|
104
|
-
<span>Request Body</span>
|
|
105
|
-
<ChevronDown className="h-4 w-4" />
|
|
106
|
-
</CollapsibleTrigger>
|
|
107
|
-
<CollapsibleContent className="mt-2">
|
|
108
|
-
<div className="text-xs text-muted-foreground">
|
|
109
|
-
<p>Type: {selectedEndpoint.requestBody.type}</p>
|
|
110
|
-
{selectedEndpoint.requestBody.description && (
|
|
111
|
-
<p className="mt-1">{selectedEndpoint.requestBody.description}</p>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
</CollapsibleContent>
|
|
115
|
-
</Collapsible>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
{/* Responses */}
|
|
119
|
-
{selectedEndpoint.responses && selectedEndpoint.responses.length > 0 && (
|
|
120
|
-
<Collapsible>
|
|
121
|
-
<CollapsibleTrigger className="flex items-center space-x-2 text-sm font-medium text-foreground">
|
|
122
|
-
<AlertCircle className="h-4 w-4" />
|
|
123
|
-
<span>Responses ({selectedEndpoint.responses.length})</span>
|
|
124
|
-
<ChevronDown className="h-4 w-4" />
|
|
125
|
-
</CollapsibleTrigger>
|
|
126
|
-
<CollapsibleContent className="mt-2 space-y-2">
|
|
127
|
-
{selectedEndpoint.responses.map((response, index) => {
|
|
128
|
-
const statusColor = getStatusColor(parseInt(response.code));
|
|
129
|
-
const badgeVariant = statusColor === 'success' ? 'default' :
|
|
130
|
-
statusColor === 'error' ? 'destructive' : 'secondary';
|
|
131
|
-
return (
|
|
132
|
-
<div key={index} className="flex items-center space-x-2 text-xs">
|
|
133
|
-
<Badge variant={badgeVariant} className="text-xs">
|
|
134
|
-
{response.code}
|
|
135
|
-
</Badge>
|
|
136
|
-
<span className="text-muted-foreground">
|
|
137
|
-
{response.description}
|
|
138
|
-
</span>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
142
|
-
</CollapsibleContent>
|
|
143
|
-
</Collapsible>
|
|
144
|
-
)}
|
|
145
|
-
</CardContent>
|
|
146
|
-
</Card>
|
|
147
|
-
</div>
|
|
148
|
-
);
|
|
149
|
-
};
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Code, Filter, Grid3X3, List, Search,
|
|
5
|
-
} from 'lucide-react';
|
|
6
|
-
import React, { useMemo } from 'react';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
Badge, Button, Card, CardContent, CardHeader, CardTitle, Combobox, DownloadButton, Input,
|
|
10
|
-
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, Table, TableBody,
|
|
11
|
-
TableCell, TableHead, TableHeader, TableRow
|
|
12
|
-
} from '@djangocfg/ui-core/components';
|
|
13
|
-
|
|
14
|
-
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
15
|
-
import useOpenApiSchema from '../hooks/useOpenApiSchema';
|
|
16
|
-
import { deduplicateEndpoints } from '../utils/versionManager';
|
|
17
|
-
|
|
18
|
-
import type { ApiEndpoint } from '../types';
|
|
19
|
-
|
|
20
|
-
// Method color mapping for badges
|
|
21
|
-
const METHOD_BADGE_VARIANTS: Record<string, string> = {
|
|
22
|
-
GET: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/20',
|
|
23
|
-
POST: 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/20',
|
|
24
|
-
PUT: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20',
|
|
25
|
-
PATCH: 'bg-orange-500/15 text-orange-600 dark:text-orange-400 border-orange-500/20',
|
|
26
|
-
DELETE: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/20',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const EndpointsLibrary: React.FC = () => {
|
|
30
|
-
const { state, config, setSelectedEndpoint, setSelectedCategory, setSearchTerm } = usePlaygroundContext();
|
|
31
|
-
const { endpoints, categories, loading, error, schemas, currentSchema, setCurrentSchema } = useOpenApiSchema({
|
|
32
|
-
schemas: config.schemas,
|
|
33
|
-
defaultSchemaId: config.defaultSchemaId,
|
|
34
|
-
});
|
|
35
|
-
const [viewMode, setViewMode] = React.useState<'table' | 'grid'>('table');
|
|
36
|
-
|
|
37
|
-
const getRelativePath = (fullPath: string): string => {
|
|
38
|
-
try {
|
|
39
|
-
const url = new URL(fullPath);
|
|
40
|
-
return url.pathname;
|
|
41
|
-
} catch {
|
|
42
|
-
return fullPath;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const filteredEndpoints = useMemo(() => {
|
|
47
|
-
let filtered = deduplicateEndpoints(endpoints, state.selectedVersion);
|
|
48
|
-
|
|
49
|
-
if (state.selectedCategory && state.selectedCategory !== 'All') {
|
|
50
|
-
filtered = filtered.filter((endpoint) => endpoint.category === state.selectedCategory);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (state.searchTerm) {
|
|
54
|
-
const searchLower = state.searchTerm.toLowerCase();
|
|
55
|
-
filtered = filtered.filter(
|
|
56
|
-
(endpoint) =>
|
|
57
|
-
endpoint.name.toLowerCase().includes(searchLower) ||
|
|
58
|
-
endpoint.description.toLowerCase().includes(searchLower) ||
|
|
59
|
-
endpoint.path.toLowerCase().includes(searchLower)
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return filtered;
|
|
64
|
-
}, [endpoints, state.selectedCategory, state.searchTerm, state.selectedVersion]);
|
|
65
|
-
|
|
66
|
-
const schemaOptions = useMemo(() =>
|
|
67
|
-
schemas.map((s) => ({ value: s.id, label: s.name })),
|
|
68
|
-
[schemas]
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const getMethodBadge = (method: string) => (
|
|
72
|
-
<Badge
|
|
73
|
-
key={method}
|
|
74
|
-
variant="outline"
|
|
75
|
-
className={`text-xs font-mono font-semibold ${METHOD_BADGE_VARIANTS[method.toUpperCase()] || ''}`}
|
|
76
|
-
>
|
|
77
|
-
{method}
|
|
78
|
-
</Badge>
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
if (loading) {
|
|
82
|
-
return (
|
|
83
|
-
<div className="space-y-3">
|
|
84
|
-
<div className="flex items-center gap-2">
|
|
85
|
-
<Skeleton className="h-9 w-44" />
|
|
86
|
-
<Skeleton className="h-9 flex-1 max-w-xs" />
|
|
87
|
-
<div className="flex-1" />
|
|
88
|
-
<Skeleton className="h-9 w-24" />
|
|
89
|
-
</div>
|
|
90
|
-
<div className="space-y-1">
|
|
91
|
-
{Array.from({ length: 8 }).map((_, i) => (
|
|
92
|
-
<Skeleton key={i} className="h-12 w-full" />
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (error) {
|
|
100
|
-
return (
|
|
101
|
-
<div className="space-y-3">
|
|
102
|
-
<Card className="bg-destructive/10 border-destructive/20">
|
|
103
|
-
<CardContent className="p-4">
|
|
104
|
-
<p className="text-sm text-destructive">Failed to load schema: {error}</p>
|
|
105
|
-
</CardContent>
|
|
106
|
-
</Card>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<div className="space-y-3">
|
|
113
|
-
{/* Toolbar — single row */}
|
|
114
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
115
|
-
{/* Left: Schema selector */}
|
|
116
|
-
{schemas.length > 1 && (
|
|
117
|
-
<Combobox
|
|
118
|
-
options={schemaOptions}
|
|
119
|
-
value={currentSchema?.id || ''}
|
|
120
|
-
onValueChange={(id) => id && setCurrentSchema(id)}
|
|
121
|
-
placeholder="Select API"
|
|
122
|
-
searchPlaceholder="Search APIs..."
|
|
123
|
-
emptyText="No APIs found"
|
|
124
|
-
className="w-44"
|
|
125
|
-
/>
|
|
126
|
-
)}
|
|
127
|
-
|
|
128
|
-
{/* Search */}
|
|
129
|
-
<div className="relative flex-1 max-w-xs">
|
|
130
|
-
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
131
|
-
<Input
|
|
132
|
-
placeholder="Search..."
|
|
133
|
-
value={state.searchTerm}
|
|
134
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
|
135
|
-
className="h-9 pl-8 text-sm"
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
{/* Category filter */}
|
|
140
|
-
<Select value={state.selectedCategory} onValueChange={setSelectedCategory}>
|
|
141
|
-
<SelectTrigger className="w-auto h-9 gap-1.5">
|
|
142
|
-
<Filter className="h-3.5 w-3.5" />
|
|
143
|
-
<SelectValue />
|
|
144
|
-
</SelectTrigger>
|
|
145
|
-
<SelectContent>
|
|
146
|
-
<SelectItem value="All">All</SelectItem>
|
|
147
|
-
{categories.map((category) => (
|
|
148
|
-
<SelectItem key={category} value={category}>
|
|
149
|
-
{category}
|
|
150
|
-
</SelectItem>
|
|
151
|
-
))}
|
|
152
|
-
</SelectContent>
|
|
153
|
-
</Select>
|
|
154
|
-
|
|
155
|
-
{/* Spacer */}
|
|
156
|
-
<div className="flex-1" />
|
|
157
|
-
|
|
158
|
-
{/* Right: View toggle + count + download */}
|
|
159
|
-
<span className="text-xs text-muted-foreground tabular-nums hidden sm:inline">
|
|
160
|
-
{filteredEndpoints.length} endpoint{filteredEndpoints.length !== 1 ? 's' : ''}
|
|
161
|
-
</span>
|
|
162
|
-
|
|
163
|
-
<div className="flex items-center border rounded-md">
|
|
164
|
-
<Button
|
|
165
|
-
variant={viewMode === 'table' ? 'default' : 'ghost'}
|
|
166
|
-
size="icon"
|
|
167
|
-
className="h-8 w-8 rounded-r-none"
|
|
168
|
-
onClick={() => setViewMode('table')}
|
|
169
|
-
>
|
|
170
|
-
<List className="h-3.5 w-3.5" />
|
|
171
|
-
</Button>
|
|
172
|
-
<Button
|
|
173
|
-
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
|
174
|
-
size="icon"
|
|
175
|
-
className="h-8 w-8 rounded-l-none"
|
|
176
|
-
onClick={() => setViewMode('grid')}
|
|
177
|
-
>
|
|
178
|
-
<Grid3X3 className="h-3.5 w-3.5" />
|
|
179
|
-
</Button>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
{currentSchema && (
|
|
183
|
-
<DownloadButton
|
|
184
|
-
url={currentSchema.url}
|
|
185
|
-
filename={`${currentSchema.id}-openapi.json`}
|
|
186
|
-
variant="outline"
|
|
187
|
-
size="sm"
|
|
188
|
-
className="h-8"
|
|
189
|
-
>
|
|
190
|
-
<span className="hidden sm:inline">Schema</span>
|
|
191
|
-
</DownloadButton>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
{/* Endpoints list */}
|
|
196
|
-
{viewMode === 'table' ? (
|
|
197
|
-
<Card>
|
|
198
|
-
<Table>
|
|
199
|
-
<TableHeader>
|
|
200
|
-
<TableRow>
|
|
201
|
-
<TableHead className="w-20 text-foreground">Method</TableHead>
|
|
202
|
-
<TableHead className="text-foreground">Path</TableHead>
|
|
203
|
-
<TableHead className="text-foreground hidden lg:table-cell">Description</TableHead>
|
|
204
|
-
</TableRow>
|
|
205
|
-
</TableHeader>
|
|
206
|
-
<TableBody>
|
|
207
|
-
{filteredEndpoints.length === 0 ? (
|
|
208
|
-
<TableRow>
|
|
209
|
-
<TableCell colSpan={3} className="text-center py-12 text-muted-foreground">
|
|
210
|
-
No endpoints found
|
|
211
|
-
</TableCell>
|
|
212
|
-
</TableRow>
|
|
213
|
-
) : (
|
|
214
|
-
filteredEndpoints.map((endpoint) => (
|
|
215
|
-
<TableRow
|
|
216
|
-
key={`${endpoint.method}-${endpoint.path}`}
|
|
217
|
-
className={`cursor-pointer transition-colors hover:bg-muted/50 ${
|
|
218
|
-
state.selectedEndpoint?.path === endpoint.path &&
|
|
219
|
-
state.selectedEndpoint?.method === endpoint.method
|
|
220
|
-
? 'bg-primary/5'
|
|
221
|
-
: ''
|
|
222
|
-
}`}
|
|
223
|
-
onClick={() => setSelectedEndpoint(endpoint)}
|
|
224
|
-
>
|
|
225
|
-
<TableCell className="py-2.5">
|
|
226
|
-
{getMethodBadge(endpoint.method)}
|
|
227
|
-
</TableCell>
|
|
228
|
-
<TableCell className="font-mono text-sm text-muted-foreground py-2.5">
|
|
229
|
-
{getRelativePath(endpoint.path)}
|
|
230
|
-
</TableCell>
|
|
231
|
-
<TableCell className="text-sm text-muted-foreground max-w-sm truncate py-2.5 hidden lg:table-cell">
|
|
232
|
-
{endpoint.description}
|
|
233
|
-
</TableCell>
|
|
234
|
-
</TableRow>
|
|
235
|
-
))
|
|
236
|
-
)}
|
|
237
|
-
</TableBody>
|
|
238
|
-
</Table>
|
|
239
|
-
</Card>
|
|
240
|
-
) : (
|
|
241
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
242
|
-
{filteredEndpoints.length === 0 ? (
|
|
243
|
-
<div className="col-span-full text-center py-12 text-muted-foreground">
|
|
244
|
-
No endpoints found
|
|
245
|
-
</div>
|
|
246
|
-
) : (
|
|
247
|
-
filteredEndpoints.map((endpoint) => (
|
|
248
|
-
<Card
|
|
249
|
-
key={`${endpoint.method}-${endpoint.path}`}
|
|
250
|
-
className={`cursor-pointer transition-all hover:shadow-md ${
|
|
251
|
-
state.selectedEndpoint?.path === endpoint.path &&
|
|
252
|
-
state.selectedEndpoint?.method === endpoint.method
|
|
253
|
-
? 'ring-2 ring-primary'
|
|
254
|
-
: ''
|
|
255
|
-
}`}
|
|
256
|
-
onClick={() => setSelectedEndpoint(endpoint)}
|
|
257
|
-
>
|
|
258
|
-
<CardHeader className="pb-2 pt-3 px-4">
|
|
259
|
-
<CardTitle className="flex items-center justify-between text-sm">
|
|
260
|
-
<span className="font-mono text-xs text-muted-foreground truncate">
|
|
261
|
-
{getRelativePath(endpoint.path)}
|
|
262
|
-
</span>
|
|
263
|
-
{getMethodBadge(endpoint.method)}
|
|
264
|
-
</CardTitle>
|
|
265
|
-
</CardHeader>
|
|
266
|
-
<CardContent className="px-4 pb-3 pt-0">
|
|
267
|
-
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
268
|
-
{endpoint.description}
|
|
269
|
-
</p>
|
|
270
|
-
</CardContent>
|
|
271
|
-
</Card>
|
|
272
|
-
))
|
|
273
|
-
)}
|
|
274
|
-
</div>
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
277
|
-
);
|
|
278
|
-
};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Menu, X } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
|
|
6
|
-
import { Button, Sheet, SheetContent, SheetTrigger } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
9
|
-
import { useMobile } from '../hooks/useMobile';
|
|
10
|
-
import { EndpointsLibrary } from './EndpointsLibrary';
|
|
11
|
-
import { PlaygroundStepper } from './PlaygroundStepper';
|
|
12
|
-
import { RequestBuilder } from './RequestBuilder';
|
|
13
|
-
import { ResponseViewer } from './ResponseViewer';
|
|
14
|
-
|
|
15
|
-
export const PlaygroundLayout: React.FC = () => {
|
|
16
|
-
const { state, setSidebarOpen } = usePlaygroundContext();
|
|
17
|
-
const { isMobile } = useMobile();
|
|
18
|
-
|
|
19
|
-
const renderStepContent = () => {
|
|
20
|
-
switch (state.currentStep) {
|
|
21
|
-
case 'endpoints':
|
|
22
|
-
return <EndpointsLibrary />;
|
|
23
|
-
case 'request':
|
|
24
|
-
return <RequestBuilder />;
|
|
25
|
-
case 'response':
|
|
26
|
-
return <ResponseViewer />;
|
|
27
|
-
default:
|
|
28
|
-
return <EndpointsLibrary />;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div>
|
|
34
|
-
{/* Stepper bar */}
|
|
35
|
-
<div className="border-b">
|
|
36
|
-
<div className="container mx-auto px-6 py-3">
|
|
37
|
-
<div className="flex items-center justify-between">
|
|
38
|
-
{/* Mobile Menu */}
|
|
39
|
-
{isMobile ? (
|
|
40
|
-
<>
|
|
41
|
-
<Sheet open={state.sidebarOpen} onOpenChange={setSidebarOpen}>
|
|
42
|
-
<SheetTrigger asChild>
|
|
43
|
-
<Button variant="ghost" size="sm">
|
|
44
|
-
<Menu className="h-4 w-4" />
|
|
45
|
-
</Button>
|
|
46
|
-
</SheetTrigger>
|
|
47
|
-
<SheetContent side="left" className="w-80">
|
|
48
|
-
<div className="space-y-4">
|
|
49
|
-
<div className="flex items-center justify-between">
|
|
50
|
-
<h2 className="text-lg font-semibold text-foreground">Navigation</h2>
|
|
51
|
-
<Button
|
|
52
|
-
variant="ghost"
|
|
53
|
-
size="sm"
|
|
54
|
-
onClick={() => setSidebarOpen(false)}
|
|
55
|
-
>
|
|
56
|
-
<X className="h-4 w-4" />
|
|
57
|
-
</Button>
|
|
58
|
-
</div>
|
|
59
|
-
<PlaygroundStepper />
|
|
60
|
-
</div>
|
|
61
|
-
</SheetContent>
|
|
62
|
-
</Sheet>
|
|
63
|
-
<span className="text-sm font-medium text-foreground capitalize">{state.currentStep}</span>
|
|
64
|
-
<div className="w-8" /> {/* Spacer for centering */}
|
|
65
|
-
</>
|
|
66
|
-
) : (
|
|
67
|
-
<PlaygroundStepper />
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{/* Content */}
|
|
74
|
-
<div className="container mx-auto px-6 py-6">
|
|
75
|
-
{renderStepContent()}
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
{/* Mobile FAB */}
|
|
79
|
-
{isMobile && state.currentStep === 'request' && (
|
|
80
|
-
<div className="fixed bottom-4 right-4 z-50">
|
|
81
|
-
<Button
|
|
82
|
-
size="lg"
|
|
83
|
-
className="rounded-full shadow-lg"
|
|
84
|
-
>
|
|
85
|
-
Send Request
|
|
86
|
-
</Button>
|
|
87
|
-
</div>
|
|
88
|
-
)}
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Check, ChevronLeft, ChevronRight, Code, FileText, Send } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
|
|
6
|
-
import { Badge, Button } from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
import { usePlaygroundContext } from '../context/PlaygroundContext';
|
|
9
|
-
import { PlaygroundStep } from '../types';
|
|
10
|
-
|
|
11
|
-
const stepConfig = {
|
|
12
|
-
endpoints: {
|
|
13
|
-
title: 'Endpoints',
|
|
14
|
-
icon: Code,
|
|
15
|
-
description: 'Select API endpoint'
|
|
16
|
-
},
|
|
17
|
-
request: {
|
|
18
|
-
title: 'Request',
|
|
19
|
-
icon: Send,
|
|
20
|
-
description: 'Configure request'
|
|
21
|
-
},
|
|
22
|
-
response: {
|
|
23
|
-
title: 'Response',
|
|
24
|
-
icon: FileText,
|
|
25
|
-
description: 'View response'
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const PlaygroundStepper: React.FC = () => {
|
|
30
|
-
const { state, setCurrentStep, goToNextStep, goToPreviousStep } = usePlaygroundContext();
|
|
31
|
-
const { currentStep, steps } = state;
|
|
32
|
-
|
|
33
|
-
const currentIndex = steps.indexOf(currentStep);
|
|
34
|
-
const canGoNext = currentIndex < steps.length - 1;
|
|
35
|
-
const canGoPrevious = currentIndex > 0;
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="flex items-center justify-between w-full">
|
|
39
|
-
{/* Steps */}
|
|
40
|
-
<div className="flex items-center space-x-4">
|
|
41
|
-
{steps.map((step, index) => {
|
|
42
|
-
const config = stepConfig[step];
|
|
43
|
-
const Icon = config.icon;
|
|
44
|
-
const isActive = step === currentStep;
|
|
45
|
-
const isCompleted = index < currentIndex;
|
|
46
|
-
const isClickable = index <= currentIndex + 1;
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div key={step} className="flex items-center space-x-2">
|
|
50
|
-
<Button
|
|
51
|
-
variant={isActive ? 'default' : 'ghost'}
|
|
52
|
-
size="sm"
|
|
53
|
-
onClick={() => isClickable && setCurrentStep(step)}
|
|
54
|
-
className={`flex items-center space-x-2 ${isClickable ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
|
|
55
|
-
}`}
|
|
56
|
-
disabled={!isClickable}
|
|
57
|
-
>
|
|
58
|
-
{isCompleted ? (
|
|
59
|
-
<Check className="h-4 w-4" />
|
|
60
|
-
) : (
|
|
61
|
-
<Icon className="h-4 w-4" />
|
|
62
|
-
)}
|
|
63
|
-
<span className="hidden sm:inline">{config.title}</span>
|
|
64
|
-
</Button>
|
|
65
|
-
|
|
66
|
-
{index < steps.length - 1 && (
|
|
67
|
-
<div className="w-8 h-px bg-border" />
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
{/* Navigation */}
|
|
75
|
-
<div className="flex items-center space-x-2">
|
|
76
|
-
<Button
|
|
77
|
-
variant="outline"
|
|
78
|
-
size="sm"
|
|
79
|
-
onClick={goToPreviousStep}
|
|
80
|
-
disabled={!canGoPrevious}
|
|
81
|
-
className="flex items-center space-x-1"
|
|
82
|
-
>
|
|
83
|
-
<ChevronLeft className="h-4 w-4" />
|
|
84
|
-
<span className="hidden sm:inline">Previous</span>
|
|
85
|
-
</Button>
|
|
86
|
-
|
|
87
|
-
<Button
|
|
88
|
-
variant="outline"
|
|
89
|
-
size="sm"
|
|
90
|
-
onClick={goToNextStep}
|
|
91
|
-
disabled={!canGoNext}
|
|
92
|
-
className="flex items-center space-x-1"
|
|
93
|
-
>
|
|
94
|
-
<span className="hidden sm:inline">Next</span>
|
|
95
|
-
<ChevronRight className="h-4 w-4" />
|
|
96
|
-
</Button>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
};
|