diff options
| author | 2024-08-30 15:47:20 +0200 | |
|---|---|---|
| committer | 2024-08-30 15:47:20 +0200 | |
| commit | fe61544bafafc8b4de78cc71cb641af2dfb7b72d (patch) | |
| tree | 7edff48c1638fcc26915dc1f40982ec2563cf3bf /src | |
| parent | f183536067dc694f37445148c15821f1621f5034 (diff) | |
| parent | 2f2048160fac06e94703bb03eea43185fc01f76c (diff) | |
| download | client-2.1.0.tar.gz client-2.1.0.tar.bz2 client-2.1.0.zip | |
Merge pull request #15 from tomvanderlee/feature/websocketsv2.1.0
Feature/websockets
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/App/App.tsx | 40 | ||||
| -rw-r--r-- | src/components/Frames/Frames.module.scss | 9 | ||||
| -rw-r--r-- | src/components/Frames/Frames.tsx | 75 | ||||
| -rw-r--r-- | src/components/Icons/Sliders.tsx | 2 | ||||
| -rw-r--r-- | src/components/Icons/Trash.tsx | 2 | ||||
| -rw-r--r-- | src/components/RequestDetails/RequestDetails.tsx | 82 | ||||
| -rw-r--r-- | src/components/RequestList/RequestList.tsx | 48 | ||||
| -rw-r--r-- | src/components/RequestSummary/RequestSummary.tsx | 2 | ||||
| -rw-r--r-- | src/contexts/Connection.tsx | 288 | ||||
| -rw-r--r-- | src/contexts/DarkMode.tsx | 7 | ||||
| -rw-r--r-- | src/hooks/useRequests.tsx | 253 | ||||
| -rw-r--r-- | src/index.tsx | 5 | ||||
| -rw-r--r-- | src/types.ts | 136 |
13 files changed, 774 insertions, 175 deletions
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index b1a4501..ad36add 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import { ReactElement, useContext, useEffect, useMemo, useState } from "react"; | 2 | import { ReactElement, useContext, useEffect, useMemo, useState } from "react"; |
| 3 | import useRequests, { ReadyState, RequestResponse } from "~/hooks/useRequests"; | 3 | import useRequests from "~/hooks/useRequests"; |
| 4 | 4 | ||
| 5 | import styles from "~/components/App/App.module.scss"; | 5 | import styles from "~/components/App/App.module.scss"; |
| 6 | import RequestDetails from "~/components/RequestDetails/RequestDetails"; | 6 | import RequestDetails from "~/components/RequestDetails/RequestDetails"; |
| @@ -13,6 +13,8 @@ import Moon from "~/components/Icons/Moon"; | |||
| 13 | import Trash from "~/components/Icons/Trash"; | 13 | import Trash from "~/components/Icons/Trash"; |
| 14 | import { DarkModeContext } from "~/contexts/DarkMode"; | 14 | import { DarkModeContext } from "~/contexts/DarkMode"; |
| 15 | import RequestList from "~/components/RequestList/RequestList"; | 15 | import RequestList from "~/components/RequestList/RequestList"; |
| 16 | import { Call, ReadyState } from "~/types"; | ||
| 17 | import { ConnectionContext } from "~/contexts/Connection"; | ||
| 16 | 18 | ||
| 17 | interface Config { | 19 | interface Config { |
| 18 | url: string; | 20 | url: string; |
| @@ -47,29 +49,14 @@ const statusTextMap: ReadyStateMap = { | |||
| 47 | 49 | ||
| 48 | export default function App() { | 50 | export default function App() { |
| 49 | const { darkMode, toggle } = useContext(DarkModeContext); | 51 | const { darkMode, toggle } = useContext(DarkModeContext); |
| 50 | const [config, setConfig] = useState<Config | null>(null); | 52 | const { config, selectedCall, setSelectedCall, readyState, clear } = |
| 51 | 53 | useContext(ConnectionContext); | |
| 52 | const { calls, readyState, clear } = useRequests({ | ||
| 53 | onConnect: async () => { | ||
| 54 | const response = await fetch(`http://${getHost()}/config/`); | ||
| 55 | const config = await response.json(); | ||
| 56 | setConfig(config); | ||
| 57 | }, | ||
| 58 | }); | ||
| 59 | 54 | ||
| 60 | useEffect(() => { | 55 | useEffect(() => { |
| 61 | const url = new URL(config?.url ?? "https://loading..."); | 56 | const url = new URL(config?.url ?? "https://loading..."); |
| 62 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; | 57 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; |
| 63 | }, [readyState, config?.url]); | 58 | }, [readyState, config?.url]); |
| 64 | 59 | ||
| 65 | const [selectedRequestIndex, setSelectedRequestIndex] = useState< | ||
| 66 | number | null | ||
| 67 | >(null); | ||
| 68 | const selectedRequest = useMemo<RequestResponse | null>( | ||
| 69 | () => (selectedRequestIndex === null ? null : calls[selectedRequestIndex]), | ||
| 70 | [selectedRequestIndex, calls] | ||
| 71 | ); | ||
| 72 | |||
| 73 | const settingsMenu: (SettingsMenu | null)[] = [ | 60 | const settingsMenu: (SettingsMenu | null)[] = [ |
| 74 | { | 61 | { |
| 75 | onClick: toggle, | 62 | onClick: toggle, |
| @@ -79,7 +66,7 @@ export default function App() { | |||
| 79 | null, | 66 | null, |
| 80 | { | 67 | { |
| 81 | onClick: () => { | 68 | onClick: () => { |
| 82 | setSelectedRequestIndex(null); | 69 | setSelectedCall(null); |
| 83 | clear(); | 70 | clear(); |
| 84 | }, | 71 | }, |
| 85 | icon: <Trash />, | 72 | icon: <Trash />, |
| @@ -105,14 +92,15 @@ export default function App() { | |||
| 105 | </a> | 92 | </a> |
| 106 | </Navbar.Text> | 93 | </Navbar.Text> |
| 107 | <Navbar.Toggle aria-controls="settings" /> | 94 | <Navbar.Toggle aria-controls="settings" /> |
| 108 | <Navbar.Collapse id="settings" className="ms-2"> | 95 | <Navbar.Collapse id="settings" className="ms-2" role="button"> |
| 109 | <Nav> | 96 | <Nav> |
| 110 | <NavDropdown align="end" title={<Sliders />}> | 97 | <NavDropdown align="end" title={<Sliders />}> |
| 111 | {settingsMenu.map((item) => { | 98 | {settingsMenu.map((item, index) => { |
| 112 | if (item !== null) { | 99 | if (item !== null) { |
| 113 | const { onClick, icon, label } = item; | 100 | const { onClick, icon, label } = item; |
| 114 | return ( | 101 | return ( |
| 115 | <NavDropdown.Item | 102 | <NavDropdown.Item |
| 103 | key={label} | ||
| 116 | onClick={onClick} | 104 | onClick={onClick} |
| 117 | className="d-flex align-items-center" | 105 | className="d-flex align-items-center" |
| 118 | > | 106 | > |
| @@ -121,7 +109,7 @@ export default function App() { | |||
| 121 | </NavDropdown.Item> | 109 | </NavDropdown.Item> |
| 122 | ); | 110 | ); |
| 123 | } else { | 111 | } else { |
| 124 | return <NavDropdown.Divider />; | 112 | return <NavDropdown.Divider key={`item-${index}`} />; |
| 125 | } | 113 | } |
| 126 | })} | 114 | })} |
| 127 | </NavDropdown> | 115 | </NavDropdown> |
| @@ -133,14 +121,10 @@ export default function App() { | |||
| 133 | 121 | ||
| 134 | <main className={styles.main}> | 122 | <main className={styles.main}> |
| 135 | <div className={classNames("border-end", styles.sidebar)}> | 123 | <div className={classNames("border-end", styles.sidebar)}> |
| 136 | <RequestList | 124 | <RequestList /> |
| 137 | requests={calls} | ||
| 138 | selectedRequestIndex={selectedRequestIndex} | ||
| 139 | setSelectedRequestIndex={setSelectedRequestIndex} | ||
| 140 | /> | ||
| 141 | </div> | 125 | </div> |
| 142 | <div className={styles.details}> | 126 | <div className={styles.details}> |
| 143 | <RequestDetails requestResponse={selectedRequest} /> | 127 | <RequestDetails /> |
| 144 | </div> | 128 | </div> |
| 145 | </main> | 129 | </main> |
| 146 | </div> | 130 | </div> |
diff --git a/src/components/Frames/Frames.module.scss b/src/components/Frames/Frames.module.scss new file mode 100644 index 0000000..821f0b8 --- /dev/null +++ b/src/components/Frames/Frames.module.scss | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | .arrow { | ||
| 2 | &.outbound { | ||
| 3 | color: green; | ||
| 4 | } | ||
| 5 | |||
| 6 | &.inbound { | ||
| 7 | color: red; | ||
| 8 | } | ||
| 9 | } | ||
diff --git a/src/components/Frames/Frames.tsx b/src/components/Frames/Frames.tsx new file mode 100644 index 0000000..83c4c96 --- /dev/null +++ b/src/components/Frames/Frames.tsx | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | import { Frame } from "~/types"; | ||
| 2 | import React, { PropsWithChildren, useContext } from "react"; | ||
| 3 | import { Col, ListGroup, Row } from "react-bootstrap"; | ||
| 4 | import classNames from "classnames"; | ||
| 5 | import styles from "./Frames.module.scss"; | ||
| 6 | import dayjs from "dayjs"; | ||
| 7 | import ReactJson from "react-json-view"; | ||
| 8 | import { DarkModeContext } from "~/contexts/DarkMode"; | ||
| 9 | |||
| 10 | function isJson(data: any): boolean { | ||
| 11 | try { | ||
| 12 | JSON.parse(data); | ||
| 13 | return true; | ||
| 14 | } catch { | ||
| 15 | return false; | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | interface FramesProps { | ||
| 20 | frames: Frame[]; | ||
| 21 | } | ||
| 22 | |||
| 23 | export default function Frames({ | ||
| 24 | frames, | ||
| 25 | }: PropsWithChildren<FramesProps>): JSX.Element { | ||
| 26 | const { darkMode } = useContext(DarkModeContext); | ||
| 27 | return ( | ||
| 28 | <ListGroup variant="flush" as="ul" className={"flex-grow-1"}> | ||
| 29 | {frames.length > 0 ? ( | ||
| 30 | frames.map((frame) => { | ||
| 31 | // Inbound relative to the client | ||
| 32 | const inbound = frame.type !== "websocket_inbound"; | ||
| 33 | |||
| 34 | const body = | ||
| 35 | frame.type !== "websocket_disconnect" | ||
| 36 | ? atob(frame.payload.body) | ||
| 37 | : null; | ||
| 38 | |||
| 39 | return ( | ||
| 40 | <ListGroup.Item | ||
| 41 | as="li" | ||
| 42 | key={frame.payload.id} | ||
| 43 | className={classNames({ | ||
| 44 | "border-bottom": true, | ||
| 45 | })} | ||
| 46 | > | ||
| 47 | <Row> | ||
| 48 | <Col className="flex-grow-0 d-flex align-items-center text-nowrap text-muted"> | ||
| 49 | {dayjs(frame.payload?.timestamp).format("HH:mm:ss.SSS")} | ||
| 50 | </Col> | ||
| 51 | <Col | ||
| 52 | className={classNames( | ||
| 53 | styles.arrow, | ||
| 54 | "flex-grow-0 d-flex align-items-center flex-nowrap", | ||
| 55 | { | ||
| 56 | [styles.inbound]: inbound, | ||
| 57 | [styles.outbound]: !inbound, | ||
| 58 | } | ||
| 59 | )} | ||
| 60 | > | ||
| 61 | {inbound ? "▼" : "▲"} | ||
| 62 | </Col> | ||
| 63 | <Col className="flex-grow-1">{body}</Col> | ||
| 64 | </Row> | ||
| 65 | </ListGroup.Item> | ||
| 66 | ); | ||
| 67 | }) | ||
| 68 | ) : ( | ||
| 69 | <div className={styles.noRequest}> | ||
| 70 | <p>No messages</p> | ||
| 71 | </div> | ||
| 72 | )} | ||
| 73 | </ListGroup> | ||
| 74 | ); | ||
| 75 | } | ||
diff --git a/src/components/Icons/Sliders.tsx b/src/components/Icons/Sliders.tsx index 66d6fac..3c9b38b 100644 --- a/src/components/Icons/Sliders.tsx +++ b/src/components/Icons/Sliders.tsx | |||
| @@ -11,7 +11,7 @@ export default function Sliders() { | |||
| 11 | viewBox="0 0 16 16" | 11 | viewBox="0 0 16 16" |
| 12 | > | 12 | > |
| 13 | <path | 13 | <path |
| 14 | fill-rule="evenodd" | 14 | fillRule="evenodd" |
| 15 | d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z" | 15 | d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z" |
| 16 | /> | 16 | /> |
| 17 | </svg> | 17 | </svg> |
diff --git a/src/components/Icons/Trash.tsx b/src/components/Icons/Trash.tsx index 6c0039b..cf03546 100644 --- a/src/components/Icons/Trash.tsx +++ b/src/components/Icons/Trash.tsx | |||
| @@ -12,7 +12,7 @@ export default function Trash() { | |||
| 12 | > | 12 | > |
| 13 | <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" /> | 13 | <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" /> |
| 14 | <path | 14 | <path |
| 15 | fill-rule="evenodd" | 15 | fillRule="evenodd" |
| 16 | d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" | 16 | d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" |
| 17 | /> | 17 | /> |
| 18 | </svg> | 18 | </svg> |
diff --git a/src/components/RequestDetails/RequestDetails.tsx b/src/components/RequestDetails/RequestDetails.tsx index f34ef6f..22b3fe2 100644 --- a/src/components/RequestDetails/RequestDetails.tsx +++ b/src/components/RequestDetails/RequestDetails.tsx | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import { useCallback, useMemo, useState } from "react"; | 2 | import { useCallback, useContext, useMemo, useState } from "react"; |
| 3 | import { Headers, RequestResponse } from "~/hooks/useRequests"; | ||
| 4 | import styles from "~/components/RequestDetails/RequestDetails.module.scss"; | 3 | import styles from "~/components/RequestDetails/RequestDetails.module.scss"; |
| 5 | import RequestSummary from "~/components/RequestSummary/RequestSummary"; | 4 | import RequestSummary from "~/components/RequestSummary/RequestSummary"; |
| 6 | import Content from "~/components/Content/Content"; | 5 | import Content from "~/components/Content/Content"; |
| 7 | import { getHost } from "~/utils"; | 6 | import { getHost } from "~/utils"; |
| 8 | import { Button, Card, Col, Container, Nav, Row, Table } from "react-bootstrap"; | 7 | import { Button, Card, Col, Container, Nav, Row, Table } from "react-bootstrap"; |
| 8 | import { Call, Headers, selectedCall } from "~/types"; | ||
| 9 | import { ConnectionContext } from "~/contexts/Connection"; | ||
| 10 | import Frames from "~/components/Frames/Frames"; | ||
| 9 | 11 | ||
| 10 | interface TimingProps { | 12 | interface TimingProps { |
| 11 | timing: number; | 13 | timing: number; |
| @@ -49,37 +51,35 @@ function HeaderTable({ title, headers }: HeaderTableProps) { | |||
| 49 | ); | 51 | ); |
| 50 | } | 52 | } |
| 51 | 53 | ||
| 52 | interface DetailsProps { | 54 | type Tab = "headers" | "request" | "response" | "messages"; |
| 53 | requestResponse: RequestResponse | null; | ||
| 54 | } | ||
| 55 | 55 | ||
| 56 | type Tab = "headers" | "request" | "response"; | 56 | export default function RequestDetails() { |
| 57 | const { selectedCall } = useContext(ConnectionContext); | ||
| 57 | 58 | ||
| 58 | export default function RequestDetails({ requestResponse }: DetailsProps) { | ||
| 59 | const [tab, selectTab] = useState<Tab>("headers"); | 59 | const [tab, selectTab] = useState<Tab>("headers"); |
| 60 | const [raw, setRaw] = useState(false); | 60 | const [raw, setRaw] = useState(false); |
| 61 | 61 | ||
| 62 | const resend = useCallback( | 62 | const resend = useCallback( |
| 63 | async () => | 63 | async () => |
| 64 | requestResponse !== null && | 64 | selectedCall !== null && |
| 65 | fetch(`http://${getHost()}/resend/`, { | 65 | fetch(`http://${getHost()}/resend/`, { |
| 66 | method: "POST", | 66 | method: "POST", |
| 67 | body: JSON.stringify({ | 67 | body: JSON.stringify({ |
| 68 | ...requestResponse.request, | 68 | ...selectedCall.request, |
| 69 | id: undefined, | 69 | id: undefined, |
| 70 | }), | 70 | }), |
| 71 | }), | 71 | }), |
| 72 | [requestResponse] | 72 | [selectedCall] |
| 73 | ); | 73 | ); |
| 74 | 74 | ||
| 75 | return requestResponse !== null ? ( | 75 | return selectedCall !== null ? ( |
| 76 | <div className={styles.details}> | 76 | <div className={styles.details}> |
| 77 | <div className={styles.header}> | 77 | <div className={styles.header}> |
| 78 | <Row> | 78 | <Row> |
| 79 | <Col> | 79 | <Col> |
| 80 | <Container fluid style={{ fontSize: "1.5em" }} className="py-3"> | 80 | <Container fluid style={{ fontSize: "1.5em" }} className="py-3"> |
| 81 | <RequestSummary | 81 | <RequestSummary |
| 82 | requestResponse={requestResponse} | 82 | requestResponse={selectedCall} |
| 83 | className={styles.summary} | 83 | className={styles.summary} |
| 84 | /> | 84 | /> |
| 85 | </Container> | 85 | </Container> |
| @@ -98,18 +98,29 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 98 | <Nav.Item> | 98 | <Nav.Item> |
| 99 | <Nav.Link eventKey="request">Request</Nav.Link> | 99 | <Nav.Link eventKey="request">Request</Nav.Link> |
| 100 | </Nav.Item> | 100 | </Nav.Item> |
| 101 | <Nav.Item> | 101 | {selectedCall.type === "RequestResponse" && ( |
| 102 | <Nav.Link | 102 | <> |
| 103 | eventKey="response" | 103 | <Nav.Item> |
| 104 | disabled={requestResponse.response === undefined} | 104 | <Nav.Link |
| 105 | > | 105 | eventKey="response" |
| 106 | Response | 106 | disabled={selectedCall.response === undefined} |
| 107 | </Nav.Link> | 107 | > |
| 108 | </Nav.Item> | 108 | Response |
| 109 | </Nav.Link> | ||
| 110 | </Nav.Item> | ||
| 111 | </> | ||
| 112 | )} | ||
| 113 | {selectedCall.type === "WebsocketConnection" && ( | ||
| 114 | <> | ||
| 115 | <Nav.Item> | ||
| 116 | <Nav.Link eventKey="response">Messages</Nav.Link> | ||
| 117 | </Nav.Item> | ||
| 118 | </> | ||
| 119 | )} | ||
| 109 | </Nav> | 120 | </Nav> |
| 110 | </Col> | 121 | </Col> |
| 111 | <Col className="border-bottom px-3 " xs="auto"> | 122 | <Col className="border-bottom px-3 " xs="auto"> |
| 112 | <Timing timing={requestResponse.response?.timing ?? NaN} /> | 123 | <Timing timing={selectedCall.response?.timing ?? NaN} /> |
| 113 | <Button | 124 | <Button |
| 114 | variant="outline-primary" | 125 | variant="outline-primary" |
| 115 | onClick={() => resend()} | 126 | onClick={() => resend()} |
| @@ -125,21 +136,30 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 125 | <> | 136 | <> |
| 126 | <HeaderTable | 137 | <HeaderTable |
| 127 | title="request headers" | 138 | title="request headers" |
| 128 | headers={requestResponse.request.headers} | 139 | headers={selectedCall.request.headers} |
| 129 | /> | 140 | /> |
| 130 | {requestResponse.response && ( | 141 | |
| 131 | <HeaderTable | 142 | {selectedCall.type === "RequestResponse" && |
| 132 | title="response headers" | 143 | selectedCall.response !== undefined && ( |
| 133 | headers={requestResponse.response.headers} | 144 | <HeaderTable |
| 134 | /> | 145 | title="response headers" |
| 135 | )} | 146 | headers={selectedCall.response.headers} |
| 147 | /> | ||
| 148 | )} | ||
| 136 | </> | 149 | </> |
| 137 | )} | 150 | )} |
| 138 | {tab === "request" && ( | 151 | {tab === "request" && ( |
| 139 | <Content data={requestResponse.request} raw={raw} setRaw={setRaw} /> | 152 | <Content data={selectedCall.request} raw={raw} setRaw={setRaw} /> |
| 140 | )} | 153 | )} |
| 141 | {tab === "response" && requestResponse.response !== undefined && ( | 154 | {selectedCall.type === "RequestResponse" && |
| 142 | <Content data={requestResponse.response} raw={raw} setRaw={setRaw} /> | 155 | tab === "response" && |
| 156 | selectedCall.response !== undefined && ( | ||
| 157 | <Content data={selectedCall.response} raw={raw} setRaw={setRaw} /> | ||
| 158 | )} | ||
| 159 | {selectedCall.type === "WebsocketConnection" && tab === "response" && ( | ||
| 160 | <Frames frames={selectedCall.frames} /> | ||
| 161 | // <Content data={selectedCall.frames} raw={false} setRaw={setRaw} /> | ||
| 162 | // <pre>{JSON.stringify(selectedCall.frames, undefined, 2)}</pre> | ||
| 143 | )} | 163 | )} |
| 144 | </div> | 164 | </div> |
| 145 | </div> | 165 | </div> |
diff --git a/src/components/RequestList/RequestList.tsx b/src/components/RequestList/RequestList.tsx index acef47a..82a6b21 100644 --- a/src/components/RequestList/RequestList.tsx +++ b/src/components/RequestList/RequestList.tsx | |||
| @@ -11,27 +11,24 @@ import classNames from "classnames"; | |||
| 11 | import styles from "~/components/RequestList/RequestList.module.scss"; | 11 | import styles from "~/components/RequestList/RequestList.module.scss"; |
| 12 | import RequestSummary from "~/components/RequestSummary/RequestSummary"; | 12 | import RequestSummary from "~/components/RequestSummary/RequestSummary"; |
| 13 | import * as React from "react"; | 13 | import * as React from "react"; |
| 14 | import { useCallback, useContext, useMemo, useState } from "react"; | 14 | import { useCallback, useContext, useEffect, useMemo, useState } from "react"; |
| 15 | import { Method, RequestResponse } from "~/hooks/useRequests"; | ||
| 16 | import { DarkModeContext } from "~/contexts/DarkMode"; | 15 | import { DarkModeContext } from "~/contexts/DarkMode"; |
| 17 | import Filter from "~/components/Icons/Filter"; | 16 | import Filter from "~/components/Icons/Filter"; |
| 18 | 17 | import { Call, Method } from "~/types"; | |
| 19 | interface ListProps { | 18 | import { ConnectionContext } from "~/contexts/Connection"; |
| 20 | requests: RequestResponse[]; | ||
| 21 | selectedRequestIndex: number | null; | ||
| 22 | setSelectedRequestIndex: (index: number) => void; | ||
| 23 | } | ||
| 24 | 19 | ||
| 25 | type EnabledMethods = { | 20 | type EnabledMethods = { |
| 26 | [method in Method]: boolean; | 21 | [method in Method]: boolean; |
| 27 | }; | 22 | }; |
| 28 | 23 | ||
| 29 | export default function RequestList({ | 24 | export default function RequestList() { |
| 30 | requests, | ||
| 31 | selectedRequestIndex, | ||
| 32 | setSelectedRequestIndex, | ||
| 33 | }: ListProps) { | ||
| 34 | const { darkMode } = useContext(DarkModeContext); | 25 | const { darkMode } = useContext(DarkModeContext); |
| 26 | const { | ||
| 27 | calls: requests, | ||
| 28 | selectedCall, | ||
| 29 | setSelectedCall, | ||
| 30 | } = useContext(ConnectionContext); | ||
| 31 | |||
| 35 | const [showFilterOptions, setShowFilterOptions] = useState(false); | 32 | const [showFilterOptions, setShowFilterOptions] = useState(false); |
| 36 | const [search, setSearch] = useState(""); | 33 | const [search, setSearch] = useState(""); |
| 37 | const [enableRegex, setEnableRegex] = useState(false); | 34 | const [enableRegex, setEnableRegex] = useState(false); |
| @@ -73,7 +70,7 @@ export default function RequestList({ | |||
| 73 | () => | 70 | () => |
| 74 | Object.entries(methods) | 71 | Object.entries(methods) |
| 75 | .filter(([method, enabled]) => enabled) | 72 | .filter(([method, enabled]) => enabled) |
| 76 | .map(([method]) => method), | 73 | .map(([method]) => method) as Method[], |
| 77 | [methods] | 74 | [methods] |
| 78 | ); | 75 | ); |
| 79 | 76 | ||
| @@ -97,16 +94,14 @@ export default function RequestList({ | |||
| 97 | } catch {} | 94 | } catch {} |
| 98 | 95 | ||
| 99 | return requests | 96 | return requests |
| 100 | .map<[number, RequestResponse]>((request, index) => [index, request]) | ||
| 101 | .reverse() | 97 | .reverse() |
| 102 | .filter( | 98 | .filter( |
| 103 | ([index, request]) => | 99 | (request) => |
| 104 | enabledMethods.length > 0 === null || | 100 | enabledMethods.length === 0 || |
| 105 | enabledMethods.includes(request.request.method) | 101 | enabledMethods.includes(request.request.method) |
| 106 | ) | 102 | ) |
| 107 | .filter( | 103 | .filter( |
| 108 | ([index, request]) => | 104 | (request) => search === "" || searchRegex.test(request.request.path) |
| 109 | search === "" || searchRegex.test(request.request.path) | ||
| 110 | ); | 105 | ); |
| 111 | }, [requests, search, enabledMethods, enableRegex]); | 106 | }, [requests, search, enabledMethods, enableRegex]); |
| 112 | 107 | ||
| @@ -124,7 +119,10 @@ export default function RequestList({ | |||
| 124 | </Dropdown.Item> | 119 | </Dropdown.Item> |
| 125 | <Dropdown.Divider /> | 120 | <Dropdown.Divider /> |
| 126 | {Object.entries(methods).map(([method, enabled]) => ( | 121 | {Object.entries(methods).map(([method, enabled]) => ( |
| 127 | <Dropdown.Item onClick={() => toggleMethods(method as Method)}> | 122 | <Dropdown.Item |
| 123 | onClick={() => toggleMethods(method as Method)} | ||
| 124 | key={method} | ||
| 125 | > | ||
| 128 | {method} | 126 | {method} |
| 129 | </Dropdown.Item> | 127 | </Dropdown.Item> |
| 130 | ))} | 128 | ))} |
| @@ -151,13 +149,14 @@ export default function RequestList({ | |||
| 151 | className={classNames(styles.list, "flex-grow-1")} | 149 | className={classNames(styles.list, "flex-grow-1")} |
| 152 | > | 150 | > |
| 153 | {filteredRequests.length > 0 ? ( | 151 | {filteredRequests.length > 0 ? ( |
| 154 | filteredRequests.map(([index, requestResponse]) => { | 152 | filteredRequests.map((requestResponse) => { |
| 155 | const selected = selectedRequestIndex === index; | 153 | const selected = |
| 154 | selectedCall?.request.id === requestResponse.request.id; | ||
| 156 | return ( | 155 | return ( |
| 157 | <ListGroup.Item | 156 | <ListGroup.Item |
| 158 | as="li" | 157 | as="li" |
| 159 | onClick={() => setSelectedRequestIndex(index)} | 158 | onClick={() => setSelectedCall?.(requestResponse)} |
| 160 | key={`request-${index}`} | 159 | key={requestResponse.request.id} |
| 161 | className={classNames({ | 160 | className={classNames({ |
| 162 | "bg-primary": selected, | 161 | "bg-primary": selected, |
| 163 | "text-light": selected, | 162 | "text-light": selected, |
| @@ -209,6 +208,7 @@ export default function RequestList({ | |||
| 209 | <Form.Label className="fw-bold">Method</Form.Label> | 208 | <Form.Label className="fw-bold">Method</Form.Label> |
| 210 | {Object.entries(methods).map(([method, enabled]) => ( | 209 | {Object.entries(methods).map(([method, enabled]) => ( |
| 211 | <Form.Check | 210 | <Form.Check |
| 211 | key={method} | ||
| 212 | type="switch" | 212 | type="switch" |
| 213 | label={method} | 213 | label={method} |
| 214 | checked={enabled} | 214 | checked={enabled} |
diff --git a/src/components/RequestSummary/RequestSummary.tsx b/src/components/RequestSummary/RequestSummary.tsx index 49e6086..4e97676 100644 --- a/src/components/RequestSummary/RequestSummary.tsx +++ b/src/components/RequestSummary/RequestSummary.tsx | |||
| @@ -7,7 +7,7 @@ import dayjs from "dayjs"; | |||
| 7 | 7 | ||
| 8 | interface RequestSummaryProps { | 8 | interface RequestSummaryProps { |
| 9 | selected?: boolean; | 9 | selected?: boolean; |
| 10 | requestResponse: RequestResponse; | 10 | requestResponse: Calls; |
| 11 | showTime?: boolean; | 11 | showTime?: boolean; |
| 12 | } | 12 | } |
| 13 | 13 | ||
diff --git a/src/contexts/Connection.tsx b/src/contexts/Connection.tsx new file mode 100644 index 0000000..42482e1 --- /dev/null +++ b/src/contexts/Connection.tsx | |||
| @@ -0,0 +1,288 @@ | |||
| 1 | import * as React from "react"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | Context, | ||
| 5 | createContext, | ||
| 6 | PropsWithChildren, | ||
| 7 | useCallback, | ||
| 8 | useEffect, | ||
| 9 | useMemo, | ||
| 10 | useState, | ||
| 11 | } from "react"; | ||
| 12 | import { | ||
| 13 | Call, | ||
| 14 | Frame, | ||
| 15 | Frames, | ||
| 16 | Historic, | ||
| 17 | ReadyState, | ||
| 18 | Request, | ||
| 19 | RequestResponseType, | ||
| 20 | Requests, | ||
| 21 | Response, | ||
| 22 | ResponsePayload, | ||
| 23 | Responses, | ||
| 24 | WebsocketConnect, | ||
| 25 | WebsocketConnected, | ||
| 26 | WebsocketConnectedPayload, | ||
| 27 | WebsocketDisconnectPayload, | ||
| 28 | WebsocketInboundPayload, | ||
| 29 | WebsocketOutboundPayload, | ||
| 30 | WebsocketType, | ||
| 31 | } from "~/types"; | ||
| 32 | import { getHost } from "~/utils"; | ||
| 33 | |||
| 34 | interface Config { | ||
| 35 | url: string; | ||
| 36 | } | ||
| 37 | |||
| 38 | interface ConnectionApi { | ||
| 39 | config: Config | null; | ||
| 40 | calls: Call[]; | ||
| 41 | selectedCall: Call | null; | ||
| 42 | setSelectedCall: (call: Call | null) => void; | ||
| 43 | readyState: ReadyState; | ||
| 44 | clear: () => void; | ||
| 45 | } | ||
| 46 | |||
| 47 | export const ConnectionContext = createContext<Partial<ConnectionApi>>( | ||
| 48 | {} | ||
| 49 | ) as Context<ConnectionApi>; | ||
| 50 | |||
| 51 | export default function ConnectionProvider({ | ||
| 52 | children, | ||
| 53 | }: PropsWithChildren<any>): JSX.Element { | ||
| 54 | const [selectedCall, setSelectedCall] = useState<Call | null>(null); | ||
| 55 | const [config, setConfig] = useState<Config | null>(null); | ||
| 56 | |||
| 57 | const wsHost = useMemo(getHost, []); | ||
| 58 | |||
| 59 | const [initialConnection, setInitialConnection] = useState(true); | ||
| 60 | const [requests, setRequests] = useState<Requests>([]); | ||
| 61 | const [responses, setResponses] = useState<Responses>({}); | ||
| 62 | const [websocketFrames, setWebsocketFrames] = useState<Frames>({}); | ||
| 63 | |||
| 64 | const connect = useCallback( | ||
| 65 | () => new WebSocket(`ws://${wsHost}/inspect/`), | ||
| 66 | [wsHost] | ||
| 67 | ); | ||
| 68 | |||
| 69 | const [ws, setWs] = useState<WebSocket>(() => connect()); | ||
| 70 | const [readyState, setReadyState] = useState<ReadyState>(ws.readyState); | ||
| 71 | |||
| 72 | useEffect(() => { | ||
| 73 | setReadyState(ws.readyState); | ||
| 74 | |||
| 75 | const onClose = () => { | ||
| 76 | setReadyState(ws.readyState); | ||
| 77 | setWs(connect()); | ||
| 78 | }; | ||
| 79 | |||
| 80 | const onOpen = async () => { | ||
| 81 | const response = await fetch(`http://${getHost()}/config/`); | ||
| 82 | const config = await response.json(); | ||
| 83 | setConfig(config); | ||
| 84 | |||
| 85 | setInitialConnection(false); | ||
| 86 | setReadyState(ws.readyState); | ||
| 87 | }; | ||
| 88 | |||
| 89 | const onMessage = ({ data }: { data: string }) => { | ||
| 90 | const { type, payload } = JSON.parse(data) as | ||
| 91 | | Historic | ||
| 92 | | RequestResponseType | ||
| 93 | | WebsocketType; | ||
| 94 | |||
| 95 | switch (type) { | ||
| 96 | case "historic": | ||
| 97 | if (initialConnection) { | ||
| 98 | const requests = ( | ||
| 99 | payload as (RequestResponseType | WebsocketType)[] | ||
| 100 | ) | ||
| 101 | .filter( | ||
| 102 | ({ type }) => type === "request" || type == "websocket_connect" | ||
| 103 | ) | ||
| 104 | .map((payload) => { | ||
| 105 | if ( | ||
| 106 | (payload as WebsocketConnect).payload.method === undefined | ||
| 107 | ) { | ||
| 108 | (payload as Request | WebsocketConnect).payload.method = | ||
| 109 | "GET"; | ||
| 110 | } | ||
| 111 | |||
| 112 | return payload; | ||
| 113 | }); | ||
| 114 | const responses = ( | ||
| 115 | payload as (RequestResponseType | WebsocketType)[] | ||
| 116 | ) | ||
| 117 | .filter( | ||
| 118 | ({ type }) => | ||
| 119 | type === "response" || type == "websocket_connected" | ||
| 120 | ) | ||
| 121 | .map(({ type, ...item }) => { | ||
| 122 | if (type == "websocket_connected") { | ||
| 123 | (item.payload as WebsocketConnectedPayload).status = 101; | ||
| 124 | } | ||
| 125 | |||
| 126 | return { type, ...item }; | ||
| 127 | }) | ||
| 128 | .reduce<{ [id: string]: Response | WebsocketConnected }>( | ||
| 129 | (out, item) => ({ | ||
| 130 | ...out, | ||
| 131 | [item.payload.id]: item as Response | WebsocketConnected, | ||
| 132 | }), | ||
| 133 | {} | ||
| 134 | ); | ||
| 135 | const frames = (payload as (RequestResponseType | WebsocketType)[]) | ||
| 136 | .filter( | ||
| 137 | ({ type }) => | ||
| 138 | type == "websocket_inbound" || | ||
| 139 | type == "websocket_outbound" || | ||
| 140 | type == "websocket_disconnect" | ||
| 141 | ) | ||
| 142 | .reduce<Frames>((out, item) => { | ||
| 143 | if (!out.hasOwnProperty(item.payload.id)) { | ||
| 144 | out[item.payload.id] = []; | ||
| 145 | } | ||
| 146 | |||
| 147 | out[item.payload.id].push(item as Frame); | ||
| 148 | return out; | ||
| 149 | }, {}); | ||
| 150 | setRequests((rqs) => [ | ||
| 151 | ...rqs, | ||
| 152 | ...requests.map( | ||
| 153 | (payload) => payload as Request | WebsocketConnect | ||
| 154 | ), | ||
| 155 | ]); | ||
| 156 | setResponses((rps) => ({ | ||
| 157 | ...rps, | ||
| 158 | ...responses, | ||
| 159 | })); | ||
| 160 | setWebsocketFrames((frms) => ({ | ||
| 161 | ...frms, | ||
| 162 | ...frames, | ||
| 163 | })); | ||
| 164 | } | ||
| 165 | break; | ||
| 166 | case "request": | ||
| 167 | case "websocket_connect": | ||
| 168 | setRequests((rqs) => [ | ||
| 169 | ...rqs, | ||
| 170 | { | ||
| 171 | type, | ||
| 172 | payload: | ||
| 173 | type === "request" | ||
| 174 | ? payload | ||
| 175 | : { | ||
| 176 | ...payload, | ||
| 177 | method: "GET", | ||
| 178 | }, | ||
| 179 | } as Request | WebsocketConnect, | ||
| 180 | ]); | ||
| 181 | break; | ||
| 182 | case "response": | ||
| 183 | case "websocket_connected": | ||
| 184 | if (type == "websocket_connected") { | ||
| 185 | (payload as WebsocketConnectedPayload).status = 101; | ||
| 186 | } | ||
| 187 | setResponses((rps) => ({ | ||
| 188 | ...rps, | ||
| 189 | [(payload as ResponsePayload | WebsocketConnectedPayload).id]: { | ||
| 190 | type, | ||
| 191 | payload, | ||
| 192 | } as Response | WebsocketConnected, | ||
| 193 | })); | ||
| 194 | break; | ||
| 195 | case "websocket_inbound": | ||
| 196 | case "websocket_outbound": | ||
| 197 | case "websocket_disconnect": | ||
| 198 | setWebsocketFrames((frms) => { | ||
| 199 | const id = ( | ||
| 200 | payload as | ||
| 201 | | WebsocketInboundPayload | ||
| 202 | | WebsocketOutboundPayload | ||
| 203 | | WebsocketDisconnectPayload | ||
| 204 | ).id; | ||
| 205 | |||
| 206 | const newFrms = { ...frms }; | ||
| 207 | |||
| 208 | if (!newFrms.hasOwnProperty(id)) { | ||
| 209 | newFrms[id] = []; | ||
| 210 | } | ||
| 211 | |||
| 212 | // @ts-ignore | ||
| 213 | newFrms[id].push({ | ||
| 214 | type, | ||
| 215 | payload: payload as | ||
| 216 | | WebsocketInboundPayload | ||
| 217 | | WebsocketOutboundPayload | ||
| 218 | | WebsocketDisconnectPayload, | ||
| 219 | }); | ||
| 220 | |||
| 221 | return newFrms; | ||
| 222 | }); | ||
| 223 | break; | ||
| 224 | } | ||
| 225 | }; | ||
| 226 | |||
| 227 | ws.addEventListener("message", onMessage); | ||
| 228 | ws.addEventListener("close", onClose); | ||
| 229 | ws.addEventListener("open", onOpen); | ||
| 230 | |||
| 231 | return () => { | ||
| 232 | ws.removeEventListener("message", onMessage); | ||
| 233 | ws.removeEventListener("close", onClose); | ||
| 234 | ws.removeEventListener("open", onOpen); | ||
| 235 | }; | ||
| 236 | }, [ws]); | ||
| 237 | |||
| 238 | const calls = useMemo<Call[]>( | ||
| 239 | () => | ||
| 240 | requests.map((request) => { | ||
| 241 | let call: Call; | ||
| 242 | if (request.type === "request") { | ||
| 243 | call = { | ||
| 244 | type: "RequestResponse", | ||
| 245 | request: request.payload, | ||
| 246 | response: responses[request.payload.id]?.payload as ResponsePayload, | ||
| 247 | }; | ||
| 248 | } else { | ||
| 249 | call = { | ||
| 250 | type: "WebsocketConnection", | ||
| 251 | request: request.payload, | ||
| 252 | response: responses[request.payload.id] | ||
| 253 | ?.payload as WebsocketConnectedPayload, | ||
| 254 | frames: websocketFrames[request.payload.id] ?? [], | ||
| 255 | }; | ||
| 256 | } | ||
| 257 | |||
| 258 | if ( | ||
| 259 | selectedCall !== null && | ||
| 260 | selectedCall.request.id === call.request.id | ||
| 261 | ) { | ||
| 262 | setSelectedCall(call); | ||
| 263 | } | ||
| 264 | |||
| 265 | return call; | ||
| 266 | }), | ||
| 267 | [requests, responses, websocketFrames] | ||
| 268 | ); | ||
| 269 | |||
| 270 | return ( | ||
| 271 | <ConnectionContext.Provider | ||
| 272 | value={{ | ||
| 273 | config, | ||
| 274 | calls, | ||
| 275 | selectedCall, | ||
| 276 | setSelectedCall, | ||
| 277 | readyState, | ||
| 278 | clear: () => { | ||
| 279 | setRequests([]); | ||
| 280 | setResponses({}); | ||
| 281 | setWebsocketFrames({}); | ||
| 282 | }, | ||
| 283 | }} | ||
| 284 | > | ||
| 285 | {children} | ||
| 286 | </ConnectionContext.Provider> | ||
| 287 | ); | ||
| 288 | } | ||
diff --git a/src/contexts/DarkMode.tsx b/src/contexts/DarkMode.tsx index 4326fe9..fd536b2 100644 --- a/src/contexts/DarkMode.tsx +++ b/src/contexts/DarkMode.tsx | |||
| @@ -2,7 +2,7 @@ import * as React from "react"; | |||
| 2 | import { | 2 | import { |
| 3 | Context, | 3 | Context, |
| 4 | createContext, | 4 | createContext, |
| 5 | ReactNode, | 5 | PropsWithChildren, |
| 6 | useEffect, | 6 | useEffect, |
| 7 | useMemo, | 7 | useMemo, |
| 8 | useState, | 8 | useState, |
| @@ -17,11 +17,8 @@ interface DarkModeApi { | |||
| 17 | export const DarkModeContext = createContext<Partial<DarkModeApi>>( | 17 | export const DarkModeContext = createContext<Partial<DarkModeApi>>( |
| 18 | {} | 18 | {} |
| 19 | ) as Context<DarkModeApi>; | 19 | ) as Context<DarkModeApi>; |
| 20 | interface DarkModeProviderProps { | ||
| 21 | children: ReactNode; | ||
| 22 | } | ||
| 23 | 20 | ||
| 24 | export default function DarkModeProvider({ children }: DarkModeProviderProps) { | 21 | export default function DarkModeProvider({ children }: PropsWithChildren<any>) { |
| 25 | const themeConfig = useMemo(() => new ThemeConfig(), []); | 22 | const themeConfig = useMemo(() => new ThemeConfig(), []); |
| 26 | const [darkMode, setDarkMode] = useState( | 23 | const [darkMode, setDarkMode] = useState( |
| 27 | () => themeConfig.getTheme() === "dark" | 24 | () => themeConfig.getTheme() === "dark" |
diff --git a/src/hooks/useRequests.tsx b/src/hooks/useRequests.tsx index 1361949..3135aa2 100644 --- a/src/hooks/useRequests.tsx +++ b/src/hooks/useRequests.tsx | |||
| @@ -1,68 +1,32 @@ | |||
| 1 | import { useCallback, useEffect, useMemo, useState } from "react"; | 1 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 2 | import { getHost } from "~/utils"; | 2 | import { getHost } from "~/utils"; |
| 3 | 3 | import { | |
| 4 | export type Headers = [string, string][]; | 4 | Call, |
| 5 | export type Method = | 5 | Frame, |
| 6 | | "GET" | 6 | Frames, |
| 7 | | "HEAD" | 7 | Historic, |
| 8 | | "POST" | 8 | ReadyState, |
| 9 | | "PUT" | 9 | Request, |
| 10 | | "DELETE" | 10 | RequestResponseType, |
| 11 | | "CONNECT" | 11 | Requests, |
| 12 | | "OPTIONS" | 12 | Response, |
| 13 | | "TRACE" | 13 | ResponsePayload, |
| 14 | | "PATCH"; | 14 | Responses, |
| 15 | 15 | WebsocketConnect, | |
| 16 | export interface RequestPayload { | 16 | WebsocketConnected, |
| 17 | id: string; | 17 | WebsocketConnectedPayload, |
| 18 | timestamp: string; | 18 | WebsocketDisconnectPayload, |
| 19 | body: string; | 19 | WebsocketInboundPayload, |
| 20 | headers: Headers; | 20 | WebsocketOutboundPayload, |
| 21 | method: Method; | 21 | WebsocketType, |
| 22 | path: string; | 22 | } from "~/types"; |
| 23 | } | ||
| 24 | |||
| 25 | interface Request { | ||
| 26 | type: "request"; | ||
| 27 | payload: RequestPayload; | ||
| 28 | } | ||
| 29 | |||
| 30 | export interface ResponsePayload { | ||
| 31 | id: string; | ||
| 32 | timing: number; | ||
| 33 | body: string; | ||
| 34 | headers: Headers; | ||
| 35 | status: number; | ||
| 36 | } | ||
| 37 | |||
| 38 | interface Response { | ||
| 39 | type: "response"; | ||
| 40 | payload: ResponsePayload; | ||
| 41 | } | ||
| 42 | |||
| 43 | interface Historic { | ||
| 44 | type: "historic"; | ||
| 45 | payload: (Request | Response)[]; | ||
| 46 | } | ||
| 47 | |||
| 48 | export interface RequestResponse { | ||
| 49 | request: RequestPayload; | ||
| 50 | response?: ResponsePayload; | ||
| 51 | } | ||
| 52 | |||
| 53 | export enum ReadyState { | ||
| 54 | CONNECTING = 0, | ||
| 55 | OPEN = 1, | ||
| 56 | CLOSING = 2, | ||
| 57 | CLOSED = 3, | ||
| 58 | } | ||
| 59 | 23 | ||
| 60 | export interface useRequestsProps { | 24 | export interface useRequestsProps { |
| 61 | onConnect: () => Promise<void>; | 25 | onConnect: () => Promise<void>; |
| 62 | } | 26 | } |
| 63 | 27 | ||
| 64 | export interface UseRequests { | 28 | export interface UseRequests { |
| 65 | calls: RequestResponse[]; | 29 | calls: Call[]; |
| 66 | readyState: ReadyState; | 30 | readyState: ReadyState; |
| 67 | clear: () => void; | 31 | clear: () => void; |
| 68 | } | 32 | } |
| @@ -73,8 +37,9 @@ export default function useRequests({ | |||
| 73 | const wsHost = useMemo(getHost, []); | 37 | const wsHost = useMemo(getHost, []); |
| 74 | 38 | ||
| 75 | const [initialConnection, setInitialConnection] = useState(true); | 39 | const [initialConnection, setInitialConnection] = useState(true); |
| 76 | const [requests, setRequests] = useState<RequestPayload[]>([]); | 40 | const [requests, setRequests] = useState<Requests>([]); |
| 77 | const [responses, setResponses] = useState<ResponsePayload[]>([]); | 41 | const [responses, setResponses] = useState<Responses>({}); |
| 42 | const [websocketFrames, setWebsocketFrames] = useState<Frames>({}); | ||
| 78 | 43 | ||
| 79 | const connect = useCallback( | 44 | const connect = useCallback( |
| 80 | () => new WebSocket(`ws://${wsHost}/inspect/`), | 45 | () => new WebSocket(`ws://${wsHost}/inspect/`), |
| @@ -96,36 +61,140 @@ export default function useRequests({ | |||
| 96 | setInitialConnection(false); | 61 | setInitialConnection(false); |
| 97 | setReadyState(ws.readyState); | 62 | setReadyState(ws.readyState); |
| 98 | }; | 63 | }; |
| 99 | const onMessage = ({ data }) => { | 64 | const onMessage = ({ data }: { data: string }) => { |
| 100 | const { type, payload } = JSON.parse(data) as | 65 | const { type, payload } = JSON.parse(data) as |
| 101 | | Historic | 66 | | Historic |
| 102 | | Request | 67 | | RequestResponseType |
| 103 | | Response; | 68 | | WebsocketType; |
| 104 | 69 | ||
| 105 | switch (type) { | 70 | switch (type) { |
| 106 | case "historic": | 71 | case "historic": |
| 107 | if (initialConnection) { | 72 | if (initialConnection) { |
| 108 | const requests = (payload as (Request | Response)[]).filter( | 73 | const requests = ( |
| 109 | ({ type }) => type === "request" | 74 | payload as (RequestResponseType | WebsocketType)[] |
| 110 | ); | 75 | ) |
| 111 | const responses = (payload as (Request | Response)[]).filter( | 76 | .filter( |
| 112 | ({ type }) => type === "response" | 77 | ({ type }) => type === "request" || type == "websocket_connect" |
| 113 | ); | 78 | ) |
| 79 | .map((payload) => { | ||
| 80 | if ( | ||
| 81 | (payload as WebsocketConnect).payload.method === undefined | ||
| 82 | ) { | ||
| 83 | (payload as Request | WebsocketConnect).payload.method = | ||
| 84 | "GET"; | ||
| 85 | } | ||
| 86 | |||
| 87 | return payload; | ||
| 88 | }); | ||
| 89 | const responses = ( | ||
| 90 | payload as (RequestResponseType | WebsocketType)[] | ||
| 91 | ) | ||
| 92 | .filter( | ||
| 93 | ({ type }) => | ||
| 94 | type === "response" || type == "websocket_connected" | ||
| 95 | ) | ||
| 96 | .map(({ type, ...item }) => { | ||
| 97 | if (type == "websocket_connected") { | ||
| 98 | (item.payload as WebsocketConnectedPayload).status = 101; | ||
| 99 | } | ||
| 100 | |||
| 101 | return { type, ...item }; | ||
| 102 | }) | ||
| 103 | .reduce<{ [id: string]: Response | WebsocketConnected }>( | ||
| 104 | (out, item) => ({ | ||
| 105 | ...out, | ||
| 106 | [item.payload.id]: item as Response | WebsocketConnected, | ||
| 107 | }), | ||
| 108 | {} | ||
| 109 | ); | ||
| 110 | const frames = (payload as (RequestResponseType | WebsocketType)[]) | ||
| 111 | .filter( | ||
| 112 | ({ type }) => | ||
| 113 | type == "websocket_inbound" || | ||
| 114 | type == "websocket_outbound" || | ||
| 115 | type == "websocket_disconnect" | ||
| 116 | ) | ||
| 117 | .reduce<Frames>((out, item) => { | ||
| 118 | if (!out.hasOwnProperty(item.payload.id)) { | ||
| 119 | out[item.payload.id] = []; | ||
| 120 | } | ||
| 121 | |||
| 122 | out[item.payload.id].push(item as Frame); | ||
| 123 | return out; | ||
| 124 | }, {}); | ||
| 114 | setRequests((rqs) => [ | 125 | setRequests((rqs) => [ |
| 115 | ...rqs, | 126 | ...rqs, |
| 116 | ...requests.map(({ payload }) => payload as RequestPayload), | 127 | ...requests.map( |
| 128 | (payload) => payload as Request | WebsocketConnect | ||
| 129 | ), | ||
| 117 | ]); | 130 | ]); |
| 118 | setResponses((rps) => [ | 131 | setResponses((rps) => ({ |
| 119 | ...rps, | 132 | ...rps, |
| 120 | ...responses.map(({ payload }) => payload as ResponsePayload), | 133 | ...responses, |
| 121 | ]); | 134 | })); |
| 135 | setWebsocketFrames((frms) => ({ | ||
| 136 | ...frms, | ||
| 137 | ...frames, | ||
| 138 | })); | ||
| 122 | } | 139 | } |
| 123 | break; | 140 | break; |
| 124 | case "request": | 141 | case "request": |
| 125 | setRequests((rqs) => [...rqs, payload as RequestPayload]); | 142 | case "websocket_connect": |
| 143 | setRequests((rqs) => [ | ||
| 144 | ...rqs, | ||
| 145 | { | ||
| 146 | type, | ||
| 147 | payload: | ||
| 148 | type === "request" | ||
| 149 | ? payload | ||
| 150 | : { | ||
| 151 | ...payload, | ||
| 152 | method: "GET", | ||
| 153 | }, | ||
| 154 | } as Request | WebsocketConnect, | ||
| 155 | ]); | ||
| 126 | break; | 156 | break; |
| 127 | case "response": | 157 | case "response": |
| 128 | setResponses((rps) => [...rps, payload as ResponsePayload]); | 158 | case "websocket_connected": |
| 159 | if (type == "websocket_connected") { | ||
| 160 | (payload as WebsocketConnectedPayload).status = 101; | ||
| 161 | } | ||
| 162 | setResponses((rps) => ({ | ||
| 163 | ...rps, | ||
| 164 | [(payload as ResponsePayload | WebsocketConnectedPayload).id]: { | ||
| 165 | type, | ||
| 166 | payload, | ||
| 167 | } as Response | WebsocketConnected, | ||
| 168 | })); | ||
| 169 | break; | ||
| 170 | case "websocket_inbound": | ||
| 171 | case "websocket_outbound": | ||
| 172 | case "websocket_disconnect": | ||
| 173 | setWebsocketFrames((frms) => { | ||
| 174 | const id = ( | ||
| 175 | payload as | ||
| 176 | | WebsocketInboundPayload | ||
| 177 | | WebsocketOutboundPayload | ||
| 178 | | WebsocketDisconnectPayload | ||
| 179 | ).id; | ||
| 180 | |||
| 181 | const newFrms = { ...frms }; | ||
| 182 | |||
| 183 | if (!newFrms.hasOwnProperty(id)) { | ||
| 184 | newFrms[id] = []; | ||
| 185 | } | ||
| 186 | |||
| 187 | // @ts-ignore | ||
| 188 | newFrms[id].push({ | ||
| 189 | type, | ||
| 190 | payload: payload as | ||
| 191 | | WebsocketInboundPayload | ||
| 192 | | WebsocketOutboundPayload | ||
| 193 | | WebsocketDisconnectPayload, | ||
| 194 | }); | ||
| 195 | |||
| 196 | return newFrms; | ||
| 197 | }); | ||
| 129 | break; | 198 | break; |
| 130 | } | 199 | } |
| 131 | }; | 200 | }; |
| @@ -141,19 +210,37 @@ export default function useRequests({ | |||
| 141 | }; | 210 | }; |
| 142 | }, [ws]); | 211 | }, [ws]); |
| 143 | 212 | ||
| 213 | const calls = useMemo<Call[]>( | ||
| 214 | () => | ||
| 215 | requests.map((request) => { | ||
| 216 | switch (request.type) { | ||
| 217 | case "request": | ||
| 218 | return { | ||
| 219 | type: "RequestResponse", | ||
| 220 | request: request.payload, | ||
| 221 | response: responses[request.payload.id] | ||
| 222 | ?.payload as ResponsePayload, | ||
| 223 | }; | ||
| 224 | case "websocket_connect": | ||
| 225 | return { | ||
| 226 | type: "WebsocketConnection", | ||
| 227 | request: request.payload, | ||
| 228 | response: responses[request.payload.id] | ||
| 229 | ?.payload as WebsocketConnectedPayload, | ||
| 230 | frames: websocketFrames[request.payload.id] ?? [], | ||
| 231 | }; | ||
| 232 | } | ||
| 233 | }), | ||
| 234 | [requests, responses, websocketFrames] | ||
| 235 | ); | ||
| 236 | |||
| 144 | return { | 237 | return { |
| 145 | calls: useMemo<RequestResponse[]>( | 238 | calls, |
| 146 | () => | ||
| 147 | requests.map((request) => ({ | ||
| 148 | request: request, | ||
| 149 | response: responses.find(({ id }) => id === request.id), | ||
| 150 | })), | ||
| 151 | [requests, responses] | ||
| 152 | ), | ||
| 153 | readyState, | 239 | readyState, |
| 154 | clear: () => { | 240 | clear: () => { |
| 155 | setRequests([]); | 241 | setRequests([]); |
| 156 | setResponses([]); | 242 | setResponses({}); |
| 243 | setWebsocketFrames({}); | ||
| 157 | }, | 244 | }, |
| 158 | }; | 245 | }; |
| 159 | } | 246 | } |
diff --git a/src/index.tsx b/src/index.tsx index d83fd38..a2c18e3 100644 --- a/src/index.tsx +++ b/src/index.tsx | |||
| @@ -7,11 +7,14 @@ import DarkModeProvider from "./contexts/DarkMode"; | |||
| 7 | 7 | ||
| 8 | import dayjs from "dayjs"; | 8 | import dayjs from "dayjs"; |
| 9 | import localizedFormat from "dayjs/plugin/localizedFormat"; | 9 | import localizedFormat from "dayjs/plugin/localizedFormat"; |
| 10 | import ConnectionProvider from "~/contexts/Connection"; | ||
| 10 | dayjs.extend(localizedFormat); | 11 | dayjs.extend(localizedFormat); |
| 11 | 12 | ||
| 12 | ReactDOM.render( | 13 | ReactDOM.render( |
| 13 | <DarkModeProvider> | 14 | <DarkModeProvider> |
| 14 | <App /> | 15 | <ConnectionProvider> |
| 16 | <App /> | ||
| 17 | </ConnectionProvider> | ||
| 15 | </DarkModeProvider>, | 18 | </DarkModeProvider>, |
| 16 | document.getElementById("root") | 19 | document.getElementById("root") |
| 17 | ); | 20 | ); |
diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0e11bee --- /dev/null +++ b/src/types.ts | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | export type Headers = [string, string][]; | ||
| 2 | export type Method = | ||
| 3 | | "GET" | ||
| 4 | | "HEAD" | ||
| 5 | | "POST" | ||
| 6 | | "PUT" | ||
| 7 | | "DELETE" | ||
| 8 | | "CONNECT" | ||
| 9 | | "OPTIONS" | ||
| 10 | | "TRACE" | ||
| 11 | | "PATCH"; | ||
| 12 | |||
| 13 | export interface RequestPayload { | ||
| 14 | id: string; | ||
| 15 | timestamp: string; | ||
| 16 | body: string; | ||
| 17 | headers: Headers; | ||
| 18 | method: Method; | ||
| 19 | path: string; | ||
| 20 | } | ||
| 21 | |||
| 22 | export interface Request { | ||
| 23 | type: "request"; | ||
| 24 | payload: RequestPayload; | ||
| 25 | } | ||
| 26 | |||
| 27 | export interface ResponsePayload { | ||
| 28 | id: string; | ||
| 29 | timing: number; | ||
| 30 | body: string; | ||
| 31 | headers: Headers; | ||
| 32 | status: number; | ||
| 33 | } | ||
| 34 | |||
| 35 | export interface Response { | ||
| 36 | type: "response"; | ||
| 37 | payload: ResponsePayload; | ||
| 38 | } | ||
| 39 | |||
| 40 | export type RequestResponseType = Request | Response; | ||
| 41 | |||
| 42 | export interface WebsocketConnectPayload { | ||
| 43 | method: "GET"; | ||
| 44 | id: string; | ||
| 45 | headers: Headers; | ||
| 46 | path: string; | ||
| 47 | timestamp: string; | ||
| 48 | } | ||
| 49 | |||
| 50 | export interface WebsocketConnect { | ||
| 51 | type: "websocket_connect"; | ||
| 52 | payload: WebsocketConnectPayload; | ||
| 53 | } | ||
| 54 | |||
| 55 | export interface WebsocketConnectedPayload { | ||
| 56 | id: string; | ||
| 57 | timing: number; | ||
| 58 | status: 101; | ||
| 59 | } | ||
| 60 | |||
| 61 | export interface WebsocketConnected { | ||
| 62 | type: "websocket_connected"; | ||
| 63 | payload: WebsocketConnectedPayload; | ||
| 64 | } | ||
| 65 | |||
| 66 | export interface WebsocketInboundPayload { | ||
| 67 | id: string; | ||
| 68 | timestamp: string; | ||
| 69 | body: string; | ||
| 70 | } | ||
| 71 | |||
| 72 | interface WebsocketInbound { | ||
| 73 | type: "websocket_inbound"; | ||
| 74 | payload: WebsocketInboundPayload; | ||
| 75 | } | ||
| 76 | |||
| 77 | export interface WebsocketOutboundPayload { | ||
| 78 | id: string; | ||
| 79 | timestamp: string; | ||
| 80 | body: string; | ||
| 81 | } | ||
| 82 | |||
| 83 | interface WebsocketOutbound { | ||
| 84 | type: "websocket_outbound"; | ||
| 85 | payload: WebsocketOutboundPayload; | ||
| 86 | } | ||
| 87 | |||
| 88 | export interface WebsocketDisconnectPayload { | ||
| 89 | id: string; | ||
| 90 | timestamp: string; | ||
| 91 | close_code: number; | ||
| 92 | } | ||
| 93 | |||
| 94 | interface WebsocketDisconnect { | ||
| 95 | type: "websocket_disconnect"; | ||
| 96 | payload: WebsocketDisconnectPayload; | ||
| 97 | } | ||
| 98 | |||
| 99 | export type WebsocketType = | ||
| 100 | | WebsocketConnect | ||
| 101 | | WebsocketConnected | ||
| 102 | | WebsocketInbound | ||
| 103 | | WebsocketOutbound | ||
| 104 | | WebsocketDisconnect; | ||
| 105 | |||
| 106 | export interface Historic { | ||
| 107 | type: "historic"; | ||
| 108 | payload: (RequestResponseType | WebsocketType)[]; | ||
| 109 | } | ||
| 110 | |||
| 111 | export interface RequestResponse { | ||
| 112 | type: "RequestResponse"; | ||
| 113 | request: RequestPayload; | ||
| 114 | response?: ResponsePayload; | ||
| 115 | } | ||
| 116 | |||
| 117 | export type Frame = WebsocketDisconnect | WebsocketInbound | WebsocketOutbound; | ||
| 118 | export interface WebsocketConnection { | ||
| 119 | type: "WebsocketConnection"; | ||
| 120 | request: WebsocketConnectPayload; | ||
| 121 | response?: WebsocketConnectedPayload; | ||
| 122 | frames?: Frame[]; | ||
| 123 | } | ||
| 124 | |||
| 125 | export type Call = RequestResponse | WebsocketConnection; | ||
| 126 | |||
| 127 | export type Requests = Array<Request | WebsocketConnect>; | ||
| 128 | export type Responses = { [id: string]: Response | WebsocketConnected }; | ||
| 129 | export type Frames = { [id: string]: Frame[] }; | ||
| 130 | |||
| 131 | export enum ReadyState { | ||
| 132 | CONNECTING = 0, | ||
| 133 | OPEN = 1, | ||
| 134 | CLOSING = 2, | ||
| 135 | CLOSED = 3, | ||
| 136 | } | ||
