Skip to content

Commit 77c6a0d

Browse files
feat(web): add request parse details to workspace tabs (#106)
* feat(web): add request parse details to workspace tabs Add support for displaying parsed request details in the request workspace UI. Includes: - New request-details utilities for parsing params, headers, and body - useRequestParseDetails hook for fetching parse data from the backend - Enhanced RequestWorkspaceTabs with tables for headers, body, and params - Unit tests for request detail parsing logic * update bun types * fix(web): avoid redundant parse refetches and polish request body details UI * PR comments
1 parent 71a5b46 commit 77c6a0d

File tree

7 files changed

+729
-13
lines changed

7 files changed

+729
-13
lines changed

bun.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/web/src/components/editor/EditorWithExecution.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { ExecutionDetail } from '../execution/ExecutionDetail';
2121
import {
2222
DEFAULT_REQUEST_WORKSPACE_TAB,
2323
type RequestWorkspaceTabId,
24-
RequestWorkspaceTabs
24+
RequestWorkspaceTabs,
25+
useRequestParseDetails
2526
} from '../request-workspace';
2627
import { ScriptPanel } from '../script';
2728
import { CodeEditor } from './CodeEditor';
@@ -68,6 +69,11 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
6869
}
6970
return allRequests[selectedRequestIndex()];
7071
});
72+
const requestParseDetails = useRequestParseDetails({
73+
client: () => connection.client,
74+
path: () => props.path,
75+
requestIndex: () => selectedRequest()?.index
76+
});
7177

