@djangocfg/ui-tools 2.1.196 → 2.1.197

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.
@@ -1,62 +1,55 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- BarChart3, Code, Database, FileText, Filter, Grid3X3, List, Search, Settings, Shield, Users
4
+ Code, Filter, Grid3X3, List, Search,
5
5
  } from 'lucide-react';
6
6
  import React, { useMemo } from 'react';
7
7
 
8
8
  import {
9
- Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, Select, SelectContent,
10
- SelectItem, SelectTrigger, SelectValue, Skeleton, Table, TableBody, TableCell, TableHead,
11
- TableHeader, TableRow
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
12
  } from '@djangocfg/ui-core/components';
13
13
 
14
14
  import { usePlaygroundContext } from '../context/PlaygroundContext';
15
15
  import useOpenApiSchema from '../hooks/useOpenApiSchema';
16
- import { getMethodColor } from '../utils';
17
16
  import { deduplicateEndpoints } from '../utils/versionManager';
18
- import { VersionSelector } from './VersionSelector';
19
17
 
20
18
  import type { ApiEndpoint } from '../types';
21
- const categoryIcons: Record<string, React.ReactNode> = {
22
- 'Authentication': <Shield className="h-4 w-4" />,
23
- 'Users': <Users className="h-4 w-4" />,
24
- 'Data': <Database className="h-4 w-4" />,
25
- 'Analytics': <BarChart3 className="h-4 w-4" />,
26
- 'Files': <FileText className="h-4 w-4" />,
27
- 'Settings': <Settings className="h-4 w-4" />,
28
- 'Other': <Code className="h-4 w-4" />,
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',
29
27
  };
30
28
 
