diff options
| author | 2024-08-30 11:19:30 +0200 | |
|---|---|---|
| committer | 2024-08-30 15:33:15 +0200 | |
| commit | 648b804e72d4831e41e02dfd7d6b5a9ac7660b58 (patch) | |
| tree | 34c0e284df0510c868606a01833aa0e57f6678c2 /src | |
| parent | b19c1877d088fbe01bcdea9fbdef282e66ab114f (diff) | |
| download | client-648b804e72d4831e41e02dfd7d6b5a9ac7660b58.tar.gz client-648b804e72d4831e41e02dfd7d6b5a9ac7660b58.tar.bz2 client-648b804e72d4831e41e02dfd7d6b5a9ac7660b58.zip | |
Added ui
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/App/App.tsx | 39 | ||||
| -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 | 195 | ||||
| -rw-r--r-- | src/index.tsx | 5 | ||||
| -rw-r--r-- | src/types.ts | 33 |
13 files changed, 654 insertions, 133 deletions
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 5e17388..ad36add 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx | |||
| @@ -13,7 +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 { ReadyState, RequestResponse } from "~/types"; | 16 | import { Call, ReadyState } from "~/types"; |
| 17 | import { ConnectionContext } from "~/contexts/Connection"; | ||
| 17 | 18 | ||
| 18 | interface Config { | 19 | interface Config { |
| 19 | url: string; | 20 | url: string; |
| @@ -48,29 +49,14 @@ const statusTextMap: ReadyStateMap = { | |||
| 48 | 49 | ||
| 49 | export default function App() { | 50 | export default function App() { |
| 50 | const { darkMode, toggle } = useContext(DarkModeContext); | 51 | const { darkMode, toggle } = useContext(DarkModeContext); |
| 51 | const [config, setConfig] = useState<Config | null>(null); | 52 | const { config, selectedCall, setSelectedCall, readyState, clear } = |
| 52 | 53 | useContext(ConnectionContext); | |
| 53 | const { calls, readyState, clear } = useRequests({ | ||
| 54 | onConnect: async () => { | ||
| 55 | const response = await fetch(`http://${getHost()}/config/`); | ||
| 56 | const config = await response.json(); | ||
| 57 | setConfig(config); | ||
| 58 | }, | ||
| 59 | }); | ||
| 60 | 54 | ||
| 61 | useEffect(() => { | 55 | useEffect(() => { |
| 62 | const url = new URL(config?.url ?? "https://loading..."); | 56 | const url = new URL(config?.url ?? "https://loading..."); |
| 63 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; | 57 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; |
| 64 | }, [readyState, config?.url]); | 58 | }, [readyState, config?.url]); |
| 65 | 59 | ||
| 66 | const [selectedRequestIndex, setSelectedRequestIndex] = useState< | ||
| 67 | number | null | ||
| 68 | >(null); | ||
| 69 | const selectedRequest = useMemo<RequestResponse | null>( | ||
| 70 | () => (selectedRequestIndex === null ? null : calls[selectedRequestIndex]), | ||
| 71 | [selectedRequestIndex, calls] | ||
| 72 | ); | ||
| 73 | |||
| 74 | const settingsMenu: (SettingsMenu | null)[] = [ | 60 | const settingsMenu: (SettingsMenu | null)[] = [ |
| 75 | { | 61 | { |
| 76 | onClick: toggle, | 62 | onClick: toggle, |
| @@ -80,7 +66,7 @@ export default function App() { | |||
| 80 | null, | 66 | null, |
| 81 | { | 67 | { |
| 82 | onClick: () => { | 68 | onClick: () => { |
| 83 | setSelectedRequestIndex(null); | 69 | setSelectedCall(null); |
| 84 | clear(); | 70 | clear(); |
| 85 | }, | 71 | }, |
| 86 | icon: <Trash />, | 72 | icon: <Trash />, |
| @@ -106,14 +92,15 @@ export default function App() { | |||
| 106 | </a> | 92 | </a> |
| 107 | </Navbar.Text> | 93 | </Navbar.Text> |
| 108 | <Navbar.Toggle aria-controls="settings" /> | 94 | <Navbar.Toggle aria-controls="settings" /> |
| 109 | <Navbar.Collapse id="settings" className="ms-2"> | 95 | <Navbar.Collapse id="settings" className="ms-2" role="button"> |
| 110 | <Nav> | 96 | <Nav> |
| 111 | <NavDropdown align="end" title={<Sliders />}> | 97 | <NavDropdown align="end" title={<Sliders />}> |
| 112 | {settingsMenu.map((item) => { | 98 | {settingsMenu.map((item, index) => { |
| 113 | if (item !== null) { | 99 | if (item !== null) { |
| 114 | const { onClick, icon, label } = item; | 100 | const { onClick, icon, label } = item; |
| 115 | return ( | 101 | return ( |
| 116 | <NavDropdown.Item | 102 | <NavDropdown.Item |
| 103 | key={label} | ||
| 117 | onClick={onClick} | 104 | onClick={onClick} |
| 118 | className="d-flex align-items-center" | 105 | className="d-flex align-items-center" |
| 119 | > | 106 | > |
| @@ -122,7 +109,7 @@ export default function App() { | |||
| 122 | </NavDropdown.Item> | 109 | </NavDropdown.Item> |
| 123 | ); | 110 | ); |
| 124 | } else { | 111 | } else { |
| 125 | return <NavDropdown.Divider />; | 112 | return <NavDropdown.Divider key={`item-${index}`} />; |
| 126 | } | 113 | } |
| 127 | })} | 114 | })} |
| 128 | </NavDropdown> | 115 | </NavDropdown> |
| @@ -134,14 +121,10 @@ export default function App() { | |||
| 134 | 121 | ||
| 135 | <main className={styles.main}> | 122 | <main className={styles.main}> |
| 136 | <div className={classNames("border-end", styles.sidebar)}> | 123 | <div className={classNames("border-end", styles.sidebar)}> |
| 137 | <RequestList | 124 | <RequestList /> |
| 138 | requests={calls} | ||
| 139 | selectedRequestIndex={selectedRequestIndex} | ||
| 140 | setSelectedRequestIndex={setSelectedRequestIndex} | ||
| 141 | /> | ||
| 142 | </div> | 125 | </div> |
| 143 | <div className={styles.details}> | 126 | <div className={styles.details}> |
| 144 | <RequestDetails requestResponse={selectedRequest} /> | 127 | <RequestDetails /> |
| 145 | </div> | 128 | </div> |
| 146 | </main> | 129 | </main> |
| 147 | </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 | > |
