Skip to content

Commit 4339a62

Browse files
committed
feat: change debugger ui
1 parent 0ad9a77 commit 4339a62

File tree

16 files changed

+387
-265
lines changed

16 files changed

+387
-265
lines changed

.changeset/social-zebras-kneel.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@msw-dev-tool/react": minor
3+
---
4+
5+
- Moved the debugger ui to a Dialog.
6+
- Made it possible to interact with the debugger when clicking on a debug column.
7+
- Add debug icon

packages/react/src/store/uiControlStore.ts

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

packages/react/src/style/msw-dev-tool.css

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,9 @@
2929
/* ===== Misc ===== */
3030
@source inline("bg-transparent underline");
3131

32-
.msw-dt-http-control-row {
33-
@apply cursor-pointer transition-colors duration-200 ease-in-out;
34-
}
35-
36-
.msw-dt-http-control-row:hover {
37-
@apply bg-gray-100;
38-
}
39-
40-
.msw-dt-current-row {
41-
@apply bg-gray-200;
42-
}
4332

4433
.msw-dt-sub-text {
45-
@apply text-sm text-gray-900;
34+
@apply text-sm text-gray-800;
4635
}
4736

4837
.msw-dt-text-ellipsis {
@@ -113,14 +102,12 @@
113102
@apply whitespace-nowrap overflow-hidden text-ellipsis;
114103
}
115104

116-
.msw-dt-dialog-overlay {
117-
@apply top-0 right-0 bottom-0 left-0 bg-black/40;
118-
}
119105

120-
.msw-dt-dialog-content {
121-
@apply bg-white flex flex-col text-gray-900;
122-
}
123106

