Skip to content

Commit 4bf69fa

Browse files
committed
Create HexViewer.tsx
1 parent 9203aba commit 4bf69fa

File tree

4 files changed

+181
-14
lines changed

4 files changed

+181
-14
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[data-start-hex-viewer] {
2+
display: flex;
3+
border: 1px oklch(70.7% 0.165 254.624) solid;
4+
overflow: auto;
5+
}
6+
7+
[data-start-hex-viewer-bytes] {
8+
flex: 1;
9+
display: flex;
10+
flex-direction: column;
11+
gap: 0.5rem;
12+
padding: 0.5rem;
13+
}
14+
15+
[data-start-hex-viewer-text] {
16+
flex: 1;
17+
display: flex;
18+
flex-direction: column;
19+
gap: 0.5rem;
20+
padding: 0.5rem;
21+
border-left: 1px oklch(70.7% 0.165 254.624) solid;
22+
}
23+
24+
[data-start-hex-row] {
25+
display: flex;
26+
align-items: center;
27+
gap: 1rem;
28+
}
29+
30+
[data-start-hex-text] {
31+
display: flex;
32+
align-items: center;
33+
justify-content: start;
34+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { createMemo, createResource, For, type JSX, Show, Suspense } from "solid-js";
2+
import { Text } from "../ui/Text.tsx";
3+
4+
import './HexViewer.css';
5+
6+
7+
function toHex(num: number, digits = 2): string {
8+
return num.toString(16).padStart(digits, '0').toUpperCase();
9+
}
10+
11+
function HexChunk(props: HexViewerInnerProps) {
12+
13+
const content = createMemo(() => {
14+
const byte1 = props.bytes[0] || 0;
15+
const byte2 = props.bytes[1] || 0;
16+
const byte3 = props.bytes[2] || 0;
17+
const byte4 = props.bytes[3] || 0;
18+
19+
return `${toHex(byte1)} ${toHex(byte2)} ${toHex(byte3)} ${toHex(byte4)}`
20+
});
21+
return (
22+
<Text data-start-hex-chunk options={{ size: 'xs', weight: 'bold', wrap: 'nowrap' }}>
23+
{content()}
24+
</Text>
25+
);
26+
}
27+
28+
function HexRow(props: HexViewerInnerProps) {
29+
const chunk1 = createMemo(() => props.bytes.subarray(0, 4));
30+
const chunk2 = createMemo(() => props.bytes.subarray(4, 8));
31+
const chunk3 = createMemo(() => props.bytes.subarray(8, 12));
32+
const chunk4 = createMemo(() => props.bytes.subarray(12, 16));
33+
34+
return (
35+
<div data-start-hex-row>
36+
<HexChunk bytes={chunk1()} />
37+
<HexChunk bytes={chunk2()} />
38+
<HexChunk bytes={chunk3()} />
39+
<HexChunk bytes={chunk4()} />
40+
</div>
41+
);
42+
}
43+
44+
function replaceString(string: string): string {
45+
const result = string.codePointAt(0);
46+
if (result == null) {
47+
return string;
48+
}
49+
return String.fromCodePoint(result + 0x2400);
50+
}
51+
52+
function HexText(props: HexViewerInnerProps) {
53+
const text = createMemo(() => {
54+
const decoder = new TextDecoder();
55+
const result = decoder.decode(props.bytes).replaceAll(/[\x00-\x1F]/g, replaceString);
56+
return result;
57+
});
58+
59+
return (
60+
<div data-start-hex-text>
61+
<Text options={{ size: 'xs', weight: 'bold', wrap: 'nowrap' }}>
62+
{text()}
63+
</Text>
64+
</div>
65+
);
66+
}
67+
68+
interface HexViewerInnerProps {
69+
bytes: Uint8Array;
70+
}
71+
72+
export function HexViewerInner(props: HexViewerInnerProps): JSX.Element {
73+
const rows = createMemo(() => {
74+
const arrays: Uint8Array[] = [];
75+
for (let i = 0, len = props.bytes.length; i < len; i += 16) {
76+
arrays.push(props.bytes.subarray(i, i + 16));
77+
}
78+
return arrays;
79+
});
80+
81+
return (
82+
<div data-start-hex-viewer>
83+
<div data-start-hex-viewer-bytes>
84+
<For each={rows()}>
85+
{(current) => <HexRow bytes={current} />}
86+
</For>
87+
</div>
88+
<div data-start-hex-viewer-text>
89+
<For each={rows()}>
90+
{(current) => <HexText bytes={current} />}
91+
</For>
92+
</div>
93+
</div>
94+
);
95+
}
96+
97+
export interface HexViewerProps {
98+
bytes: Uint8Array | Promise<Uint8Array>;
99+
}
100+
101+
export function HexViewer(props: HexViewerProps): JSX.Element {
102+
const [data] = createResource(() => props.bytes);
103+
104+
return (
105+
<Suspense>
106+
<Show when={data()}>
107+
{(current) => <HexViewerInner bytes={current()} />}
108+
</Show>
109+
</Suspense>
110+
);
111+
}

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

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Cascade, CascadeOption } from "../ui/Cascade.tsx";
66
import { Section } from "../ui/Section.tsx";
77
import { Text } from "../ui/Text.tsx";
88
import './SerovalViewer.css';
9+
import { HexViewer } from "./HexViewer.tsx";
910

