Skip to content

Commit 818540d

Browse files
feat(desktop): wire RequestDetailsPanel with live parsed request data (#88)
1 parent 07917ea commit 818540d

File tree

4 files changed

+551
-57
lines changed

4 files changed

+551
-57
lines changed

packages/desktop/src/features/explorer/components/ExplorerScreen.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type PostExecuteResponses, unwrap } from '@t-req/sdk/client';
2-
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js';
2+
import { createMemo, createResource, createSignal, Match, Show, Switch } from 'solid-js';
33
import { createStore } from 'solid-js/store';
44
import { useServer } from '../../../context/server-context';
55
import { toErrorMessage } from '../../../lib/errors';
@@ -13,6 +13,12 @@ import { FALLBACK_REQUEST_METHOD, FALLBACK_REQUEST_URL } from '../request-line';
1313
import { useExplorerStore } from '../use-explorer-store';
1414
import { buildCreateFilePath, toCreateHttpPath } from '../utils/mutations';
1515
import { parentDirectory } from '../utils/path';
16+
import {
17+
findRequestBlock,
18+
toRequestBodySummary,
19+
toRequestHeaders,
20+
toRequestParams
21+
} from '../utils/request-details';
1622
import { isHttpProtocol, type RequestOption, toRequestOption } from '../utils/request-workspace';
1723
import { CreateRequestDialog } from './CreateRequestDialog';
1824
import { ExplorerToolbar } from './ExplorerToolbar';
@@ -80,11 +86,67 @@ export default function ExplorerScreen() {
8086
const targetIndex = selectedRequestIndex();
8187
return requests.find((request) => request.index === targetIndex) ?? requests[0];
8288
});
89+
const parseSource = createMemo(() => {
90+
const client = server.client();
91+
const path = selectedPath();
92+
if (!client || !path) {
93+
return null;
94+
}
95+
return {
96+
client,
97+
path
98+
};
99+
});
100+
const [parsedRequestFile] = createResource(parseSource, async (context) => {
101+
return await unwrap(
102+
context.client.postParse({
103+
body: {
104+
path: context.path,
105+
includeDiagnostics: true
106+
}
107+
})
108+
);
109+
});
110+
const requestDetailsError = createMemo(() => {
111+
if (!parseSource() || !parsedRequestFile.error) {
112+
return undefined;
113+
}
114+
return `Failed to parse request details: ${toErrorMessage(parsedRequestFile.error)}`;
115+
});
116+
const isRequestDetailsLoading = createMemo(
117+
() => Boolean(parseSource()) && parsedRequestFile.loading
118+
);
119+
const selectedRequestBlock = createMemo(() => {
120+
const request = selectedRequest();
121+
if (!request) {
122+
return undefined;
123+
}
124+
return findRequestBlock(parsedRequestFile()?.requests ?? [], request.index);
125+
});
83126
const requestOptions = createMemo<RequestOption[]>(() => selectedRequests().map(toRequestOption));
84127
const requestMethod = createMemo(
85128
() => selectedRequest()?.method.toUpperCase() ?? FALLBACK_REQUEST_METHOD
86129
);
87130
const requestUrl = createMemo(() => selectedRequest()?.url ?? FALLBACK_REQUEST_URL);
131+
const requestParams = createMemo(() => {
132+
const request = selectedRequest();
133+
if (!request) {
134+
return [];
135+
}
136+
return toRequestParams(request.url);
137+
});
138+
const requestHeaders = createMemo(() => {
139+
const parsedRequest = selectedRequestBlock()?.request;
140+
if (!parsedRequest) {
141+
return [];
142+
}
143+
return toRequestHeaders(parsedRequest.headers);
144+
});
145+
const requestBodySummary = createMemo(() =>
146+
toRequestBodySummary(selectedRequestBlock()?.request)
147+
);
148+
const requestDiagnostics = createMemo(() => selectedRequestBlock()?.diagnostics ?? []);
149+
const fileDiagnostics = createMemo(() => parsedRequestFile()?.diagnostics ?? []);
88150
const isUnsupportedProtocol = createMemo(() => !isHttpProtocol(selectedRequest()?.protocol));
89151
const isBusy = createMemo(() => explorer.isMutating());
90152
const sendDisabled = createMemo(() => {
@@ -423,7 +485,16 @@ export default function ExplorerScreen() {
423485
class="grid min-h-0 min-w-0 flex-1 overflow-hidden grid-cols-[var(--request-panels-cols)] gap-0"
424486
style={requestPanelsStyle()}
425487
>
426-
<RequestDetailsPanel />
488+
<RequestDetailsPanel
489+
hasRequest={Boolean(selectedRequest())}
490+
params={requestParams()}
491+
headers={requestHeaders()}
492+
bodySummary={requestBodySummary()}
493+
diagnostics={requestDiagnostics()}
494+
fileDiagnostics={fileDiagnostics()}
495+
isLoading={isRequestDetailsLoading()}
496+
error={requestDetailsError()}
497+
/>
427498
<Show
428499
when={!isResponseCollapsed()}
429500
fallback={

packages/desktop/src/features/explorer/components/workspace/RequestDetailsPanel.tsx

Lines changed: 194 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,67 @@
1-
import { createSignal, Match, Switch } from 'solid-js';
1+
import { createMemo, createSignal, For, Match, Show, Switch } from 'solid-js';
2+
import {
3+
formatDiagnosticLocation,
4+
type ParseDiagnostic,
5+
type RequestBodySummary,
6+
type RequestDetailsRow
7+
} from '../../utils/request-details';
28

3-
type RequestDetailsTab = 'params' | 'body' | 'headers';
9+
type RequestDetailsTab = 'params' | 'body' | 'headers' | 'diagnostics';
410

5-
export function RequestDetailsPanel() {
11+
type RequestDetailsPanelProps = {
12+
hasRequest: boolean;
13+
params: RequestDetailsRow[];
14+
headers: RequestDetailsRow[];
15+
bodySummary: RequestBodySummary;
16+
diagnostics: ParseDiagnostic[];
17+
fileDiagnostics: ParseDiagnostic[];
18+
isLoading?: boolean;
19+
error?: string;
20+
};
21+
22+
function diagnosticSeverityClass(severity: ParseDiagnostic['severity']): string {
23+
const base = 'badge badge-xs font-mono uppercase tracking-[0.04em]';
24+
switch (severity) {
25+
case 'error':
26+
return `${base} badge-error`;
27+
case 'warning':
28+
return `${base} badge-warning`;
29+
default:
30+
return `${base} badge-info`;
31+
}
32+
}
33+
34+
export function RequestDetailsPanel(props: RequestDetailsPanelProps) {
635
const [activeTab, setActiveTab] = createSignal<RequestDetailsTab>('params');
36+
const visibleDiagnostics = createMemo(() => {
37+
if (props.diagnostics.length > 0) {
38+
return props.diagnostics;
39+
}
40+
return props.fileDiagnostics;
41+
});
742

843
return (
944
<section class="min-h-0 min-w-0 flex flex-col overflow-hidden border-r border-base-300 bg-base-200/10">
10-
<header class="border-b border-base-300/80 px-3 py-2.5">
45+
<header class="flex items-center justify-between gap-2 border-b border-base-300/80 px-3 py-2.5">
1146
<h3 class="m-0 text-sm font-semibold text-base-content">Request Details</h3>
47+
<div class="flex items-center gap-2">
48+
<Show when={props.isLoading}>
49+
<span class="badge badge-sm badge-warning font-mono">Parsing…</span>
50+
</Show>
51+
<Show when={!props.isLoading && props.error}>
52+
<span class="badge badge-sm badge-error font-mono">Unavailable</span>
53+
</Show>
54+
</div>
1255
</header>
1356

57+
<Show when={props.error}>
58+
{(message) => (
59+
<div class="mx-3 mt-2 rounded-box border border-error/40 bg-error/15 px-3 py-2 text-sm text-base-content">
60+
{message()}
61+
</div>
62+
)}
63+
</Show>
64+
1465
<div role="tablist" class="tabs tabs-bordered tabs-md px-3 pt-1">
1566
<button
1667
type="button"
@@ -39,68 +90,156 @@ export function RequestDetailsPanel() {
3990
>
4091
Headers
4192
</button>
93+
<button
94+
type="button"
95+
role="tab"
96+
class="tab"
97+
classList={{ 'tab-active': activeTab() === 'diagnostics' }}
98+
onClick={() => setActiveTab('diagnostics')}
99+
>
100+
Diagnostics
101+
</button>
42102
</div>
43103

44104
<div class="min-h-0 min-w-0 flex-1 overflow-hidden px-3 pb-3 pt-2">
45105
<Switch>
46106
<Match when={activeTab() === 'params'}>
47-
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2">
48-
<table class="table table-sm">
49-
<thead>
50-
<tr>
51-
<th class="font-mono">Name</th>
52-
<th class="font-mono">Value</th>
53-
</tr>
54-
</thead>
55-
<tbody>
56-
<tr>
57-
<td class="font-mono text-base-content/70">limit</td>
58-
<td class="font-mono text-base-content/60">100</td>
59-
</tr>
60-
<tr>
61-
<td class="font-mono text-base-content/70">sort</td>
62-
<td class="font-mono text-base-content/60">desc</td>
63-
</tr>
64-
</tbody>
65-
</table>
66-
</div>
107+
<Show
108+
when={props.hasRequest}
109+
fallback={
110+
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2" />
111+
}
112+
>
113+
<div class="h-full min-w-0 overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2">
114+
<table class="table table-sm table-fixed">
115+
<thead>
116+
<tr>
117+
<th class="font-mono">Name</th>
118+
<th class="font-mono">Value</th>
119+
</tr>
120+
</thead>
121+
<tbody>
122+
<For each={props.params}>
123+
{(param) => (
124+
<tr>
125+
<td class="font-mono break-all text-base-content/70">{param.key}</td>
126+
<td class="font-mono break-all text-base-content/60">{param.value}</td>
127+
</tr>
128+
)}
129+
</For>
130+
</tbody>
131+
</table>
132+
</div>
133+
</Show>
67134
</Match>
68135

69136
<Match when={activeTab() === 'body'}>
70-
<label class="flex h-full flex-col gap-2">
71-
<span class="text-[12px] font-semibold uppercase tracking-[0.05em] text-base-content/60">
72-
JSON Body
73-
</span>
74-
<textarea
75-
class="textarea textarea-sm h-full min-h-[160px] w-full border-base-300 bg-base-100 font-mono text-sm"
76-
value={'{\n "name": "example",\n "enabled": true\n}'}
77-
disabled
78-
aria-label="Request body editor"
79-
/>
80-
</label>
137+
<Show
138+
when={props.hasRequest}
139+
fallback={
140+
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-3" />
141+
}
142+
>
143+
{(() => {
144+
const hasAnyBodySignal =
145+
props.bodySummary.hasBody ||
146+
props.bodySummary.hasFormData ||
147+
props.bodySummary.hasBodyFile;
148+
const bodyKindLabel =
149+
props.bodySummary.kind === 'inline'
150+
? 'Inline Body'
151+
: props.bodySummary.kind === 'form-data'
152+
? 'Form Data'
153+
: props.bodySummary.kind === 'file'
154+
? 'Body File'
155+
: undefined;
156+
157+
return (
158+
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-3">
159+
<Show when={hasAnyBodySignal}>
160+
<div class="flex flex-wrap items-center gap-2">
161+
<Show when={bodyKindLabel}>
162+
{(label) => (
163+
<span class="badge badge-sm border-base-300 bg-base-300/60 font-mono">
164+
{label()}
165+
</span>
166+
)}
167+
</Show>
168+
<Show when={props.bodySummary.hasBody}>
169+
<span class="badge badge-sm badge-success font-mono">hasBody</span>
170+
</Show>
171+
<Show when={props.bodySummary.hasFormData}>
172+
<span class="badge badge-sm badge-success font-mono">hasFormData</span>
173+
</Show>
174+
<Show when={props.bodySummary.hasBodyFile}>
175+
<span class="badge badge-sm badge-success font-mono">hasBodyFile</span>
176+
</Show>
177+
</div>
178+
</Show>
179+
</div>
180+
);
181+
})()}
182+
</Show>
81183
</Match>
82184

83185
<Match when={activeTab() === 'headers'}>
84-
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2">
85-
<table class="table table-sm">
86-
<thead>
87-
<tr>
88-
<th class="font-mono">Header</th>
89-
<th class="font-mono">Value</th>
90-
</tr>
91-
</thead>
92-
<tbody>
93-
<tr>
94-
<td class="font-mono text-base-content/70">Accept</td>
95-
<td class="font-mono text-base-content/60">application/json</td>
96-
</tr>
97-
<tr>
98-
<td class="font-mono text-base-content/70">Authorization</td>
99-
<td class="font-mono text-base-content/60">Bearer {'{{token}}'}</td>
100-
</tr>
101-
</tbody>
102-
</table>
103-
</div>
186+
<Show
187+
when={props.hasRequest}
188+
fallback={
189+
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2" />
190+
}
191+
>
192+
<div class="h-full min-w-0 overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2">
193+
<table class="table table-sm table-fixed">
194+
<thead>
195+
<tr>
196+
<th class="font-mono">Header</th>
197+
<th class="font-mono">Value</th>
198+
</tr>
199+
</thead>
200+
<tbody>
201+
<For each={props.headers}>
202+
{(header) => (
203+
<tr>
204+
<td class="font-mono break-all text-base-content/70">{header.key}</td>
205+
<td class="font-mono break-all text-base-content/60">{header.value}</td>
206+
</tr>
207+
)}
208+
</For>
209+
</tbody>
210+
</table>
211+
</div>
212+
</Show>
213+
</Match>
214+
215+
<Match when={activeTab() === 'diagnostics'}>
216+
<Show
217+
when={props.hasRequest}
218+
fallback={
219+
<div class="h-full overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2" />
220+
}
221+
>
222+
<ul class="h-full space-y-2 overflow-auto rounded-box border border-base-300 bg-base-100/80 p-2">
223+
<For each={visibleDiagnostics()}>
224+
{(diagnostic) => (
225+
<li class="rounded-box border border-base-300/80 bg-base-200/35 px-2.5 py-2">
226+
<div class="flex flex-wrap items-center gap-2">
227+
<span class={diagnosticSeverityClass(diagnostic.severity)}>
228+
{diagnostic.severity}
229+
</span>
230+
<span class="font-mono text-[11px] text-base-content/55">
231+
{diagnostic.code}
232+
</span>
233+
<span class="font-mono text-[11px] text-base-content/50">
234+
{formatDiagnosticLocation(diagnostic)}
235+
</span>
236+
</div>
237+
<p class="mt-1 text-sm text-base-content/80">{diagnostic.message}</p>
238+
</li>
239+
)}
240+
</For>
241+
</ul>
242+
</Show>
104243
</Match>
105244
</Switch>
106245
</div>

0 commit comments

Comments
 (0)