31
29
  export const EndpointsLibrary: React.FC = () => {
32
30
  const { state, config, setSelectedEndpoint, setSelectedCategory, setSearchTerm } = usePlaygroundContext();
33
- const { endpoints, categories, loading, error } = useOpenApiSchema({
31
+ const { endpoints, categories, loading, error, schemas, currentSchema, setCurrentSchema } = useOpenApiSchema({
34
32
  schemas: config.schemas,
35
33
  defaultSchemaId: config.defaultSchemaId,
36
34
  });
37
35
  const [viewMode, setViewMode] = React.useState<'table' | 'grid'>('table');
38
36
 
39
- // Helper to extract relative path from full URL
40
37
  const getRelativePath = (fullPath: string): string => {
41
38
  try {
42
39
  const url = new URL(fullPath);
43
40
  return url.pathname;
44
41
  } catch {
45
- // If not a valid URL, return as is (already relative)
46
42
  return fullPath;
47
43
  }
48
44
  };
49
45
 
50
46
  const filteredEndpoints = useMemo(() => {
51
- // First, deduplicate endpoints based on selected version
52
47
  let filtered = deduplicateEndpoints(endpoints, state.selectedVersion);
53
48
 
54
- // Filter by category
55
49
  if (state.selectedCategory && state.selectedCategory !== 'All') {
56
50
  filtered = filtered.filter((endpoint) => endpoint.category === state.selectedCategory);
57
51
  }
58
52
 
59
- // Filter by search term
60
53
  if (state.searchTerm) {
61
54
  const searchLower = state.searchTerm.toLowerCase();
62
55
  filtered = filtered.filter(
@@ -70,31 +63,33 @@ export const EndpointsLibrary: React.FC = () => {
70
63
  return filtered;
71
64
  }, [endpoints, state.selectedCategory, state.searchTerm, state.selectedVersion]);
72
65
 
73
- const handleEndpointSelect = (endpoint: ApiEndpoint) => {
74
- setSelectedEndpoint(endpoint);
75
- };
66
+ const schemaOptions = useMemo(() =>
67
+ schemas.map((s) => ({ value: s.id, label: s.name })),
68
+ [schemas]
69
+ );
76
70
 
77
- const getMethodBadges = (methods: string) => {
78
- return methods.split(', ').map((method) => (
79
- <Badge key={method} variant={getMethodColor(method) === 'success' ? 'default' : 'secondary'} className="text-xs">
80
- {method}
81
- </Badge>
82
- ));
83
- };
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
+ );
84
80
 
85
81
  if (loading) {
86
82
  return (
87
- <div className="space-y-4">
88
- <div className="flex items-center justify-between">
89
- <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
90
- <div className="flex items-center space-x-2">
91
- <Skeleton className="h-8 w-32" />
92
- <Skeleton className="h-8 w-48" />
93
- </div>
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" />
94
89
  </div>
95
- <div className="space-y-2">
96
- {Array.from({ length: 5 }).map((_, i) => (
97
- <Skeleton key={i} className="h-20 w-full" />
90
+ <div className="space-y-1">
91
+ {Array.from({ length: 8 }).map((_, i) => (
92
+ <Skeleton key={i} className="h-12 w-full" />
98
93
  ))}
99
94
  </div>
100
95
  </div>
@@ -103,13 +98,10 @@ export const EndpointsLibrary: React.FC = () => {
103
98
 
104
99
  if (error) {
105
100
  return (
106
- <div className="space-y-4">
107
- <div className="flex items-center justify-between">
108
- <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
109
- </div>
101
+ <div className="space-y-3">
110
102
  <Card className="bg-destructive/10 border-destructive/20">
111
103
  <CardContent className="p-4">
112
- <p className="text-sm text-destructive">Error loading endpoints: {error}</p>
104
+ <p className="text-sm text-destructive">Failed to load schema: {error}</p>
113
105
  </CardContent>
114
106
  </Card>
115
107
  </div>
@@ -117,81 +109,104 @@ export const EndpointsLibrary: React.FC = () => {
117
109
  }
118
110
 
119
111
  return (
120
- <div className="space-y-4">
121
- {/* Header */}
122
- <div className="flex flex-col gap-4">
123
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
124
- <h2 className="text-lg font-semibold text-foreground">API Endpoints</h2>
125
- <VersionSelector />
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
+ />
126
137
  </div>
127
138
 
128
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
129
- <div className="flex items-center space-x-2">
130
- {/* View Mode Toggle */}
131
- <div className="flex items-center border rounded-md">
132
- <Button
133
- variant={viewMode === 'table' ? 'default' : 'ghost'}
134
- size="sm"
135
- onClick={() => setViewMode('table')}
136
- className="rounded-r-none"
137
- >
138
- <List className="h-4 w-4" />
139
- </Button>
140
- <Button
141
- variant={viewMode === 'grid' ? 'default' : 'ghost'}
142
- size="sm"
143
- onClick={() => setViewMode('grid')}
144
- className="rounded-l-none"
145
- >
146
- <Grid3X3 className="h-4 w-4" />
147
- </Button>
148
- </div>
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>
149
154
 
150
- {/* Category Filter */}
151
- <Select value={state.selectedCategory} onValueChange={setSelectedCategory}>
152
- <SelectTrigger className="w-32">
153
- <Filter className="h-4 w-4 mr-2" />
154
- <SelectValue />
155
- </SelectTrigger>
156
- <SelectContent>
157
- <SelectItem value="All">All</SelectItem>
158
- {categories.map((category) => (
159
- <SelectItem key={category} value={category}>
160
- {category}
161
- </SelectItem>
162
- ))}
163
- </SelectContent>
164
- </Select>
155
+ {/* Spacer */}
156
+ <div className="flex-1" />
165
157
 
166
- {/* Search */}
167
- <div className="relative">
168
- <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
169
- <Input
170
- placeholder="Search endpoints..."
171
- value={state.searchTerm}
172
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
173
- className="w-48 pl-8"
174
- />
175
- </div>
176
- </div>
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>
177
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
+ )}
178
193
  </div>
179
194
 
180
- {/* Content */}
195
+ {/* Endpoints list */}
181
196
  {viewMode === 'table' ? (
182
197
  <Card>
183
198
  <Table>
184
199
  <TableHeader>
185
200
  <TableRow>
186
- <TableHead className="text-foreground">Methods</TableHead>
201
+ <TableHead className="w-20 text-foreground">Method</TableHead>
187
202
  <TableHead className="text-foreground">Path</TableHead>
188
- <TableHead className="text-foreground">Description</TableHead>
203
+ <TableHead className="text-foreground hidden lg:table-cell">Description</TableHead>
189
204
  </TableRow>
190
205
  </TableHeader>
191
206
  <TableBody>
192
207
  {filteredEndpoints.length === 0 ? (
193
208
  <TableRow>
194
- <TableCell colSpan={3} className="text-center py-8 text-muted-foreground">
209
+ <TableCell colSpan={3} className="text-center py-12 text-muted-foreground">
195
210
  No endpoints found
196
211
  </TableCell>
197
212
  </TableRow>
@@ -199,19 +214,21 @@ export const EndpointsLibrary: React.FC = () => {
199
214
  filteredEndpoints.map((endpoint) => (
200
215
  <TableRow
201
216
  key={`${endpoint.method}-${endpoint.path}`}
202
- className={`cursor-pointer transition-colors hover:bg-muted/50 ${state.selectedEndpoint?.path === endpoint.path ? 'bg-primary/10' : ''
203
- }`}
204
- onClick={() => handleEndpointSelect(endpoint)}
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)}
205
224
  >
206
- <TableCell>
207
- <div className="flex space-x-1">
208
- {getMethodBadges(endpoint.method)}
209
- </div>
225
+ <TableCell className="py-2.5">
226
+ {getMethodBadge(endpoint.method)}
210
227
  </TableCell>
211
- <TableCell className="font-mono text-sm text-muted-foreground">
228
+ <TableCell className="font-mono text-sm text-muted-foreground py-2.5">
212
229
  {getRelativePath(endpoint.path)}
213
230
  </TableCell>
214
- <TableCell className="text-sm text-muted-foreground max-w-xs truncate">
231
+ <TableCell className="text-sm text-muted-foreground max-w-sm truncate py-2.5 hidden lg:table-cell">
215
232
  {endpoint.description}
216
233
  </TableCell>
217
234
  </TableRow>
@@ -221,34 +238,32 @@ export const EndpointsLibrary: React.FC = () => {
221
238
  </Table>
222
239
  </Card>
223
240
  ) : (
224
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
241
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
225
242
  {filteredEndpoints.length === 0 ? (
226
- <div className="col-span-full text-center py-8 text-muted-foreground">
243
+ <div className="col-span-full text-center py-12 text-muted-foreground">
227
244
  No endpoints found
228
245
  </div>
229
246
  ) : (
230
247
  filteredEndpoints.map((endpoint) => (
231
248
  <Card
232
249
  key={`${endpoint.method}-${endpoint.path}`}
233
- className={`cursor-pointer transition-all hover:shadow-md ${state.selectedEndpoint?.path === endpoint.path ? 'ring-2 ring-primary' : ''
234
- }`}
235
- onClick={() => handleEndpointSelect(endpoint)}
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)}
236
257
  >
237
- <CardHeader className="pb-3">
258
+ <CardHeader className="pb-2 pt-3 px-4">
238
259
  <CardTitle className="flex items-center justify-between text-sm">
239
- <div className="flex items-center space-x-2">
240
- {categoryIcons[endpoint.category] || <Code className="h-4 w-4" />}
241
- <span className="truncate">{endpoint.name}</span>
242
- </div>
243
- <div className="flex space-x-1">
244
- {getMethodBadges(endpoint.method)}
245
- </div>
260
+ <span className="font-mono text-xs text-muted-foreground truncate">
261
+ {getRelativePath(endpoint.path)}
262
+ </span>
263
+ {getMethodBadge(endpoint.method)}
246
264
  </CardTitle>
247
265
  </CardHeader>
248
- <CardContent className="space-y-2">
249
- <p className="text-xs font-mono text-muted-foreground break-all">
250
- {getRelativePath(endpoint.path)}
251
- </p>
266
+ <CardContent className="px-4 pb-3 pt-0">
252
267
  <p className="text-xs text-muted-foreground line-clamp-2">
253
268
  {endpoint.description}
254
269
  </p>
@@ -260,4 +275,4 @@ export const EndpointsLibrary: React.FC = () => {
260
275
  )}
261
276
  </div>
262
277
  );
263
- };
278
+ };
@@ -29,92 +29,58 @@ export const PlaygroundLayout: React.FC = () => {
29
29
  }
30
30
  };
31
31
 
32
- const getStepTitle = () => {
33
- switch (state.currentStep) {
34
- case 'endpoints':
35
- return 'API Endpoints';
36
- case 'request':
37
- return 'Request Builder';
38
- case 'response':
39
- return 'Response Viewer';
40
- default:
41
- return 'API Playground';
42
- }
43
- };
44
-
45
32
  return (
46
- <div className="min-h-screen">
47
- {/* Header */}
33
+ <div>
34
+ {/* Stepper bar */}
48
35
  <div className="border-b">
49
- <div className="container mx-auto px-4 py-4">
36
+ <div className="container mx-auto px-6 py-3">
50
37
  <div className="flex items-center justify-between">
51
- <div className="flex items-center space-x-4">
52
- <h1 className="text-xl font-bold text-foreground">API Playground</h1>
53
- <p className="text-sm text-muted-foreground hidden sm:block">
54
- Test and explore API endpoints
55
- </p>
56
- </div>
57
-
58
38
  {/* Mobile Menu */}
59
- {isMobile && (
60
- <Sheet open={state.sidebarOpen} onOpenChange={setSidebarOpen}>
61
- <SheetTrigger asChild>
62
- <Button variant="outline" size="sm">
63
- <Menu className="h-4 w-4" />
64
- </Button>
65
- </SheetTrigger>
66
- <SheetContent side="left" className="w-80">
67
- <div className="space-y-4">
68
- <div className="flex items-center justify-between">
69
- <h2 className="text-lg font-semibold text-foreground">Navigation</h2>
70
- <Button
71
- variant="ghost"
72
- size="sm"
73
- onClick={() => setSidebarOpen(false)}
74
- >
75
- <X className="h-4 w-4" />
76
- </Button>
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 />
77
60
  </div>
78
- <PlaygroundStepper />
79
- </div>
80
- </SheetContent>
81
- </Sheet>
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 />
82
68
  )}
83
69
  </div>
84
70
  </div>
85
71
  </div>
86
72
 
87
- {/* Desktop Stepper */}
88
- {!isMobile && <PlaygroundStepper />}
89
-
90
- {/* Main Content */}
73
+ {/* Content */}
91
74
  <div className="container mx-auto px-6 py-6">
92
- {/* Step Title */}
93
- <div className="mb-6">
94
- <h2 className="text-2xl font-bold text-foreground">{getStepTitle()}</h2>
95
- <p className="text-muted-foreground mt-1">
96
- {state.currentStep === 'endpoints' && 'Browse and select API endpoints'}
97
- {state.currentStep === 'request' && 'Configure your API request'}
98
- {state.currentStep === 'response' && 'View API response and details'}
99
- </p>
100
- </div>
101
-
102
- {/* Content */}
103
- <div className="space-y-6">
104
- {renderStepContent()}
105
- </div>
75
+ {renderStepContent()}
106
76
  </div>
107
77
 
108
- {/* Mobile Floating Action Button */}
78
+ {/* Mobile FAB */}
109
79
  {isMobile && state.currentStep === 'request' && (
110
80
  <div className="fixed bottom-4 right-4 z-50">
111
81
  <Button
112
82
  size="lg"
113
83
  className="rounded-full shadow-lg"
114
- onClick={() => {
115
- // Auto-advance to response step
116
- // This would be handled by the context
117
- }}
118
84
  >
119
85
  Send Request
120
86
  </Button>
@@ -122,4 +88,4 @@ export const PlaygroundLayout: React.FC = () => {
122
88
  )}
123
89
  </div>
124
90
  );
125
- };
91
+ };
@@ -35,7 +35,7 @@ export const PlaygroundStepper: React.FC = () => {
35
35
  const canGoPrevious = currentIndex > 0;
36
36
 
37
37
  return (
38
- <div className="flex items-center justify-between p-4 border-b">
38
+ <div className="flex items-center justify-between w-full">
39
39
  {/* Steps */}
40
40
  <div className="flex items-center space-x-4">
41
41
  {steps.map((step, index) => {
@@ -151,11 +151,9 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
151
151
  // Endpoint management
152
152
  const setSelectedEndpoint = (endpoint: ApiEndpoint | null) => {
153
153
  if (endpoint) {
154
- // All endpoints are GET only
155
- // Path is already a full URL from the endpoint
156
154
  updateState({
157
155
  selectedEndpoint: endpoint,
158
- requestMethod: 'GET',
156
+ requestMethod: endpoint.method,
159
157
  requestUrl: endpoint.path,
160
158
  parameters: {}, // Reset parameters when endpoint changes
161
159
  currentStep: 'request'