Skip to content

Commit 25b0ce5

Browse files
committed
add the remaining viewers
1 parent 432b8cd commit 25b0ce5

File tree

10 files changed

+180
-61
lines changed

10 files changed

+180
-61
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createEffect, createSignal } from "solid-js";
2+
3+
async function ping(file: File) {
4+
"use server";
5+
return await file.text();
6+
}
7+
8+
export default function App() {
9+
const [output, setOutput] = createSignal<{ result?: boolean }>({});
10+
11+
createEffect(async () => {
12+
const file = new File(['Hello, World!'], 'hello-world.txt');
13+
const result = await ping(file);
14+
const value = await file.text();
15+
setOutput(prev => ({ ...prev, result: value === result }));
16+
});
17+
18+
return (
19+
<main>
20+
<span id="server-fn-test">{JSON.stringify(output())}</span>
21+
</main>
22+
);
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[data-start-blob-viewer] svg {
2+
width: 1rem;
3+
height: 1rem;
4+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createMemo, createResource, type JSX, onCleanup, Show, Suspense } from 'solid-js';
2+
3+
import { Badge } from "../ui/Badge";
4+
import Button from "../ui/Button";
5+
6+
import './BlobViewer.css';
7+
8+
9+
function DocumentIcon(
10+
props: JSX.IntrinsicElements["svg"] & { title: string },
11+
): JSX.Element {
12+
return (
13+
<svg
14+
xmlns="http://www.w3.org/2000/svg"
15+
fill="none"
16+
viewBox="0 0 24 24"
17+
stroke="currentColor"
18+
{...props}
19+
>
20+
<title>{props.title}</title>
21+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
22+
</svg>
23+
);
24+
}
25+
26+
interface BlobViewerInnerProps {
27+
source: File | Blob;
28+
}
29+
30+
function BlobViewerInner(props: BlobViewerInnerProps): JSX.Element {
31+
const fileURL = createMemo(() => URL.createObjectURL(props.source));
32+
33+
onCleanup(() => {
34+
URL.revokeObjectURL(fileURL());
35+
});
36+
37+
function openFileInNewTab() {
38+
const link = document.createElement("a");
39+
link.href = fileURL();
40+
link.target = "_blank"; // Open in a new tab
41+
link.style.display = "none"; // Hide the link
42+
document.body.appendChild(link);
43+
link.click();
44+
document.body.removeChild(link);
45+
}
46+
47+
return (
48+
<Button data-start-blob-viewer onClick={() => openFileInNewTab()}>
49+
{props.source instanceof File
50+
? (
51+
<Badge type="info">
52+
<DocumentIcon title={props.source.name} />
53+
{props.source.name}
54+
</Badge>
55+
)
56+
: <Badge type="info">{props.source.type}</Badge>
57+
}
58+
</Button>
59+
)
60+
}
61+
62+
63+
export interface BlobViewerProps {
64+
source: Blob | File | Promise<Blob | File>;
65+
}
66+
67+
68+
export function BlobViewer(props: BlobViewerProps): JSX.Element {
69+
const [data] = createResource(() => props.source);
70+
71+
return (
72+
<Suspense>
73+
<Show when={data()}>
74+
{(current) => <BlobViewerInner source={current()} />}
75+
</Show>
76+
</Suspense>
77+
);
78+
}

packages/start/src/shared/server-function-inspector/FormDataViewer.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/start/src/shared/server-function-inspector/FormDataViewer.tsx

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,14 @@
1-
import { createResource, For, Show, Suspense, type JSX } from 'solid-js';
2-
3-
import { SerovalValue } from './SerovalValue.tsx';
1+
import { createResource, For, type JSX, Show, Suspense } from 'solid-js';
42
import { PropertySeparator } from '../ui/Properties.tsx';
53
import { Section } from '../ui/Section';
6-
import Button from '../ui/Button';
7-
import { Badge } from '../ui/Badge.tsx';
8-
9-
10-
import './FormDataViewer.css';
11-
12-
function DocumentIcon(
13-
props: JSX.IntrinsicElements["svg"] & { title: string },
14-
): JSX.Element {
15-
return (
16-
<svg
17-
xmlns="http://www.w3.org/2000/svg"
18-
fill="none"
19-
viewBox="0 0 24 24"
20-
stroke="currentColor"
21-
{...props}
22-
>
23-
<title>{props.title}</title>
24-
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
25-
</svg>
26-
);
27-
}
4+
import { BlobViewer } from './BlobViewer.tsx';
5+
import { SerovalValue } from './SerovalValue.tsx';
286

297
interface FormDataViewerInnerProps {
308
source: FormData;
319
}
3210

3311
function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element {
34-
function openFileInNewTab(file: File) {
35-
const fileURL = URL.createObjectURL(file);
36-
const link = document.createElement("a");
37-
link.href = fileURL;
38-
link.target = "_blank"; // Open in a new tab
39-
link.style.display = "none"; // Hide the link
40-
document.body.appendChild(link);
41-
link.click();
42-
document.body.removeChild(link);
43-
}
44-
4512
return (
4613
<Section title="FormData" options={{ size: 'sm' }}>
4714
<div data-start-form-data-viewer data-start-seroval-properties>
@@ -51,15 +18,8 @@ function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element {
5118
<SerovalValue value={`"${key}"`} />
5219
<PropertySeparator />
5320
{typeof value === 'string'
54-
? <SerovalValue value={JSON.stringify(value)} />
55-
: (
56-
<Button onClick={() => openFileInNewTab(value)}>
57-
<Badge type="info">
58-
<DocumentIcon title={value.name} />
59-
{value.name}
60-
</Badge>
61-
</Button>
62-
)}
21+
? <SerovalValue value={`"${JSON.stringify(value)}"`} />
22+
: <BlobViewer source={value} />}
6323
</div>
6424
)}
6525
</For>

packages/start/src/shared/server-function-inspector/SerovalViewer.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
display: flex;
2828
flex-direction: column;
2929
gap: 0.25rem;
30-
padding: 0.25rem;
30+
padding: 0.5rem;
3131

3232
width: 100%;
3333
max-width: 16rem;
@@ -67,7 +67,6 @@
6767
display: flex;
6868
flex-direction: column;
6969
gap: 0.25rem;
70-
margin: 0rem 0.25rem;
7170
}
7271