1011
function LinkIcon(
1112
props: JSX.IntrinsicElements["svg"] & { title: string },
@@ -545,24 +546,22 @@ function renderSerovalNode(
545546
</Show >
546547
</>
547548
);
548-
// TypedArray = 15,
549-
case 15:
550-
// BigIntTypedArray = 16,
551-
case 16:
552-
return (
553-
<Cascade<number | undefined> data-start-seroval-properties defaultValue={undefined} onChange={onSelect}>
554-
{renderSerovalNode(ctx, node.f, onSelect, true)}
555-
</Cascade>
556-
);
557549
// WKSymbol = 17,
558550
case 17:
559551
return <SerovalValue value={getSymbolValue(node.s)} />
560552
// Reference = 18,
561553
case 18:
562554
break;
563555
// ArrayBuffer = 19,
564-
case 19:
565-
return <SerovalValue value={node.s} />;
556+
case 19: {
557+
const data = atob(node.s);
558+
const result = new TextEncoder().encode(data);
559+
return <HexViewer bytes={result} />;
560+
}
561+
// TypedArray = 15,
562+
case 15:
563+
// BigIntTypedArray = 16,
564+
case 16:
566565
// DataView = 20,
567566
case 20:
568567
return (

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ 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";
1112
import { Badge } from "../ui/Badge.tsx";
1213
import Button from "../ui/Button.tsx";
1314
import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx";
15+
import { Section } from "../ui/Section.tsx";
1416
import { Select, SelectOption } from "../ui/Select.tsx";
1517
import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx";
1618
import { HeadersViewer } from "./HeadersViewer.tsx";
19+
import { HexViewer } from "./HexViewer.tsx";
1720
import { SerovalViewer } from "./SerovalViewer.tsx";
1821
import {
1922
captureServerFunctionCall,
2023
type ServerFunctionRequest,
2124
type ServerFunctionResponse,
2225
} from "./server-function-tracker.ts";
2326
import "./styles.css";
24-
import { Section } from "../ui/Section.tsx";
2527

2628
interface ContentViewerProps {
2729
source: ServerFunctionRequest | ServerFunctionResponse;
@@ -35,8 +37,29 @@ function ContentViewer(props: ContentViewerProps): JSX.Element {
3537
</Section>
3638
<Section title="Body">
3739
{(() => {
38-
if (props.source.source.headers.has('x-serialized')) {
39-
return <SerovalViewer stream={props.source.source.clone()} />
40+
const startType = props.source.source.headers.get(BODY_FORMAT_KEY);
41+
const contentType = props.source.source.headers.get('Content-Type');
42+
switch (true) {
43+
case startType === "true":
44+
case startType === BodyFormat.Seroval:
45+
return <SerovalViewer stream={props.source.source.clone()} />;
46+
case startType === BodyFormat.String:
47+
return undefined;
48+
case startType === BodyFormat.File: {
49+
return undefined;
50+
}
51+
case startType === BodyFormat.FormData:
52+
case contentType?.startsWith("multipart/form-data"):
53+
return undefined;
54+
case startType === BodyFormat.URLSearchParams:
55+
case contentType?.startsWith("application/x-www-form-urlencoded"):
56+
return undefined;
57+
case startType === BodyFormat.Blob:
58+
return undefined;
59+
case startType === BodyFormat.ArrayBuffer:
60+
return undefined;
61+
case startType === BodyFormat.Uint8Array:
62+
return <HexViewer bytes={props.source.source.clone().bytes()} />;
4063
}
4164
})()}
4265
</Section>

0 commit comments

Comments
 (0)