Skip to content

Commit f5743f2

Browse files
committed
Enhance ApiPlayground components with new features and improved styling
- Updated SiteLayout to conditionally render the Footer based on the current pathname, including the new /api-playground route. - Adjusted the height of the ApiPlayground component for better layout. - Refined EndpointList item styling for improved visual consistency. - Expanded ParameterControls to include request execution logic, validating parameters and handling API requests. - Enhanced RequestBodyEditor styling for better user experience. - Streamlined ResponsePanel by removing unnecessary elements and improving code clarity.
1 parent 7c2ed7f commit f5743f2

7 files changed

Lines changed: 186 additions & 120 deletions

File tree

client/src/app/[site]/api-playground/components/ApiPlayground.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ResponsePanel } from "./ResponsePanel";
66

77
export function ApiPlayground() {
88
return (
9-
<div className="h-[calc(100vh-120px)] flex border border-neutral-100 dark:border-neutral-850 rounded-lg overflow-hidden bg-white dark:bg-neutral-900">
9+
<div className="h-[calc(100vh-100px)] flex border border-neutral-100 dark:border-neutral-850 rounded-lg overflow-hidden bg-white dark:bg-neutral-900">
1010
{/* Left Column - Endpoint List */}
1111
<div className="w-64 shrink-0">
1212
<EndpointList />

client/src/app/[site]/api-playground/components/EndpointList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function EndpointItem({ endpoint, isSelected, onClick }: EndpointItemProps) {
5151
isSelected && "bg-neutral-100 dark:bg-neutral-800"
5252
)}
5353
>
54-
<span className={cn("shrink-0 px-1.5 py-0.5 text-[10px] font-bold rounded", methodColors[endpoint.method])}>
54+
<span className={cn("shrink-0 px-1 py-0.5 text-[10px] font-bold rounded", methodColors[endpoint.method])}>
5555
{endpoint.method}
5656
</span>
5757
<span className="text-sm text-neutral-700 dark:text-neutral-300 truncate">{endpoint.name}</span>

client/src/app/[site]/api-playground/components/ParameterControls.tsx

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,134 @@
11
"use client";
22

3+
import { authedFetch } from "@/api/utils";
34
import { Input } from "@/components/ui/input";
45
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6+
import { Loader2, Play } from "lucide-react";
7+
import { useParams } from "next/navigation";
58
import { usePlaygroundStore } from "../hooks/usePlaygroundStore";
6-
import { parameterMetadata } from "../utils/endpointConfig";
9+
import { methodColors, parameterMetadata } from "../utils/endpointConfig";
710
import { FilterBuilder } from "./FilterBuilder";
811
import { TimezoneSelect } from "./TimezoneSelect";
912
import { RequestBodyEditor } from "./RequestBodyEditor";
1013

1114
export function ParameterControls() {
15+
const params = useParams();
16+
const siteId = params.site as string;
17+
1218
const {
1319
selectedEndpoint,
1420
startDate,
1521
endDate,
22+
timeZone,
23+
filters,
1624
setStartDate,
1725
setEndDate,
1826
endpointParams,
1927
setEndpointParam,
2028
pathParams,
2129
setPathParam,
30+
requestBody,
31+
isLoading,
32+
setResponse,
33+
setResponseError,
34+
setIsLoading,
2235
} = usePlaygroundStore();
2336

37+
// Handle execute request
38+
const handleExecute = async () => {
39+
if (!selectedEndpoint) return;
40+
41+
// Validate path params
42+
if (selectedEndpoint.pathParams) {
43+
for (const param of selectedEndpoint.pathParams) {
44+
if (!pathParams[param]) {
45+
setResponseError(`Missing required path parameter: ${param}`);
46+
return;
47+
}
48+
}
49+
}
50+
51+
// Validate required query params
52+
if (selectedEndpoint.requiredParams) {
53+
for (const param of selectedEndpoint.requiredParams) {
54+
if (!endpointParams[param]) {
55+
setResponseError(`Missing required parameter: ${param}`);
56+
return;
57+
}
58+
}
59+
}
60+
61+
// Parse request body
62+
let parsedBody: any;
63+
if (selectedEndpoint.hasRequestBody && requestBody) {
64+
try {
65+
parsedBody = JSON.parse(requestBody);
66+
} catch {
67+
setResponseError("Invalid JSON in request body");
68+
return;
69+
}
70+
}
71+
72+
setIsLoading(true);
73+
const startTime = performance.now();
74+
75+
try {
76+
// Build the path
77+
let path = selectedEndpoint.path.replace(":site", siteId);
78+
if (selectedEndpoint.pathParams) {
79+
for (const param of selectedEndpoint.pathParams) {
80+
path = path.replace(`:${param}`, pathParams[param]);
81+
}
82+
}
83+
84+
// Build query params
85+
const queryParams: Record<string, any> = {};
86+
87+
if (selectedEndpoint.hasCommonParams) {
88+
queryParams.start_date = startDate;
89+
queryParams.end_date = endDate;
90+
queryParams.time_zone = timeZone;
91+
92+
const apiFilters = filters
93+
.filter(f => f.value.trim() !== "")
94+
.map(f => ({
95+
parameter: f.parameter,
96+
type: f.operator,
97+
value: [f.value],
98+
}));
99+
if (apiFilters.length > 0) {
100+
queryParams.filters = JSON.stringify(apiFilters);
101+
}
102+
}
103+
104+
// Add endpoint-specific params
105+
if (selectedEndpoint.specificParams) {
106+
for (const param of selectedEndpoint.specificParams) {
107+
if (endpointParams[param]) {
108+
queryParams[param] = endpointParams[param];
109+
}
110+
}
111+
}
112+
113+
// Make the request
114+
const result = await authedFetch<any>(
115+
path,
116+
queryParams,
117+
selectedEndpoint.method !== "GET"
118+
? {
119+
method: selectedEndpoint.method,
120+
data: parsedBody,
121+
}
122+
: undefined
123+
);
124+
125+
const endTime = performance.now();
126+
setResponse(result, Math.round(endTime - startTime));
127+
} catch (err: any) {
128+
setResponseError(err.message || "Request failed");
129+
}
130+
};
131+
24132
if (!selectedEndpoint) {
25133
return (
26134
<div className="h-full flex items-center justify-center text-neutral-500 dark:text-neutral-400 p-4">
@@ -29,13 +137,72 @@ export function ParameterControls() {
29137
);
30138
}
31139

140+
// Format path with highlighted parameters
141+
const formatPath = (path: string) => {
142+
// Replace :site with actual siteId
143+
let displayPath = path.replace(":site", siteId);
144+
145+
// Split by path parameters (e.g., :param) and render them differently
146+
const parts = displayPath.split(/(:[\w]+)/g);
147+
148+
return parts.map((part, index) => {
149+
if (part.startsWith(":")) {
150+
const paramName = part.slice(1);
151+
const value = pathParams[paramName];
152+
return (
153+
<span
154+
key={index}
155+
className="px-1.5 py-0.5 mx-0.5 rounded border border-emerald-500/50 bg-emerald-500/10 text-emerald-400 font-mono"
156+
>
157+
{value || `{${paramName}}`}
158+
</span>
159+
);
160+
}
161+
return <span key={index}>{part}</span>;
162+
});
163+
};
164+
32165
return (
33166
<div className="h-full overflow-y-auto p-4 space-y-6">
34-
<div>
35-
<h2 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-1">{selectedEndpoint.name}</h2>
36-
{selectedEndpoint.description && (
37-
<p className="text-xs text-neutral-500 dark:text-neutral-400">{selectedEndpoint.description}</p>
38-
)}
167+
{/* Header */}
168+
<div className="space-y-3">
169+
<div>
170+
<h2 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-1">{selectedEndpoint.name}</h2>
171+
{selectedEndpoint.description && (
172+
<p className="text-xs text-neutral-500 dark:text-neutral-400">{selectedEndpoint.description}</p>
173+
)}
174+
</div>
175+
176+
{/* Method + Path + Try it button */}
177+
<div className="flex items-center gap-2 p-1 border border-neutral-100 rounded-[8px] dark:border-neutral-800">
178+
<div className="flex items-center gap-2 border border-neutral-100 dark:border-neutral-800 rounded-[6px] p-1 flex-1">
179+
{/* Method Badge */}
180+
<span className={`shrink-0 px-1.5 py-1 text-xs font-bold rounded ${methodColors[selectedEndpoint.method]}`}>
181+
{selectedEndpoint.method}
182+
</span>
183+
184+
{/* Path */}
185+
<div className="flex-1 min-w-0 text-sm text-neutral-900 dark:text-neutral-100 font-mono truncate">
186+
{formatPath(selectedEndpoint.path)}
187+
</div>
188+
</div>
189+
190+
{/* Try it button */}
191+
<button
192+
onClick={handleExecute}
193+
disabled={isLoading}
194+
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500 hover:bg-emerald-600 disabled:bg-emerald-400 text-white text-sm font-medium rounded-lg transition-colors shrink-0"
195+
>
196+
{isLoading ? (
197+
<Loader2 className="h-4 w-4 animate-spin" />
198+
) : (
199+
<>
200+
Run
201+
<Play className="h-3.5 w-3.5 fill-current" />
202+
</>
203+
)}
204+
</button>
205+
</div>
39206
</div>
40207

41208
{/* Path Parameters */}

client/src/app/[site]/api-playground/components/RequestBodyEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function RequestBodyEditor() {
3737
{error && <span className="text-xs text-red-500">{error}</span>}
3838
</div>
3939
<div
40-
className="relative min-h-[120px] rounded border border-neutral-300 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-900 overflow-hidden"
40+
className="relative min-h-[120px] rounded border border-neutral-150 dark:border-neutral-800 overflow-hidden"
4141
onClick={() => textareaRef.current?.focus()}
4242
>
4343
<SyntaxHighlighter

client/src/app/[site]/api-playground/components/ResponsePanel.tsx

Lines changed: 6 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
"use client";
22

3-
import { authedFetch } from "@/api/utils";
43
import { CodeSnippet } from "@/components/CodeSnippet";
5-
import { Button } from "@/components/ui/button";
6-
import { BACKEND_URL } from "@/lib/const";
7-
import { Loader2, Play, XCircle } from "lucide-react";
4+
import { XCircle } from "lucide-react";
85
import { useParams } from "next/navigation";
96
import { useMemo } from "react";
107
import { usePlaygroundStore } from "../hooks/usePlaygroundStore";
@@ -26,25 +23,13 @@ export function ResponsePanel() {
2623
requestBody,
2724
response,
2825
responseError,
29-
isLoading,
3026
responseTime,
31-
setResponse,
32-
setResponseError,
33-
setIsLoading,
3427
} = usePlaygroundStore();
3528

36-
// Build the full URL
37-
const { fullUrl, queryParams, parsedBody } = useMemo(() => {
29+
// Build the full URL and query params for code generation
30+
const { queryParams, parsedBody } = useMemo(() => {
3831
if (!selectedEndpoint) {
39-
return { fullUrl: "", queryParams: {}, parsedBody: undefined };
40-
}
41-
42-
// Replace path params and :site
43-
let path = selectedEndpoint.path.replace(":site", siteId);
44-
if (selectedEndpoint.pathParams) {
45-
for (const param of selectedEndpoint.pathParams) {
46-
path = path.replace(`:${param}`, pathParams[param] || `:${param}`);
47-
}
32+
return { queryParams: {}, parsedBody: undefined };
4833
}
4934

5035
// Build query params
@@ -87,16 +72,8 @@ export function ResponsePanel() {
8772
}
8873
}
8974

90-
// Build query string
91-
const queryString = Object.entries(qp)
92-
.filter(([, v]) => v !== undefined && v !== null && v !== "")
93-
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
94-
.join("&");
95-
96-
const url = `${BACKEND_URL}${path}${queryString ? `?${queryString}` : ""}`;
97-
98-
return { fullUrl: url, queryParams: qp, parsedBody: body };
99-
}, [selectedEndpoint, siteId, startDate, endDate, timeZone, filters, endpointParams, pathParams, requestBody]);
75+
return { queryParams: qp, parsedBody: body };
76+
}, [selectedEndpoint, startDate, endDate, timeZone, filters, endpointParams, requestBody]);
10077

10178
// Code generation config
10279
const codeConfig: CodeGenConfig = useMemo(() => {
@@ -123,71 +100,6 @@ export function ResponsePanel() {
123100
};
124101
}, [selectedEndpoint, siteId, pathParams, queryParams, parsedBody]);
125102

126-
// Handle execute
127-
const handleExecute = async () => {
128-
if (!selectedEndpoint) return;
129-
130-
// Validate path params
131-
if (selectedEndpoint.pathParams) {
132-
for (const param of selectedEndpoint.pathParams) {
133-
if (!pathParams[param]) {
134-
setResponseError(`Missing required path parameter: ${param}`);
135-
return;
136-
}
137-
}
138-
}
139-
140-
// Validate required query params
141-
if (selectedEndpoint.requiredParams) {
142-
for (const param of selectedEndpoint.requiredParams) {
143-
if (!endpointParams[param]) {
144-
setResponseError(`Missing required parameter: ${param}`);
145-
return;
146-
}
147-
}
148-
}
149-
150-
// Validate request body for POST/PUT
151-
if (selectedEndpoint.hasRequestBody && requestBody) {
152-
try {
153-
JSON.parse(requestBody);
154-
} catch {
155-
setResponseError("Invalid JSON in request body");
156-
return;
157-
}
158-
}
159-
160-
setIsLoading(true);
161-
const startTime = performance.now();
162-
163-
try {
164-
// Build the path
165-
let path = selectedEndpoint.path.replace(":site", siteId);
166-
if (selectedEndpoint.pathParams) {
167-
for (const param of selectedEndpoint.pathParams) {
168-
path = path.replace(`:${param}`, pathParams[param]);
169-
}
170-
}
171-
172-
// Make the request
173-
const result = await authedFetch<any>(
174-
path,
175-
queryParams,
176-
selectedEndpoint.method !== "GET"
177-
? {
178-
method: selectedEndpoint.method,
179-
data: parsedBody,
180-
}
181-
: undefined
182-
);
183-
184-
const endTime = performance.now();
185-
setResponse(result, Math.round(endTime - startTime));
186-
} catch (err: any) {
187-
setResponseError(err.message || "Request failed");
188-
}
189-
};
190-
191103
if (!selectedEndpoint) {
192104
return (
193105
<div className="h-full flex items-center justify-center text-neutral-500 dark:text-neutral-400 p-4">
@@ -198,23 +110,7 @@ export function ResponsePanel() {
198110

199111
return (
200112
<div className="h-full overflow-y-auto overflow-x-hidden p-4 space-y-4 min-w-0">
201-
{/* Execute Button */}
202-
<Button onClick={handleExecute} disabled={isLoading} className="w-full">
203-
{isLoading ? (
204-
<>
205-
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
206-
Executing...
207-
</>
208-
) : (
209-
<>
210-
<Play className="h-4 w-4 mr-2" />
211-
Execute Request
212-
</>
213-
)}
214-
</Button>
215-
216113
{/* Code Examples */}
217-
218114
<CodeExamples config={codeConfig} />
219115

220116
{/* Response */}

0 commit comments

Comments
 (0)