diff options
| -rw-r--r-- | .github/workflows/python-publish.yml | 6 | ||||
| -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 | ||||
| -rw-r--r-- | ttun/__main__.py | 11 | ||||
| -rw-r--r-- | ttun/client.py | 216 | ||||
| -rw-r--r-- | ttun/inspect_server.py | 5 | ||||
| -rw-r--r-- | ttun/pubsub.py | 1 | ||||
| -rw-r--r-- | ttun/types.py | 60 | ||||
| -rw-r--r-- | yarn.lock | 1092 |
20 files changed, 1554 insertions, 786 deletions
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 9246d7e..d90abfb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml | |||
| @@ -12,13 +12,13 @@ jobs: | |||
| 12 | deploy: | 12 | deploy: |
| 13 | runs-on: ubuntu-latest | 13 | runs-on: ubuntu-latest |
| 14 | steps: | 14 | steps: |
| 15 | - uses: actions/checkout@v2 | 15 | - uses: actions/checkout@v4 |
| 16 | - name: Set up Python | 16 | - name: Set up Python |
| 17 | uses: actions/setup-python@v2 | 17 | uses: actions/setup-python@v5 |
| 18 | with: | 18 | with: |
| 19 | python-version: '3.10' | 19 | python-version: '3.10' |
| 20 | - name: Set up Node | 20 | - name: Set up Node |
| 21 | uses: actions/setup-node@v2 | 21 | uses: actions/setup-node@v4 |
| 22 | with: | 22 | with: |
| 23 | node-version: '16' | 23 | node-version: '16' |
| 24 | - name: Install node dependencies | 24 | - name: Install node dependencies |
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 | > | ||