7372
[data-start-seroval-property] {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createResource, For, type JSX, Show, Suspense } from 'solid-js';
2+
import { PropertySeparator } from '../ui/Properties.tsx';
3+
import { Section } from '../ui/Section';
4+
import { SerovalValue } from './SerovalValue.tsx';
5+
6+
interface URLSearchParamsViewerInnerProps {
7+
source: URLSearchParams;
8+
}
9+
10+
function URLSearchParamsViewerInner(props: URLSearchParamsViewerInnerProps): JSX.Element {
11+
return (
12+
<Section title="URLSearchParams" options={{ size: 'sm' }}>
13+
<div data-start-seroval-properties>
14+
<For each={Array.from(props.source.entries())}>
15+
{([key, value]) => (
16+
<div data-start-seroval-property>
17+
<SerovalValue value={`"${key}"`} />
18+
<PropertySeparator />
19+
<SerovalValue value={`"${JSON.stringify(value)}"`} />
20+
</div>
21+
)}
22+
</For>
23+
</div>
24+
</Section>
25+
);
26+
}
27+
28+
export interface URLSearchParamsViewerProps {
29+
source: URLSearchParams | Promise<URLSearchParams>;
30+
}
31+
32+
export function URLSearchParamsViewer(props: URLSearchParamsViewerProps) {
33+
const [data] = createResource(() => props.source);
34+
35+
return (
36+
<Suspense>
37+
<Show when={data()}>
38+
{(current) => <URLSearchParamsViewerInner source={current()} />}
39+
</Show>
40+
</Suspense>
41+
);
42+
}

packages/start/src/shared/server-function-inspector/index.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import {
88
} from "solid-js";
99
import { createStore } from "solid-js/store";
1010
import { Portal } from "solid-js/web";
11-
import { BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts";
11+
import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts";
1212
import { Badge } from "../ui/Badge.tsx";
1313
import Button from "../ui/Button.tsx";
1414
import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx";
1515
import { Section } from "../ui/Section.tsx";
1616
import { Select, SelectOption } from "../ui/Select.tsx";
1717
import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx";
18+
import { BlobViewer } from "./BlobViewer.tsx";
1819
import { FormDataViewer } from "./FormDataViewer.tsx";
1920
import { HeadersViewer } from "./HeadersViewer.tsx";
2021
import { HexViewer } from "./HexViewer.tsx";
@@ -25,6 +26,21 @@ import {
2526
type ServerFunctionResponse,
2627
} from "./server-function-tracker.ts";
2728
import "./styles.css";
29+
import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx";
30+
31+
async function getFile(source: Response | Request): Promise<File> {
32+
const formData = await source.formData();
33+
const file = formData.get(BODY_FORMAL_FILE);
34+
if (!(file && file instanceof File)) {
35+
throw new Error('invalid file input');
36+
}
37+
return file;
38+
}
39+
40+
async function getURLSearchParams(source: Response | Request): Promise<URLSearchParams> {
41+
const text = await source.text();
42+
return new URLSearchParams(text);
43+
}
2844

2945
interface ContentViewerProps {
3046
source: ServerFunctionRequest | ServerFunctionResponse;
@@ -46,20 +62,18 @@ function ContentViewer(props: ContentViewerProps): JSX.Element {
4662
case startType === BodyFormat.Seroval:
4763
return <SerovalViewer stream={source} />;
4864
case startType === BodyFormat.String:
49-
return undefined;
50-
case startType === BodyFormat.File: {
51-
return undefined;
52-
}
65+
return <HexViewer bytes={source.bytes()} />;
66+
case startType === BodyFormat.File:
67+
return <BlobViewer source={getFile(source)} />;
5368
case startType === BodyFormat.FormData:
5469
case contentType?.startsWith("multipart/form-data"):
5570
return <FormDataViewer source={source.formData()} />;
5671
case startType === BodyFormat.URLSearchParams:
5772
case contentType?.startsWith("application/x-www-form-urlencoded"):
58-
return undefined;
73+
return <URLSearchParamsViewer source={getURLSearchParams(source)} />;
5974
case startType === BodyFormat.Blob:
60-
return undefined;
75+
return <BlobViewer source={source.blob()} />;
6176
case startType === BodyFormat.ArrayBuffer:
62-
return undefined;
6377
case startType === BodyFormat.Uint8Array:
6478
return <HexViewer bytes={source.bytes()} />;
6579
}

packages/start/src/shared/ui/Section.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111

1212
[data-start-section-content] {
13-
width: 100%;
13+
padding-left: 0.5rem;
14+
border-left: 1px oklch(70.7% 0.165 254.624) solid;
15+
16+
width: calc(100% - 0.5rem);
1417

1518
overflow: auto;
1619
}

packages/start/src/shared/ui/Tabs.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
}
4242

4343
[data-start-tab-panel] {
44-
padding: 0.5rem;
44+
padding: 1rem;
4545
background-color: rgb(249 250 251);
4646
border-radius: 0.5rem;
4747
height: 100%;

0 commit comments

Comments
 (0)