124-
.msw-dt-dialog-layout {
107+
.dialog-content {
108+
@apply bg-white flex flex-col text-gray-900;
125109
@apply fixed z-[9999];
110+
@apply top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%];
111+
@apply max-h-[85vh] max-w-[70vw];
112+
@apply overflow-hidden rounded-lg px-[1rem] py-[2rem] box-border;
126113
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from "react";
2+
3+
export const DebugIcon = () => {
4+
return (
5+
<svg
6+
width="1rem"
7+
height="1rem"
8+
viewBox="0 0 24 24"
9+
xmlns="http://www.w3.org/2000/svg"
10+
fill="#000000"
11+
>
12+
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
13+
<g
14+
id="SVGRepo_tracerCarrier"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
></g>
18+
<g id="SVGRepo_iconCarrier">
19+
<path d="M4.6 15c-.9-2.6-.6-4.6-.5-5.4 2.4-1.5 5.3-2 8-1.3.7-.3 1.5-.5 2.3-.6-.1-.3-.2-.5-.3-.8h2l1.2-3.2-.9-.4-1 2.6h-1.8C13 4.8 12.1 4 11.1 3.4l2.1-2.1-.7-.7L10.1 3c-.7 0-1.5 0-2.3.1L5.4.7l-.7.7 2.1 2.1C5.7 4.1 4.9 4.9 4.3 6H2.5l-1-2.6-.9.4L1.8 7h2C3.3 8.3 3 9.6 3 11H1v1h2c0 1 .2 2 .5 3H1.8L.6 18.3l.9.3 1-2.7h1.4c.4.8 2.1 4.5 5.8 3.9-.3-.2-.5-.5-.7-.8-2.9 0-4.4-3.5-4.4-4zM9 3.9c2 0 3.7 1.6 4.4 3.8-2.9-1-6.2-.8-9 .6.7-2.6 2.5-4.4 4.6-4.4zm14.8 19.2l-4.3-4.3c2.1-2.5 1.8-6.3-.7-8.4s-6.3-1.8-8.4.7-1.8 6.3.7 8.4c2.2 1.9 5.4 1.9 7.7 0l4.3 4.3c.2.2.5.2.7 0 .2-.2.2-.5 0-.7zm-8.8-3c-2.8 0-5.1-2.3-5.1-5.1s2.3-5.1 5.1-5.1 5.1 2.3 5.1 5.1-2.3 5.1-5.1 5.1z"></path>
20+
<path fill="none" d="M0 0h24v24H0z"></path>
21+
</g>
22+
</svg>
23+
);
24+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { matchRequestUrl, Path, PathParams } from "msw";
2+
import React, {
3+
createContext,
4+
PropsWithChildren,
5+
useContext,
6+
useState,
7+
} from "react";
8+
9+
export interface DebugResponse {
10+
errorMessage?: string;
11+
data?: string;
12+
statusCode?: number;
13+
statusText?: string;
14+
}
15+
16+
export interface DebugState {
17+
loading: boolean;
18+
requestHeader: Record<string, string>;
19+
searchParam: Record<string, string>;
20+
pathParam: PathParams<string> | undefined;
21+
response: DebugResponse;
22+
url: URL;
23+
}
24+
25+
export interface DebugContextValue extends DebugState {
26+
setDebug: <K extends keyof DebugState>(
27+
key: K,
28+
value: DebugState[K] | ((prev: DebugState[K]) => DebugState[K])
29+
) => void;
30+
reset: () => void;
31+
}
32+
33+
const getInitialState = (
34+
params: PathParams<string> | undefined
35+
): DebugState => ({
36+
url: new URL(window.location.href),
37+
loading: false,
38+
requestHeader: {},
39+
searchParam: {},
40+
pathParam: params
41+
? Object.keys(params).reduce(
42+
(acc, key) => ({
43+
...acc,
44+
[key]: "",
45+
}),
46+
{}
47+
)
48+
: undefined,
49+
response: {},
50+
});
51+
52+
const DebugContext = createContext<DebugContextValue | null>(null);
53+
54+
export const DebugProvider = ({
55+
children,
56+
url,
57+
path,
58+
}: PropsWithChildren<{ url: URL; path: Path }>) => {
59+
const { params } = matchRequestUrl(url, path, url.origin);
60+
const [state, setState] = useState<DebugState>(getInitialState(params));
61+
62+
const setDebug = <K extends keyof DebugState>(
63+
key: K,
64+
value: DebugState[K] | ((prev: DebugState[K]) => DebugState[K])
65+
) => {
66+
setState((prev) => ({
67+
...prev,
68+
[key]: typeof value === "function" ? value(prev[key]) : value,
69+
}));
70+
};
71+
72+
const reset = () => {
73+
setState(getInitialState(params));
74+
};
75+
76+
return (
77+
<DebugContext.Provider
78+
value={{
79+
...state,
80+
url,
81+
setDebug,
82+
reset,
83+
}}
84+
>
85+
{children}
86+
</DebugContext.Provider>
87+
);
88+
};
89+
90+
export const useDebugContext = () => {
91+
const context = useContext(DebugContext);
92+
if (!context) {
93+
throw new Error("useDebug must be used within a DebugProvider");
94+
}
95+
return context;
96+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from "react";
2+
import { Button } from "../../Components/Button";
3+
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
4+
import { useDebugContext } from "./DebugProvider";
5+
import {
6+
getPathWithParams,
7+
getSearchParams,
8+
getTotalUrl,
9+
} from "../../../utils/url";
10+
11+
export interface FetchButtonProps {
12+
url: string;
13+
}
14+
15+
const getResponseString = async (res: Response): Promise<string> => {
16+
const contentType = res.headers.get("content-type")?.toLowerCase() || "";
17+
18+
if (contentType.includes("application/json")) {
19+
const data = await res.json();
20+
return JSON.stringify(data, null, 2);
21+
}
22+
23+
if (contentType.includes("multipart/form-data")) {
24+
const formData = await res.formData();
25+
const formDataObj: Record<string, string> = {};
26+
formData.forEach((value, key) => {
27+
formDataObj[key] = value.toString();
28+
});
29+
return JSON.stringify(formDataObj, null, 2);
30+
}
31+
32+
return await res.text();
33+
};
34+
35+
export const FetchButton = () => {
36+
const { loading, requestHeader, setDebug, url, pathParam, searchParam } =
37+
useDebugContext();
38+
39+
const finalPath = getPathWithParams(url, pathParam);
40+
const searchParamsString = getSearchParams(searchParam);
41+
const totalUrl = getTotalUrl(url.origin, finalPath, searchParamsString);
42+
43+
const handleFetch = () => {
44+
setDebug("loading", true);
45+
setDebug("response", {});
46+
47+
fetch(totalUrl, {
48+
headers: {
49+
...requestHeader,
50+
},
51+
})
52+
.then(async (res) => {
53+
const data = await getResponseString(res);
54+
setDebug("response", {
55+
data,
56+
statusCode: res.status,
57+
statusText: res.statusText,
58+
});
59+
})
60+
.catch((err) => {
61+
setDebug("response", {
62+
errorMessage: err instanceof Error ? err.message : "Failed to fetch",
63+
});
64+
})
65+
.finally(() => {
66+
setDebug("loading", false);
67+
});
68+
};
69+
70+
return (
71+
<Button
72+
onClick={handleFetch}
73+
color="primary"
74+
className="flex items-center gap-2"
75+
>
76+
{loading ? <ReloadIcon className="animate-spin" /> : <PlayIcon />}
77+
Send Request
78+
</Button>
79+
);
80+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from "react";
2+
import { useDebugContext } from "./DebugProvider";
3+
import { KeyValueInputList } from "./KeyValueInputList";
4+
5+
export const HeaderSetter = () => {
6+
const { requestHeader, setDebug } = useDebugContext();
7+
8+
return (
9+
<KeyValueInputList
10+
items={requestHeader}
11+
setItems={(items) => {
12+
setDebug("requestHeader", items);
13+
}}
14+
title="Request Headers"
15+
/>
16+
);
17+
};

packages/react/src/ui/DevToolContent/HandlerDebugger/PathParamSetter.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
import { Label } from "@radix-ui/react-label";
2-
import { PathParams } from "msw";
32
import React from "react";
43
import { Flex } from "../../Components/Flex";
54
import { Input } from "../../Components/Input";
5+
import { useDebugContext } from "./DebugProvider";
6+
7+
export const PathParamSetter = () => {
8+
const { pathParam, setDebug } = useDebugContext();
69

7-
export const PathParamSetter = ({
8-
paramValues,
9-
onParamChange,
10-
}: {
11-
paramValues?: PathParams<string>;
12-
onParamChange: (key: string, value: string) => void;
13-
}) => {
1410
return (
15-
paramValues &&
16-
Object.keys(paramValues).length > 0 && (
11+
pathParam &&
12+
Object.keys(pathParam).length > 0 && (
1713
<div>
1814
<Label>Path Parameters</Label>
1915
<Flex direction="column" gap={2} py={2}>
20-
{Object.entries(paramValues).map(([key, value]) => (
16+
{Object.entries(pathParam).map(([key, value]) => (
2117
<Flex align="center" gap={2} key={key}>
22-
<Label htmlFor={`param-${key}`} style={{ width:"160px" }}>
18+
<Label htmlFor={`param-${key}`} className="w-[160px]">
2319
{key}:
2420
</Label>
2521
<Input
2622
id={`param-${key}`}
2723
type="text"
2824
value={value as string}
29-
onChange={(e) => onParamChange(key, e.target.value)}
25+
onChange={(e) => {
26+
setDebug("pathParam", {
27+
...pathParam,
28+
[key]: e.target.value,
29+
});
30+
}}
3031
placeholder="value of path param"
3132
className="w-[180px]"
3233
/>

0 commit comments

Comments
 (0)