7278
const saveCollapsedState = (collapsed: boolean) => {
7379
if (typeof localStorage !== 'undefined') {
@@ -207,6 +213,10 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
207213
onTabChange={setActiveRequestTab}
208214
selectedRequest={selectedRequest()}
209215
requestCount={requests().length}
216+
requestHeaders={requestParseDetails.headers()}
217+
requestBodySummary={requestParseDetails.bodySummary()}
218+
requestDetailsLoading={requestParseDetails.loading()}
219+
requestDetailsError={requestParseDetails.error()}
210220
/>
211221
<div class="flex-1 min-h-0">
212222
<HttpEditor path={props.path} onExecute={handleHttpExecute} />

packages/web/src/components/request-workspace/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export {
55
type RequestWorkspaceTabId
66
} from './model';
77
export { RequestWorkspaceTabs } from './request-workspace-tabs';
8+
export { useRequestParseDetails } from './use-request-parse-details';

packages/web/src/components/request-workspace/request-workspace-tabs.tsx

Lines changed: 167 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { For, Match, Show, Switch } from 'solid-js';
1+
import { createMemo, For, Match, Show, Switch } from 'solid-js';
22
import type { WorkspaceRequest } from '../../sdk';
3+
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';
4+
import { toRequestParams } from '../../utils/request-details';
35
import { REQUEST_WORKSPACE_TABS, type RequestWorkspaceTabId } from './model';
46

57
interface RequestWorkspaceTabsProps {
68
activeTab: RequestWorkspaceTabId;
79
onTabChange: (tab: RequestWorkspaceTabId) => void;
810
selectedRequest?: WorkspaceRequest;
911
requestCount: number;
12+
requestHeaders: RequestDetailsRow[];
13+
requestBodySummary: RequestBodySummary;
14+
requestDetailsLoading: boolean;
15+
requestDetailsError?: string;
1016
}
1117

1218
const TAB_LABELS: Record<RequestWorkspaceTabId, string> = {
@@ -16,6 +22,14 @@ const TAB_LABELS: Record<RequestWorkspaceTabId, string> = {
1622
};
1723

1824
export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
25+
const requestParams = createMemo(() => {
26+
const request = props.selectedRequest;
27+
if (!request) {
28+
return [];
29+
}
30+
return toRequestParams(request.url);
31+
});
32+
1933
return (
2034
<section
2135
class="border-b border-treq-border-light dark:border-treq-dark-border-light bg-base-100/80"
@@ -56,15 +70,162 @@ export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
5670
{(request) => (
5771
<Switch>
5872
<Match when={props.activeTab === 'params'}>
59-
<p>Params editor wiring is next for {request().method.toUpperCase()} requests.</p>
73+
<Show
74+
when={requestParams().length > 0}
75+
fallback={
76+
<p>No query params in URL for {request().method.toUpperCase()} requests.</p>
77+
}
78+
>
79+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
80+
<table class="table table-xs">
81+
<thead>
82+
<tr>
83+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
84+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
85+
</tr>
86+
</thead>
87+
<tbody>
88+
<For each={requestParams()}>
89+
{(param) => (
90+
<tr>
91+
<td class="font-mono text-xs text-base-content">{param.key}</td>
92+
<td class="font-mono text-xs text-base-content/80">
93+
{param.value}
94+
</td>
95+
</tr>
96+
)}
97+
</For>
98+
</tbody>
99+
</table>
100+
</div>
101+
</Show>
60102
</Match>
61103
<Match when={props.activeTab === 'headers'}>
62-
<p>
63-
Headers editor wiring is next for {request().method.toUpperCase()} requests.
64-
</p>
104+
<Show
105+
when={!props.requestDetailsLoading}
106+
fallback={<p>Loading request headers…</p>}
107+
>
108+
<Show
109+
when={!props.requestDetailsError}
110+
fallback={<p>{props.requestDetailsError}</p>}
111+
>
112+
<Show
113+
when={props.requestHeaders.length > 0}
114+
fallback={<p>No headers were parsed for this request.</p>}
115+
>
116+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
117+
<table class="table table-xs">
118+
<thead>
119+
<tr>
120+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
121+
Name
122+
</th>
123+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
124+
Value
125+
</th>
126+
</tr>
127+
</thead>
128+
<tbody>
129+
<For each={props.requestHeaders}>
130+
{(header) => (
131+
<tr>
132+
<td class="font-mono text-xs text-base-content">
133+
{header.key}
134+
</td>
135+
<td class="font-mono text-xs text-base-content/80">
136+
{header.value}
137+
</td>
138+
</tr>
139+
)}
140+
</For>
141+
</tbody>
142+
</table>
143+
</div>
144+
</Show>
145+
</Show>
146+
</Show>
65147
</Match>
66148
<Match when={props.activeTab === 'body'}>
67-
<p>Body editor wiring is next for {request().method.toUpperCase()} requests.</p>
149+
<Show when={!props.requestDetailsLoading} fallback={<p>Loading request body…</p>}>
150+
<Show
151+
when={!props.requestDetailsError}
152+
fallback={<p>{props.requestDetailsError}</p>}
153+
>
154+
<div class="space-y-2">
155+
<p>{props.requestBodySummary.description}</p>
156+
157+
<Switch>
158+
<Match
159+
when={
160+
props.requestBodySummary.kind === 'inline' &&
161+
props.requestBodySummary.text !== undefined
162+
}
163+
>
164+
<pre class="max-h-52 overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2 font-mono text-xs text-base-content">
165+
{props.requestBodySummary.text}
166+
</pre>
167+
</Match>
168+
169+
<Match when={props.requestBodySummary.kind === 'form-data'}>
170+
<Show
171+
when={(props.requestBodySummary.fields?.length ?? 0) > 0}
172+
fallback={<p>No form-data fields were parsed.</p>}
173+
>
174+
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
175+
<table class="table table-xs">
176+
<thead>
177+
<tr>
178+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
179+
Name
180+
</th>
181+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
182+
Type
183+
</th>
184+
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
185+
Value
186+
</th>
187+
</tr>
188+
</thead>
189+
<tbody>
190+
<For each={props.requestBodySummary.fields}>
191+
{(field) => (
192+
<tr>
193+
<td class="font-mono text-xs text-base-content">
194+
{field.name}
195+
</td>
196+
<td class="font-mono text-xs text-base-content/80">
197+
{field.isFile ? 'file' : 'text'}
198+
</td>
199+
<td class="font-mono text-xs text-base-content/80">
200+
{field.isFile
201+
? (field.path ?? field.filename ?? field.value)
202+
: field.value}
203+
</td>
204+
</tr>
205+
)}
206+
</For>
207+
</tbody>
208+
</table>
209+
</div>
210+
</Show>
211+
</Match>
212+
213+
<Match when={props.requestBodySummary.kind === 'file'}>
214+
<Show
215+
when={props.requestBodySummary.filePath}
216+
fallback={<p>No request body file path was parsed.</p>}
217+
>
218+
{(filePath) => (
219+
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
220+
<p class="font-mono text-xs text-base-content/80">{filePath()}</p>
221+
</div>
222+
)}
223+
</Show>
224+
</Match>
225+
</Switch>
226+
</div>
227+
</Show>
228+
</Show>
68229
</Match>
69230
</Switch>
70231
)}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { type PostParseResponses, type TreqClient, unwrap } from '@t-req/sdk/client';
2+
import { createMemo, createResource } from 'solid-js';
3+
import {
4+
findRequestBlock,
5+
type RequestBodySummary,
6+
type RequestDetailsRow,
7+
toRequestBodySummary,
8+
toRequestHeaders
9+
} from '../../utils/request-details';
10+
11+
type ParseRequestDetailsResponse = PostParseResponses[200];
12+
13+
interface ParseRequestDetailsSource {
14+
client: TreqClient;
15+
path: string;
16+
}
17+
18+
interface UseRequestParseDetailsOptions {
19+
client: () => TreqClient | null;
20+
path: () => string;
21+
requestIndex: () => number | undefined;
22+
}
23+
24+
interface UseRequestParseDetailsReturn {
25+
headers: () => RequestDetailsRow[];
26+
bodySummary: () => RequestBodySummary;
27+
loading: () => boolean;
28+
error: () => string | undefined;
29+
}
30+
31+
const DEFAULT_PARSE_ERROR = 'Unable to load request details.';
32+
33+
export function useRequestParseDetails(
34+
options: UseRequestParseDetailsOptions
35+
): UseRequestParseDetailsReturn {
36+
const source = createMemo<ParseRequestDetailsSource | null>(() => {
37+
const client = options.client();
38+
const path = options.path();
39+
if (!client || !path) {
40+
return null;
41+
}
42+
return {
43+
client,
44+
path
45+
};
46+
});
47+
48+
const [parseResult] = createResource(
49+
source,
50+
async (current): Promise<ParseRequestDetailsResponse> => {
51+
return await unwrap(
52+
current.client.postParse({
53+
body: {
54+
path: current.path,
55+
includeDiagnostics: true,
56+
includeBodyContent: true
57+
}
58+
})
59+
);
60+
}
61+
);
62+
63+
const requestBlock = createMemo(() => {
64+
const parsedRequestFile = parseResult();
65+
const requestIndex = options.requestIndex();
66+
if (!parsedRequestFile || requestIndex === undefined) {
67+
return undefined;
68+
}
69+
return findRequestBlock(parsedRequestFile.requests, requestIndex);
70+
});
71+
72+
const headers = createMemo(() => {
73+
const request = requestBlock()?.request;
74+
if (!request) {
75+
return [];
76+
}
77+
return toRequestHeaders(request.headers);
78+
});
79+
80+
const bodySummary = createMemo(() => toRequestBodySummary(requestBlock()?.request));
81+
82+
const error = createMemo(() => {
83+
const fetchError = parseResult.error;
84+
if (!fetchError) {
85+
return undefined;
86+
}
87+
if (fetchError instanceof Error && fetchError.message) {
88+
return fetchError.message;
89+
}
90+
return DEFAULT_PARSE_ERROR;
91+
});
92+
93+
return {
94+
headers,
95+
bodySummary,
96+
loading: () => parseResult.loading,
97+
error
98+
};
99+
}

0 commit comments

Comments
 (0)