Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion packages/web/src/components/editor/EditorWithExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { ExecutionDetail } from '../execution/ExecutionDetail';
import {
DEFAULT_REQUEST_WORKSPACE_TAB,
type RequestWorkspaceTabId,
RequestWorkspaceTabs
RequestWorkspaceTabs,
useRequestParseDetails
} from '../request-workspace';
import { ScriptPanel } from '../script';
import { CodeEditor } from './CodeEditor';
Expand Down Expand Up @@ -68,6 +69,11 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
}
return allRequests[selectedRequestIndex()];
});
const requestParseDetails = useRequestParseDetails({
client: () => connection.client,
path: () => props.path,
requestIndex: () => selectedRequest()?.index
});

const saveCollapsedState = (collapsed: boolean) => {
if (typeof localStorage !== 'undefined') {
Expand Down Expand Up @@ -207,6 +213,10 @@ export const EditorWithExecution: Component<EditorWithExecutionProps> = (props)
onTabChange={setActiveRequestTab}
selectedRequest={selectedRequest()}
requestCount={requests().length}
requestHeaders={requestParseDetails.headers()}
requestBodySummary={requestParseDetails.bodySummary()}
requestDetailsLoading={requestParseDetails.loading()}
requestDetailsError={requestParseDetails.error()}
/>
<div class="flex-1 min-h-0">
<HttpEditor path={props.path} onExecute={handleHttpExecute} />
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/request-workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export {
type RequestWorkspaceTabId
} from './model';
export { RequestWorkspaceTabs } from './request-workspace-tabs';
export { useRequestParseDetails } from './use-request-parse-details';
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { For, Match, Show, Switch } from 'solid-js';
import { createMemo, For, Match, Show, Switch } from 'solid-js';
import type { WorkspaceRequest } from '../../sdk';
import type { RequestBodySummary, RequestDetailsRow } from '../../utils/request-details';
import { toRequestParams } from '../../utils/request-details';
import { REQUEST_WORKSPACE_TABS, type RequestWorkspaceTabId } from './model';

interface RequestWorkspaceTabsProps {
activeTab: RequestWorkspaceTabId;
onTabChange: (tab: RequestWorkspaceTabId) => void;
selectedRequest?: WorkspaceRequest;
requestCount: number;
requestHeaders: RequestDetailsRow[];
requestBodySummary: RequestBodySummary;
requestDetailsLoading: boolean;
requestDetailsError?: string;
}

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

export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
const requestParams = createMemo(() => {
const request = props.selectedRequest;
if (!request) {
return [];
}
return toRequestParams(request.url);
});

return (
<section
class="border-b border-treq-border-light dark:border-treq-dark-border-light bg-base-100/80"
Expand Down Expand Up @@ -56,15 +70,162 @@ export function RequestWorkspaceTabs(props: RequestWorkspaceTabsProps) {
{(request) => (
<Switch>
<Match when={props.activeTab === 'params'}>
<p>Params editor wiring is next for {request().method.toUpperCase()} requests.</p>
<Show
when={requestParams().length > 0}
fallback={
<p>No query params in URL for {request().method.toUpperCase()} requests.</p>
}
>
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Name</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">Value</th>
</tr>
</thead>
<tbody>
<For each={requestParams()}>
{(param) => (
<tr>
<td class="font-mono text-xs text-base-content">{param.key}</td>
<td class="font-mono text-xs text-base-content/80">
{param.value}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</Match>
<Match when={props.activeTab === 'headers'}>
<p>
Headers editor wiring is next for {request().method.toUpperCase()} requests.
</p>
<Show
when={!props.requestDetailsLoading}
fallback={<p>Loading request headers…</p>}
>
<Show
when={!props.requestDetailsError}
fallback={<p>{props.requestDetailsError}</p>}
>
<Show
when={props.requestHeaders.length > 0}
fallback={<p>No headers were parsed for this request.</p>}
>
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
Name
</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
Value
</th>
</tr>
</thead>
<tbody>
<For each={props.requestHeaders}>
{(header) => (
<tr>
<td class="font-mono text-xs text-base-content">
{header.key}
</td>
<td class="font-mono text-xs text-base-content/80">
{header.value}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</Show>
</Show>
</Match>
<Match when={props.activeTab === 'body'}>
<p>Body editor wiring is next for {request().method.toUpperCase()} requests.</p>
<Show when={!props.requestDetailsLoading} fallback={<p>Loading request body…</p>}>
<Show
when={!props.requestDetailsError}
fallback={<p>{props.requestDetailsError}</p>}
>
<div class="space-y-2">
<p>{props.requestBodySummary.description}</p>

<Switch>
<Match
when={
props.requestBodySummary.kind === 'inline' &&
props.requestBodySummary.text !== undefined
}
>
<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">
{props.requestBodySummary.text}
</pre>
</Match>

<Match when={props.requestBodySummary.kind === 'form-data'}>
<Show
when={(props.requestBodySummary.fields?.length ?? 0) > 0}
fallback={<p>No form-data fields were parsed.</p>}
>
<div class="overflow-auto rounded-box border border-base-300 bg-base-100/80">
<table class="table table-xs">
<thead>
<tr>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
Name
</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
Type
</th>
<th class="font-mono uppercase tracking-[0.06em] text-[11px]">
Value
</th>
</tr>
</thead>
<tbody>
<For each={props.requestBodySummary.fields}>
{(field) => (
<tr>
<td class="font-mono text-xs text-base-content">
{field.name}
</td>
<td class="font-mono text-xs text-base-content/80">
{field.isFile ? 'file' : 'text'}
</td>
<td class="font-mono text-xs text-base-content/80">
{field.isFile
? (field.path ?? field.filename ?? field.value)
: field.value}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</Show>
</Match>

<Match when={props.requestBodySummary.kind === 'file'}>
<Show
when={props.requestBodySummary.filePath}
fallback={<p>No request body file path was parsed.</p>}
>
{(filePath) => (
<div class="rounded-box border border-base-300 bg-base-100/80 p-2">
<p class="font-mono text-xs text-base-content/80">{filePath()}</p>
</div>
)}
</Show>
</Match>
</Switch>
</div>
</Show>
</Show>
</Match>
</Switch>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type PostParseResponses, type TreqClient, unwrap } from '@t-req/sdk/client';
import { createMemo, createResource } from 'solid-js';
import {
findRequestBlock,
type ParseRequestBlock,
type RequestBodySummary,
type RequestDetailsRow,
toRequestBodySummary,
toRequestHeaders
} from '../../utils/request-details';

type ParseRequestDetailsResponse = PostParseResponses[200];

interface ParseRequestDetailsSource {
client: TreqClient;
path: string;
requestIndex: number;
}

interface ParseRequestDetailsResult {
source: ParseRequestDetailsSource;
response: ParseRequestDetailsResponse;
}

interface UseRequestParseDetailsOptions {
client: () => TreqClient | null;
path: () => string;
requestIndex: () => number | undefined;
}

interface UseRequestParseDetailsReturn {
requestBlock: () => ParseRequestBlock | undefined;
headers: () => RequestDetailsRow[];
bodySummary: () => RequestBodySummary;
loading: () => boolean;
error: () => string | undefined;
refetch: () => void;
}

const DEFAULT_PARSE_ERROR = 'Unable to load request details.';

export function useRequestParseDetails(
options: UseRequestParseDetailsOptions
): UseRequestParseDetailsReturn {
const source = createMemo<ParseRequestDetailsSource | null>(() => {
const client = options.client();
const path = options.path();
const requestIndex = options.requestIndex();
if (!client || !path || requestIndex === undefined) {
return null;
}
return {
client,
path,
requestIndex
};
});

const [parseResult, { refetch }] = createResource(
source,
async (current): Promise<ParseRequestDetailsResult> => {
const response = await unwrap(
current.client.postParse({
body: {
path: current.path,
includeDiagnostics: true,
includeBodyContent: true
}
})
);

return {
source: current,
response
};
}
);

const requestBlock = createMemo(() => {
const result = parseResult();
if (!result) {
return undefined;
}
return findRequestBlock(result.response.requests, result.source.requestIndex);
});

const headers = createMemo(() => {
const request = requestBlock()?.request;
if (!request) {
return [];
}
return toRequestHeaders(request.headers);
});

const bodySummary = createMemo(() => toRequestBodySummary(requestBlock()?.request));

const error = createMemo(() => {
const fetchError = parseResult.error;
if (!fetchError) {
return undefined;
}
if (fetchError instanceof Error && fetchError.message) {
return fetchError.message;
}
return DEFAULT_PARSE_ERROR;
});

return {
requestBlock,
headers,
bodySummary,
loading: () => parseResult.loading,
error,
refetch
};
}
Loading