diff options
28 files changed, 750 insertions, 606 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c5c9a1e --- /dev/null +++ b/.pre-commit-config.yaml | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | repos: | ||
| 2 | - repo: https://github.com/pre-commit/pre-commit-hooks | ||
| 3 | rev: v2.3.0 | ||
| 4 | hooks: | ||
| 5 | - id: check-yaml | ||
| 6 | - id: end-of-file-fixer | ||
| 7 | - id: trailing-whitespace | ||
| 8 | - repo: https://github.com/psf/black | ||
| 9 | rev: 21.12b0 | ||
| 10 | hooks: | ||
| 11 | - id: black | ||
| 12 | types_or: | ||
| 13 | - python | ||
| 14 | - repo: https://github.com/asottile/reorder_python_imports | ||
| 15 | rev: v2.7.1 | ||
| 16 | hooks: | ||
| 17 | - id: reorder-python-imports | ||
| 18 | types_or: | ||
| 19 | - python | ||
| 20 | - repo: https://github.com/pre-commit/mirrors-prettier | ||
| 21 | rev: 'v2.5.1' | ||
| 22 | hooks: | ||
| 23 | - id: prettier | ||
| 24 | types_or: | ||
| 25 | - ts | ||
| 26 | - tsx | ||
| 27 | - css | ||
| 28 | - scss | ||
diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..46e2c6d --- /dev/null +++ b/.prettierignore | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | ttun/ | ||
| 2 | ttun.egg-info/ | ||
| 3 | .github/ | ||
| 4 | dist/ | ||
| 5 | node_modules/ | ||
diff --git a/build_requirements.txt b/dev_requirements.txt index 03befb7..d5ce409 100644 --- a/build_requirements.txt +++ b/dev_requirements.txt | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | setuptools | 1 | setuptools |
| 2 | build | 2 | build |
| 3 | setuptools-git-versioning | 3 | setuptools-git-versioning |
| 4 | pre-commit | ||
diff --git a/requirements.txt b/requirements.txt index 04b1cf3..97a6769 100644 --- a/requirements.txt +++ b/requirements.txt | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | -r build_requirements.txt | 1 | -r dev_requirements.txt |
| 2 | websockets ~= 10.0 | 2 | websockets ~= 10.0 |
| 3 | aiohttp ~= 3.8 | 3 | aiohttp ~= 3.8 |
| 4 | appdirs ~= 1.4 | 4 | appdirs ~= 1.4 |
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index e08c84f..1d231ae 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx | |||
| @@ -1,81 +1,88 @@ | |||
| 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, { | 3 | import useRequests, { |
| 4 | ReadyState, | 4 | ReadyState, |
| 5 | RequestResponse | 5 | RequestResponse, |
| 6 | } from "../../hooks/useRequests"; | 6 | } from "../../hooks/useRequests"; |
| 7 | 7 | ||
| 8 | import styles from './App.module.scss'; | 8 | import styles from "./App.module.scss"; |
| 9 | import RequestDetails from "../RequestDetails/RequestDetails"; | 9 | import RequestDetails from "../RequestDetails/RequestDetails"; |
| 10 | import {getHost} from "../../utils"; | 10 | import { getHost } from "../../utils"; |
| 11 | import {Container, ListGroup, Nav, Navbar, NavDropdown} from "react-bootstrap"; | 11 | import { |
| 12 | Container, | ||
| 13 | ListGroup, | ||
| 14 | Nav, | ||
| 15 | Navbar, | ||
| 16 | NavDropdown, | ||
| 17 | } from "react-bootstrap"; | ||
| 12 | import classNames from "classnames"; | 18 | import classNames from "classnames"; |
| 13 | import {Sliders} from "../Icons/Sliders"; | 19 | import { Sliders } from "../Icons/Sliders"; |
| 14 | import {Sun} from "../Icons/Sun"; | 20 | import { Sun } from "../Icons/Sun"; |
| 15 | import {Moon} from "../Icons/Moon"; | 21 | import { Moon } from "../Icons/Moon"; |
| 16 | import Trash from "../Icons/Trash"; | 22 | import Trash from "../Icons/Trash"; |
| 17 | import {DarkModeContext} from "../../contexts/DarkMode"; | 23 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 18 | import RequestList from "../RequestList/RequestList"; | 24 | import RequestList from "../RequestList/RequestList"; |
| 19 | 25 | ||
| 20 | interface Config { | 26 | interface Config { |
| 21 | url: string | 27 | url: string; |
| 22 | } | 28 | } |
| 23 | 29 | ||
| 24 | interface SettingsMenu { | 30 | interface SettingsMenu { |
| 25 | icon: ReactElement, | 31 | icon: ReactElement; |
| 26 | label: string, | 32 | label: string; |
| 27 | onClick: () => void, | 33 | onClick: () => void; |
| 28 | } | 34 | } |
| 29 | 35 | ||
| 30 | type ReadyStateMap = { | 36 | type ReadyStateMap = { |
| 31 | [ReadyState.CONNECTING]: string, | 37 | [ReadyState.CONNECTING]: string; |
| 32 | [ReadyState.OPEN]: string, | 38 | [ReadyState.OPEN]: string; |
| 33 | [ReadyState.CLOSING]: string, | 39 | [ReadyState.CLOSING]: string; |
| 34 | [ReadyState.CLOSED]: string, | 40 | [ReadyState.CLOSED]: string; |
| 35 | } | 41 | }; |
| 36 | 42 | ||
| 37 | const statusIconMap: ReadyStateMap = { | 43 | const statusIconMap: ReadyStateMap = { |
| 38 | [ReadyState.CONNECTING]: '🔴', | 44 | [ReadyState.CONNECTING]: "🔴", |
| 39 | [ReadyState.OPEN]: '🟢', | 45 | [ReadyState.OPEN]: "🟢", |
| 40 | [ReadyState.CLOSING]: '🔴', | 46 | [ReadyState.CLOSING]: "🔴", |
| 41 | [ReadyState.CLOSED]: '🔴', | 47 | [ReadyState.CLOSED]: "🔴", |
| 42 | } | 48 | }; |
| 43 | 49 | ||
| 44 | const statusTextMap: ReadyStateMap = { | 50 | const statusTextMap: ReadyStateMap = { |
| 45 | [ReadyState.CONNECTING]: 'Connecting...', | 51 | [ReadyState.CONNECTING]: "Connecting...", |
| 46 | [ReadyState.OPEN]: 'Connected', | 52 | [ReadyState.OPEN]: "Connected", |
| 47 | [ReadyState.CLOSING]: 'Closing...', | 53 | [ReadyState.CLOSING]: "Closing...", |
| 48 | [ReadyState.CLOSED]: 'Closed', | 54 | [ReadyState.CLOSED]: "Closed", |
| 49 | } | 55 | }; |
| 50 | 56 | ||
| 51 | export default function App() { | 57 | export default function App() { |
| 52 | const { darkMode, toggle } = useContext(DarkModeContext); | 58 | const { darkMode, toggle } = useContext(DarkModeContext); |
| 53 | const [config, setConfig]= useState<Config | null>(null) | 59 | const [config, setConfig] = useState<Config | null>(null); |
| 54 | 60 | ||
| 55 | const { calls, readyState, clear } = useRequests({ | 61 | const { calls, readyState, clear } = useRequests({ |
| 56 | onConnect: async () => { | 62 | onConnect: async () => { |
| 57 | const response = await fetch(`http://${getHost()}/config/`) | 63 | const response = await fetch(`http://${getHost()}/config/`); |
| 58 | const config = await response.json() | 64 | const config = await response.json(); |
| 59 | setConfig(config) | 65 | setConfig(config); |
| 60 | } | 66 | }, |
| 61 | }); | 67 | }); |
| 62 | 68 | ||
| 63 | useEffect(() => { | 69 | useEffect(() => { |
| 64 | const url = new URL(config?.url ?? 'https://loading...'); | 70 | const url = new URL(config?.url ?? "https://loading..."); |
| 65 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; | 71 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; |
| 66 | }, [readyState, config?.url]) | 72 | }, [readyState, config?.url]); |
| 67 | 73 | ||
| 68 | const [selectedRequestIndex, setSelectedRequestIndex] = useState<number | null>(null); | 74 | const [selectedRequestIndex, setSelectedRequestIndex] = useState< |
| 69 | const selectedRequest = useMemo<RequestResponse | null>(() => ( | 75 | number | null |
| 70 | selectedRequestIndex === null | 76 | >(null); |
| 71 | ? null | 77 | const selectedRequest = useMemo<RequestResponse | null>( |
| 72 | : calls[selectedRequestIndex] | 78 | () => (selectedRequestIndex === null ? null : calls[selectedRequestIndex]), |
| 73 | ), [selectedRequestIndex, calls]); | 79 | [selectedRequestIndex, calls] |
| 80 | ); | ||
| 74 | 81 | ||
| 75 | const settingsMenu: (SettingsMenu | null)[] = [ | 82 | const settingsMenu: (SettingsMenu | null)[] = [ |
| 76 | { | 83 | { |
| 77 | onClick: toggle, | 84 | onClick: toggle, |
| 78 | icon: darkMode ? <Sun />: <Moon />, | 85 | icon: darkMode ? <Sun /> : <Moon />, |
| 79 | label: darkMode ? "Light mode" : "Dark mode", | 86 | label: darkMode ? "Light mode" : "Dark mode", |
| 80 | }, | 87 | }, |
| 81 | null, | 88 | null, |
| @@ -85,40 +92,32 @@ export default function App() { | |||
| 85 | clear(); | 92 | clear(); |
| 86 | }, | 93 | }, |
| 87 | icon: <Trash />, | 94 | icon: <Trash />, |
| 88 | label: "Clear" | 95 | label: "Clear", |
| 89 | } | 96 | }, |
| 90 | ]; | 97 | ]; |
| 91 | 98 | ||
| 92 | return config && ( | 99 | return ( |
| 93 | <div className={styles.app}> | 100 | config && ( |
| 94 | <Navbar | 101 | <div className={styles.app}> |
| 95 | bg="dark" | 102 | <Navbar bg="dark" variant="dark" expand as="header"> |
| 96 | variant="dark" | 103 | <Container fluid> |
| 97 | expand | 104 | <div> |
| 98 | as="header" | 105 | <Navbar.Brand>TTUN</Navbar.Brand> |
| 99 | > | 106 | <Navbar.Text> |
| 100 | <Container fluid> | 107 | {`${statusIconMap[readyState]} ${statusTextMap[readyState]}`} |
| 101 | <div> | 108 | </Navbar.Text> |
| 102 | <Navbar.Brand> | 109 | </div> |
| 103 | TTUN | 110 | <div className="d-flex"> |
| 104 | </Navbar.Brand> | 111 | <Navbar.Text> |
| 105 | <Navbar.Text> | 112 | <a href={config.url} target="_blank"> |
| 106 | {`${statusIconMap[readyState]} ${statusTextMap[readyState]}`} | 113 | {config.url} |
| 107 | </Navbar.Text> | 114 | </a> |
| 108 | </div> | 115 | </Navbar.Text> |
| 109 | <div className="d-flex"> | 116 | <Navbar.Toggle aria-controls="settings" /> |
| 110 | <Navbar.Text> | 117 | <Navbar.Collapse id="settings" className="ms-2"> |
| 111 | <a href={config.url} target="_blank">{config.url}</a> | 118 | <Nav> |
| 112 | </Navbar.Text> | 119 | <NavDropdown align="end" title={<Sliders />}> |
| 113 | <Navbar.Toggle aria-controls="settings"/> | 120 | {settingsMenu.map((item) => { |
| 114 | <Navbar.Collapse id="settings" className="ms-2"> | ||
| 115 | <Nav> | ||
| 116 | <NavDropdown | ||
| 117 | align="end" | ||
| 118 | title={<Sliders/>} | ||
| 119 | > | ||
| 120 | { | ||
| 121 | settingsMenu.map((item) => { | ||
| 122 | if (item !== null) { | 121 | if (item !== null) { |
| 123 | const { onClick, icon, label } = item; | 122 | const { onClick, icon, label } = item; |
| 124 | return ( | 123 | return ( |
| @@ -129,27 +128,31 @@ export default function App() { | |||
| 129 | {icon} | 128 | {icon} |
| 130 | <span className="ms-3">{label}</span> | 129 | <span className="ms-3">{label}</span> |
| 131 | </NavDropdown.Item> | 130 | </NavDropdown.Item> |
| 132 | ) | 131 | ); |
| 133 | } else { | 132 | } else { |
| 134 | return <NavDropdown.Divider /> | 133 | return <NavDropdown.Divider />; |
| 135 | } | 134 | } |
| 136 | }) | 135 | })} |
| 137 | } | 136 | </NavDropdown> |
| 138 | </NavDropdown> | 137 | </Nav> |
| 139 | </Nav> | 138 | </Navbar.Collapse> |
| 140 | </Navbar.Collapse> | 139 | </div> |
| 141 | </div> | 140 | </Container> |
| 142 | </Container> | 141 | </Navbar> |
| 143 | </Navbar> | ||
| 144 | 142 | ||
| 145 | <main className={styles.main}> | 143 | <main className={styles.main}> |
| 146 | <div className={classNames("border-end", styles.sidebar)}> | 144 | <div className={classNames("border-end", styles.sidebar)}> |
| 147 | <RequestList requests={calls} selectedRequestIndex={selectedRequestIndex} setSelectedRequestIndex={setSelectedRequestIndex}/> | 145 | <RequestList |
| 148 | </div> | 146 | requests={calls} |
| 149 | <div className={styles.details}> | 147 | selectedRequestIndex={selectedRequestIndex} |
| 150 | <RequestDetails requestResponse={selectedRequest} /> | 148 | setSelectedRequestIndex={setSelectedRequestIndex} |
| 151 | </div> | 149 | /> |
| 152 | </main> | 150 | </div> |
| 153 | </div> | 151 | <div className={styles.details}> |
| 152 | <RequestDetails requestResponse={selectedRequest} /> | ||
| 153 | </div> | ||
| 154 | </main> | ||
| 155 | </div> | ||
| 156 | ) | ||
| 154 | ); | 157 | ); |
| 155 | } | 158 | } |
diff --git a/src/components/Content/Content.tsx b/src/components/Content/Content.tsx index c7a5f58..56a802f 100644 --- a/src/components/Content/Content.tsx +++ b/src/components/Content/Content.tsx | |||
| @@ -1,120 +1,141 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import {Dispatch, SetStateAction, useContext, useMemo} from "react"; | 2 | import { Dispatch, SetStateAction, useContext, useMemo } from "react"; |
| 3 | import {RequestPayload, ResponsePayload, Headers} from "~hooks/useRequests"; | 3 | import { RequestPayload, ResponsePayload, Headers } from "~hooks/useRequests"; |
| 4 | import ReactJson from 'react-json-view'; | 4 | import ReactJson from "react-json-view"; |
| 5 | import styles from './Content.module.scss'; | 5 | import styles from "./Content.module.scss"; |
| 6 | import {Button, Col, Container, Row} from "react-bootstrap"; | 6 | import { Button, Col, Container, Row } from "react-bootstrap"; |
| 7 | import {DarkModeContext} from "../../contexts/DarkMode"; | 7 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 8 | 8 | ||
| 9 | function getHeader(headers: Headers, key: string, unit?: string): string | null { | 9 | function getHeader( |
| 10 | headers: Headers, | ||
| 11 | key: string, | ||
| 12 | unit?: string | ||
| 13 | ): string | null { | ||
| 10 | try { | 14 | try { |
| 11 | const [_, value] = headers.find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase()) | 15 | const [_, value] = headers.find( |
| 12 | return unit !== undefined | 16 | ([headerKey]) => headerKey.toLowerCase() === key.toLowerCase() |
| 13 | ? `${value}${unit}` | 17 | ); |
| 14 | : value | 18 | return unit !== undefined ? `${value}${unit}` : value; |
| 15 | } catch { | 19 | } catch { |
| 16 | return null; | 20 | return null; |
| 17 | } | 21 | } |
| 18 | } | 22 | } |
| 19 | 23 | ||
| 20 | interface ContentProps { | 24 | interface ContentProps { |
| 21 | data: RequestPayload | ResponsePayload | 25 | data: RequestPayload | ResponsePayload; |
| 22 | setRaw: Dispatch<SetStateAction<boolean>> | 26 | setRaw: Dispatch<SetStateAction<boolean>>; |
| 23 | raw?: boolean | 27 | raw?: boolean; |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 26 | export default function Content({ raw, setRaw, data }: ContentProps): JSX.Element { | 30 | export default function Content({ |
| 31 | raw, | ||
| 32 | setRaw, | ||
| 33 | data, | ||
| 34 | }: ContentProps): JSX.Element { | ||
| 27 | return ( | 35 | return ( |
| 28 | <div className={styles.content}> | 36 | <div className={styles.content}> |
| 29 | <Container fluid className="border-bottom"> | 37 | <Container fluid className="border-bottom"> |
| 30 | <Row className="py-3"> | 38 | <Row className="py-3"> |
| 31 | <Col className="form-check form-switch ms-3"> | 39 | <Col className="form-check form-switch ms-3"> |
| 32 | <input className="form-check-input" id='raw' type='checkbox' checked={raw} role="switch" onChange={() => setRaw(!raw)}/> | 40 | <input |
| 33 | <label htmlFor='raw' className="form-check-label">Raw</label> | 41 | className="form-check-input" |
| 34 | </Col> | 42 | id="raw" |
| 43 | type="checkbox" | ||
| 44 | checked={raw} | ||
| 45 | role="switch" | ||
| 46 | onChange={() => setRaw(!raw)} | ||
| 47 | /> | ||
| 48 | <label htmlFor="raw" className="form-check-label"> | ||
| 49 | Raw | ||
| 50 | </label> | ||
| 51 | </Col> | ||
| 35 | <Col xs="auto"> | 52 | <Col xs="auto"> |
| 36 | { | 53 | {[ |
| 37 | [ | 54 | getHeader(data.headers, "content-length", "bytes"), |
| 38 | getHeader(data.headers, 'content-length', 'bytes'), | 55 | getHeader(data.headers, "content-type"), |
| 39 | getHeader(data.headers, 'content-type'), | 56 | ] |
| 40 | ].filter(x => x !== null).join('; ') | 57 | .filter((x) => x !== null) |
| 41 | } | 58 | .join("; ")} |
| 42 | </Col> | 59 | </Col> |
| 43 | </Row> | 60 | </Row> |
| 44 | </Container> | 61 | </Container> |
| 45 | <Row className={styles.body}> | 62 | <Row className={styles.body}> |
| 46 | {(() => { | 63 | {(() => { |
| 47 | try { | 64 | try { |
| 48 | return ContentBody({ data, raw }) | 65 | return ContentBody({ data, raw }); |
| 49 | } catch { | 66 | } catch { |
| 50 | return ( | 67 | return ( |
| 51 | <div className={styles.renderError}> | 68 | <div className={styles.renderError}> |
| 52 | <p>Body could not be rendered</p> | 69 | <p>Body could not be rendered</p> |
| 53 | <Button variant="link" onClick={() => setRaw(true)}>View raw</Button> | 70 | <Button variant="link" onClick={() => setRaw(true)}> |
| 71 | View raw | ||
| 72 | </Button> | ||
| 54 | </div> | 73 | </div> |
| 55 | ) | 74 | ); |
| 56 | } | 75 | } |
| 57 | })()} | 76 | })()} |
| 58 | </Row> | 77 | </Row> |
| 59 | </div > | 78 | </div> |
| 60 | ) | 79 | ); |
| 61 | }; | 80 | } |
| 62 | |||
| 63 | function ContentBody({ data, raw = false }: Omit<ContentProps, 'setRaw'>) { | ||
| 64 | const { darkMode } = useContext(DarkModeContext); | ||
| 65 | const contentType = useMemo(() => { | ||
| 66 | if (raw) { | ||
| 67 | return ''; | ||
| 68 | } | ||
| 69 | |||
| 70 | const type = getHeader(data.headers, 'content-type'); | ||
| 71 | return type.toLowerCase().split(';')[0]; | ||
| 72 | }, [data, raw]); | ||
| 73 | 81 | ||
| 82 | function ContentBody({ data, raw = false }: Omit<ContentProps, "setRaw">) { | ||
| 83 | const { darkMode } = useContext(DarkModeContext); | ||
| 84 | const contentType = useMemo(() => { | ||
| 74 | if (raw) { | 85 | if (raw) { |
| 75 | return ( | 86 | return ""; |
| 76 | <pre className="mb-0"> | ||
| 77 | <code> | ||
| 78 | {atob(data.body)} | ||
| 79 | </code> | ||
| 80 | </pre> | ||
| 81 | ) | ||
| 82 | } | 87 | } |
| 83 | 88 | ||
| 84 | if (['application/pdf', 'text/html'].includes(contentType)) { | 89 | const type = getHeader(data.headers, "content-type"); |
| 85 | return <iframe | 90 | return type.toLowerCase().split(";")[0]; |
| 91 | }, [data, raw]); | ||
| 92 | |||
| 93 | if (raw) { | ||
| 94 | return ( | ||
| 95 | <pre className="mb-0"> | ||
| 96 | <code>{atob(data.body)}</code> | ||
| 97 | </pre> | ||
| 98 | ); | ||
| 99 | } | ||
| 100 | |||
| 101 | if (["application/pdf", "text/html"].includes(contentType)) { | ||
| 102 | return ( | ||
| 103 | <iframe | ||
| 86 | className="bg-white" | 104 | className="bg-white" |
| 87 | src={`data:${contentType};base64,${data.body}`} | 105 | src={`data:${contentType};base64,${data.body}`} |
| 88 | srcDoc={contentType === 'text/html' ? atob(data.body) : undefined} | 106 | srcDoc={contentType === "text/html" ? atob(data.body) : undefined} |
| 89 | loading='lazy' | 107 | loading="lazy" |
| 90 | sandbox='' | 108 | sandbox="" |
| 91 | /> | 109 | /> |
| 92 | } | 110 | ); |
| 111 | } | ||
| 93 | 112 | ||
| 94 | if (contentType.startsWith('application/json')) { | 113 | if (contentType.startsWith("application/json")) { |
| 95 | return <ReactJson | 114 | return ( |
| 115 | <ReactJson | ||
| 96 | src={JSON.parse(atob(data.body))} | 116 | src={JSON.parse(atob(data.body))} |
| 97 | theme={darkMode ? "monokai" : undefined} | 117 | theme={darkMode ? "monokai" : undefined} |
| 98 | style={{ | 118 | style={{ |
| 99 | padding: '1em', | 119 | padding: "1em", |
| 100 | width: '100%', | 120 | width: "100%", |
| 101 | height: '100%', | 121 | height: "100%", |
| 102 | overflowY: 'auto', | 122 | overflowY: "auto", |
| 103 | }} | 123 | }} |
| 104 | /> | 124 | /> |
| 105 | } | 125 | ); |
| 126 | } | ||
| 106 | 127 | ||
| 107 | if (contentType.startsWith('audio')) { | 128 | if (contentType.startsWith("audio")) { |
| 108 | return <audio src={`data:${contentType};base64,${data.body}`} /> | 129 | return <audio src={`data:${contentType};base64,${data.body}`} />; |
| 109 | } | 130 | } |
| 110 | 131 | ||
| 111 | if (contentType.startsWith('video')) { | 132 | if (contentType.startsWith("video")) { |
| 112 | return <video src={`data:${contentType};base64,${data.body}`} /> | 133 | return <video src={`data:${contentType};base64,${data.body}`} />; |
| 113 | } | 134 | } |
| 114 | 135 | ||
| 115 | if (contentType.startsWith('image')) { | 136 | if (contentType.startsWith("image")) { |
| 116 | return <img src={`data:${contentType};base64,${data.body}`} alt=''/> | 137 | return <img src={`data:${contentType};base64,${data.body}`} alt="" />; |
| 117 | } | 138 | } |
| 118 | 139 | ||
| 119 | throw new Error('Not Rendered'); | 140 | throw new Error("Not Rendered"); |
| 120 | }; | 141 | } |
diff --git a/src/components/Icons/Filter.tsx b/src/components/Icons/Filter.tsx index a14183d..07796df 100644 --- a/src/components/Icons/Filter.tsx +++ b/src/components/Icons/Filter.tsx | |||
| @@ -1,10 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Filter() { | 3 | export function Filter() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" | 4 | return ( |
| 5 | height="16" fill="currentColor" className="bi bi-filter" | 5 | <svg |
| 6 | viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | <path | 7 | width="16" |
| 8 | d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/> | 8 | height="16" |
| 9 | </svg>; | 9 | fill="currentColor" |
| 10 | className="bi bi-filter" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 10 | } | 16 | } |
diff --git a/src/components/Icons/Moon.tsx b/src/components/Icons/Moon.tsx index 7e3e680..303da04 100644 --- a/src/components/Icons/Moon.tsx +++ b/src/components/Icons/Moon.tsx | |||
| @@ -1,11 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Moon() { | 3 | export function Moon() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" | 4 | return ( |
| 5 | width="16" height="16" fill="currentColor" | 5 | <svg |
| 6 | className="bi bi-moon" | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | viewBox="0 0 16 16"> | 7 | width="16" |
| 8 | <path | 8 | height="16" |
| 9 | d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"/> | 9 | fill="currentColor" |
| 10 | </svg>; | 10 | className="bi bi-moon" |
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 11 | } | 16 | } |
diff --git a/src/components/Icons/Sliders.tsx b/src/components/Icons/Sliders.tsx index 9feef34..36c632f 100644 --- a/src/components/Icons/Sliders.tsx +++ b/src/components/Icons/Sliders.tsx | |||
| @@ -1,10 +1,19 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Sliders() { | 3 | export function Sliders() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" | 4 | return ( |
| 5 | height="16" fill="currentColor" | 5 | <svg |
| 6 | className="bi bi-sliders" viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | <path fill-rule="evenodd" | 7 | width="16" |
| 8 | 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"/> | 8 | height="16" |
| 9 | </svg>; | 9 | fill="currentColor" |
| 10 | className="bi bi-sliders" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path | ||
| 14 | fill-rule="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" | ||
| 16 | /> | ||
| 17 | </svg> | ||
| 18 | ); | ||
| 10 | } | 19 | } |
diff --git a/src/components/Icons/Sun.tsx b/src/components/Icons/Sun.tsx index 93a5187..f1e37a6 100644 --- a/src/components/Icons/Sun.tsx +++ b/src/components/Icons/Sun.tsx | |||
| @@ -1,9 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Sun() { | 3 | export function Sun() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" | 4 | return ( |
| 5 | fill="currentColor" className="bi bi-sun" viewBox="0 0 16 16"> | 5 | <svg |
| 6 | <path | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/> | 7 | width="16" |
| 8 | </svg>; | 8 | height="16" |
| 9 | fill="currentColor" | ||
| 10 | className="bi bi-sun" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 9 | } | 16 | } |
diff --git a/src/components/Icons/Trash.tsx b/src/components/Icons/Trash.tsx index b1fb2c3..6c0039b 100644 --- a/src/components/Icons/Trash.tsx +++ b/src/components/Icons/Trash.tsx | |||
| @@ -2,12 +2,19 @@ import * as React from "react"; | |||
| 2 | 2 | ||
| 3 | export default function Trash() { | 3 | export default function Trash() { |
| 4 | return ( | 4 | return ( |
| 5 | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" | 5 | <svg |
| 6 | fill="currentColor" className="bi bi-trash" viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | width="16" | ||
| 8 | height="16" | ||
| 9 | fill="currentColor" | ||
| 10 | className="bi bi-trash" | ||
| 11 | viewBox="0 0 16 16" | ||
| 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" /> | ||
| 7 | <path | 14 | <path |
| 8 | 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"/> | 15 | fill-rule="evenodd" |
| 9 | <path fill-rule="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" |
| 10 | 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 | /> |
| 11 | </svg> | 18 | </svg> |
| 12 | ) | 19 | ); |
| 13 | } | 20 | } |
diff --git a/src/components/RequestDetails/RequestDetails.tsx b/src/components/RequestDetails/RequestDetails.tsx index 34e572c..693f691 100644 --- a/src/components/RequestDetails/RequestDetails.tsx +++ b/src/components/RequestDetails/RequestDetails.tsx | |||
| @@ -1,94 +1,87 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import {useCallback, useMemo, useState} from "react"; | 2 | import { useCallback, useMemo, useState } from "react"; |
| 3 | import {RequestResponse, Headers} from "../../hooks/useRequests"; | 3 | import { RequestResponse, Headers } from "../../hooks/useRequests"; |
| 4 | import styles from "./RequestDetails.module.scss"; | 4 | import styles from "./RequestDetails.module.scss"; |
| 5 | import RequestSummary from "../RequestSummary/RequestSummary"; | 5 | import RequestSummary from "../RequestSummary/RequestSummary"; |
| 6 | import Content from "../Content/Content"; | 6 | import Content from "../Content/Content"; |
| 7 | import {getHost} from "../../utils"; | 7 | import { getHost } from "../../utils"; |
| 8 | import {Button, Card, Col, Container, Nav, Row, Table} from "react-bootstrap"; | 8 | import { Button, Card, Col, Container, Nav, Row, Table } from "react-bootstrap"; |
| 9 | |||
| 10 | 9 | ||
| 11 | interface TimingProps { | 10 | interface TimingProps { |
| 12 | timing: number; | 11 | timing: number; |
| 13 | } | 12 | } |
| 14 | 13 | ||
| 15 | |||
| 16 | function Timing({ timing }: TimingProps) { | 14 | function Timing({ timing }: TimingProps) { |
| 17 | const value = useMemo(() => (Math.round(timing * 1000) / 1000), [timing]); | 15 | const value = useMemo(() => Math.round(timing * 1000) / 1000, [timing]); |
| 18 | const showSeconds = useMemo(() => value > 1, [value]); | 16 | const showSeconds = useMemo(() => value > 1, [value]); |
| 19 | 17 | ||
| 20 | return !Number.isNaN(value) | 18 | return !Number.isNaN(value) ? ( |
| 21 | ? ( | 19 | <>{`${showSeconds ? value : value * 1000}${showSeconds ? "s" : "ms"}`}</> |
| 22 | <> | 20 | ) : null; |
| 23 | {`${showSeconds ? value : value * 1000}${ showSeconds ? 's' : 'ms' }`} | ||
| 24 | </> | ||
| 25 | ) | ||
| 26 | : null; | ||
| 27 | } | 21 | } |
| 28 | 22 | ||
| 29 | interface HeadersProps { | 23 | interface HeadersProps { |
| 30 | title: string | 24 | title: string; |
| 31 | headers: Headers | 25 | headers: Headers; |
| 32 | } | 26 | } |
| 33 | 27 | ||
| 34 | function Headers({ title, headers }: HeadersProps) { | 28 | function Headers({ title, headers }: HeadersProps) { |
| 35 | return ( | 29 | return ( |
| 36 | <Card className="m-3"> | 30 | <Card className="m-3"> |
| 37 | <Table | 31 | <Table striped responsive borderless hover className="mb-0"> |
| 38 | striped | 32 | <thead> |
| 39 | responsive | 33 | <tr> |
| 40 | borderless | 34 | <th colSpan={2} className="bg-dark text-white rounded-top"> |
| 41 | hover | 35 | {title} |
| 42 | className='mb-0' | 36 | </th> |
| 43 | > | 37 | </tr> |
| 44 | <thead> | 38 | </thead> |
| 39 | <tbody> | ||
| 40 | {headers.map(([key, value]) => ( | ||
| 45 | <tr> | 41 | <tr> |
| 46 | <th colSpan={2} className="bg-dark text-white rounded-top">{ title }</th> | 42 | <td>{key}</td> |
| 43 | <td>{value}</td> | ||
| 47 | </tr> | 44 | </tr> |
| 48 | </thead> | 45 | ))} |
| 49 | <tbody> | 46 | </tbody> |
| 50 | { | 47 | </Table> |
| 51 | headers.map(([key, value]) => ( | ||
| 52 | <tr> | ||
| 53 | <td>{key}</td> | ||
| 54 | <td>{value}</td> | ||
| 55 | </tr> | ||
| 56 | )) | ||
| 57 | } | ||
| 58 | </tbody> | ||
| 59 | </Table> | ||
| 60 | </Card> | 48 | </Card> |
| 61 | ) | 49 | ); |
| 62 | } | 50 | } |
| 63 | 51 | ||
| 64 | interface DetailsProps { | 52 | interface DetailsProps { |
| 65 | requestResponse: RequestResponse | null | 53 | requestResponse: RequestResponse | null; |
| 66 | } | 54 | } |
| 67 | 55 | ||
| 68 | type Tab = 'headers' | 'request' | 'response' | 56 | type Tab = "headers" | "request" | "response"; |
| 69 | 57 | ||
| 70 | export default function RequestDetails({ requestResponse }: DetailsProps) { | 58 | export default function RequestDetails({ requestResponse }: DetailsProps) { |
| 71 | const [tab, selectTab] = useState<Tab>('headers'); | 59 | const [tab, selectTab] = useState<Tab>("headers"); |
| 72 | const [raw, setRaw] = useState(false); | 60 | const [raw, setRaw] = useState(false); |
| 73 | 61 | ||
| 74 | const resend = useCallback(async () => requestResponse !== null && fetch( | 62 | const resend = useCallback( |
| 75 | `http://${getHost()}/resend/`, | 63 | async () => |
| 76 | { | 64 | requestResponse !== null && |
| 77 | method: 'POST', | 65 | fetch(`http://${getHost()}/resend/`, { |
| 78 | body: JSON.stringify({ | 66 | method: "POST", |
| 79 | ...requestResponse.request, | 67 | body: JSON.stringify({ |
| 80 | id: undefined | 68 | ...requestResponse.request, |
| 81 | }) | 69 | id: undefined, |
| 82 | } | 70 | }), |
| 83 | ), [requestResponse]); | 71 | }), |
| 72 | [requestResponse] | ||
| 73 | ); | ||
| 84 | 74 | ||
| 85 | return requestResponse !== null ? ( | 75 | return requestResponse !== null ? ( |
| 86 | <div className={styles.details}> | 76 | <div className={styles.details}> |
| 87 | <div className={styles.header}> | 77 | <div className={styles.header}> |
| 88 | <Row> | 78 | <Row> |
| 89 | <Col> | 79 | <Col> |
| 90 | <Container fluid style={{ fontSize: '1.5em' }} className="py-3"> | 80 | <Container fluid style={{ fontSize: "1.5em" }} className="py-3"> |
| 91 | <RequestSummary requestResponse={requestResponse} className={styles.summary}/> | 81 | <RequestSummary |
| 82 | requestResponse={requestResponse} | ||
| 83 | className={styles.summary} | ||
| 84 | /> | ||
| 92 | </Container> | 85 | </Container> |
| 93 | </Col> | 86 | </Col> |
| 94 | </Row> | 87 | </Row> |
| @@ -100,14 +93,10 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 100 | onSelect={(tab) => selectTab(tab as Tab)} | 93 | onSelect={(tab) => selectTab(tab as Tab)} |
| 101 | > | 94 | > |
| 102 | <Nav.Item> | 95 | <Nav.Item> |
| 103 | <Nav.Link eventKey="headers"> | 96 | <Nav.Link eventKey="headers">Headers</Nav.Link> |
| 104 | Headers | ||
| 105 | </Nav.Link > | ||
| 106 | </Nav.Item> | 97 | </Nav.Item> |
| 107 | <Nav.Item> | 98 | <Nav.Item> |
| 108 | <Nav.Link eventKey="request"> | 99 | <Nav.Link eventKey="request">Request</Nav.Link> |
| 109 | Request | ||
| 110 | </Nav.Link> | ||
| 111 | </Nav.Item> | 100 | </Nav.Item> |
| 112 | <Nav.Item> | 101 | <Nav.Item> |
| 113 | <Nav.Link | 102 | <Nav.Link |
| @@ -121,46 +110,42 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 121 | </Col> | 110 | </Col> |
| 122 | <Col className="border-bottom px-3 " xs="auto"> | 111 | <Col className="border-bottom px-3 " xs="auto"> |
| 123 | <Timing timing={requestResponse.response?.timing ?? NaN} /> | 112 | <Timing timing={requestResponse.response?.timing ?? NaN} /> |
| 124 | <Button variant="outline-primary" onClick={() => resend()} className="ms-3"> | 113 | <Button |
| 114 | variant="outline-primary" | ||
| 115 | onClick={() => resend()} | ||
| 116 | className="ms-3" | ||
| 117 | > | ||
| 125 | Resend | 118 | Resend |
| 126 | </Button> | 119 | </Button> |
| 127 | </Col> | 120 | </Col> |
| 128 | </Row> | 121 | </Row> |
| 129 | </div> | 122 | </div> |
| 130 | <div className={styles.content}> | 123 | <div className={styles.content}> |
| 131 | { | 124 | {tab === "headers" && ( |
| 132 | tab === 'headers' && ( | 125 | <> |
| 133 | <> | 126 | <Headers |
| 127 | title="request headers" | ||
| 128 | headers={requestResponse.request.headers} | ||
| 129 | /> | ||
| 130 | {requestResponse.response && ( | ||
| 134 | <Headers | 131 | <Headers |
| 135 | title="request headers" | 132 | title="response headers" |
| 136 | headers={requestResponse.request.headers} | 133 | headers={requestResponse.response.headers} |
| 137 | /> | 134 | /> |
| 138 | { | 135 | )} |
| 139 | requestResponse.response && ( | 136 | </> |
| 140 | <Headers | 137 | )} |
| 141 | title="response headers" | 138 | {tab === "request" && ( |
| 142 | headers={requestResponse.response.headers} | 139 | <Content data={requestResponse.request} raw={raw} setRaw={setRaw} /> |
| 143 | /> | 140 | )} |
| 144 | ) | 141 | {tab === "response" && requestResponse.response !== undefined && ( |
| 145 | } | 142 | <Content data={requestResponse.response} raw={raw} setRaw={setRaw} /> |
| 146 | </> | 143 | )} |
| 147 | ) | ||
| 148 | } | ||
| 149 | { | ||
| 150 | tab === 'request' && ( | ||
| 151 | <Content data={requestResponse.request} raw={raw} setRaw={setRaw}/> | ||
| 152 | ) | ||
| 153 | } | ||
| 154 | { | ||
| 155 | tab === 'response' && requestResponse.response !== undefined && ( | ||
| 156 | <Content data={requestResponse.response} raw={raw} setRaw={setRaw}/> | ||
| 157 | ) | ||
| 158 | } | ||
| 159 | </div> | 144 | </div> |
| 160 | </div> | 145 | </div> |
| 161 | ) : ( | 146 | ) : ( |
| 162 | <div className={styles.noRequestSelected}> | 147 | <div className={styles.noRequestSelected}> |
| 163 | <p>Select a request to inspect it</p> | 148 | <p>Select a request to inspect it</p> |
| 164 | </div> | 149 | </div> |
| 165 | ) | 150 | ); |
| 166 | } | 151 | } |
diff --git a/src/components/RequestList/RequestList.module.scss b/src/components/RequestList/RequestList.module.scss index 47f36c8..8022978 100644 --- a/src/components/RequestList/RequestList.module.scss +++ b/src/components/RequestList/RequestList.module.scss | |||
| @@ -16,5 +16,4 @@ | |||
| 16 | overflow-y: hidden; | 16 | overflow-y: hidden; |
| 17 | display: flex; | 17 | display: flex; |
| 18 | flex-flow: column nowrap; | 18 | flex-flow: column nowrap; |
| 19 | |||
| 20 | } | 19 | } |
diff --git a/src/components/RequestList/RequestList.tsx b/src/components/RequestList/RequestList.tsx index 95c5b75..6980738 100644 --- a/src/components/RequestList/RequestList.tsx +++ b/src/components/RequestList/RequestList.tsx | |||
| @@ -11,26 +11,29 @@ import classNames from "classnames"; | |||
| 11 | import styles from "./RequestList.module.scss"; | 11 | import styles from "./RequestList.module.scss"; |
| 12 | import RequestSummary from "../RequestSummary/RequestSummary"; | 12 | import RequestSummary from "../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, useMemo, useState } from "react"; |
| 15 | import {Method, RequestResponse} from "../../hooks/useRequests"; | 15 | import { Method, RequestResponse } from "../../hooks/useRequests"; |
| 16 | import {DarkModeContext} from "../../contexts/DarkMode"; | 16 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 17 | import {Filter} from "../Icons/Filter"; | 17 | import { Filter } from "../Icons/Filter"; |
| 18 | 18 | ||
| 19 | interface ListProps { | 19 | interface ListProps { |
| 20 | requests: RequestResponse[], | 20 | requests: RequestResponse[]; |
| 21 | selectedRequestIndex: number | null | 21 | selectedRequestIndex: number | null; |
| 22 | setSelectedRequestIndex: (index: number) => void | 22 | setSelectedRequestIndex: (index: number) => void; |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | type EnabledMethods = { | 25 | type EnabledMethods = { |
| 26 | [method in Method]: boolean | 26 | [method in Method]: boolean; |
| 27 | } | 27 | }; |
| 28 | |||
| 29 | 28 | ||
| 30 | export default function RequestList({ requests, selectedRequestIndex, setSelectedRequestIndex }: ListProps) { | 29 | export default function RequestList({ |
| 31 | const {darkMode} = useContext(DarkModeContext) | 30 | requests, |
| 31 | selectedRequestIndex, | ||
| 32 | setSelectedRequestIndex, | ||
| 33 | }: ListProps) { | ||
| 34 | const { darkMode } = useContext(DarkModeContext); | ||
| 32 | const [showFilterOptions, setShowFilterOptions] = useState(false); | 35 | const [showFilterOptions, setShowFilterOptions] = useState(false); |
| 33 | const [search, setSearch] = useState(''); | 36 | const [search, setSearch] = useState(""); |
| 34 | const [enableRegex, setEnableRegex] = useState(false); | 37 | const [enableRegex, setEnableRegex] = useState(false); |
| 35 | 38 | ||
| 36 | const [methods, setMethods] = useState<EnabledMethods>({ | 39 | const [methods, setMethods] = useState<EnabledMethods>({ |
| @@ -49,7 +52,7 @@ export default function RequestList({ requests, selectedRequestIndex, setSelecte | |||
| 49 | PUT: enabled, | 52 | PUT: enabled, |
| 50 | PATCH: enabled, | 53 | PATCH: enabled, |
| 51 | DELETE: enabled, | 54 | DELETE: enabled, |
| 52 | } | 55 | }; |
| 53 | 56 | ||
| 54 | if (!enabled) { | 57 | if (!enabled) { |
| 55 | methods[method] = true; | 58 | methods[method] = true; |
| @@ -58,146 +61,168 @@ export default function RequestList({ requests, selectedRequestIndex, setSelecte | |||
| 58 | setMethods(methods); | 61 | setMethods(methods); |
| 59 | }, []); | 62 | }, []); |
| 60 | 63 | ||
| 61 | const enabledMethods: Method[] = useMemo(() => (Object | 64 | const enabledMethods: Method[] = useMemo( |
| 62 | .entries(methods) | 65 | () => |
| 63 | .filter(([method, enabled]) => enabled) | 66 | Object.entries(methods) |
| 64 | .map(([method]) => method) | 67 | .filter(([method, enabled]) => enabled) |
| 65 | ), [methods]); | 68 | .map(([method]) => method), |
| 69 | [methods] | ||
| 70 | ); | ||
| 66 | 71 | ||
| 67 | const methodLabel = useMemo(() => { | 72 | const methodLabel = useMemo(() => { |
| 68 | if (enabledMethods.length == 1) { | 73 | if (enabledMethods.length == 1) { |
| 69 | return enabledMethods[0] | 74 | return enabledMethods[0]; |
| 70 | } else if (enabledMethods.length === 5) { | 75 | } else if (enabledMethods.length === 5) { |
| 71 | return 'ANY' | 76 | return "ANY"; |
| 72 | } else { | 77 | } else { |
| 73 | return 'CUSTOM' | 78 | return "CUSTOM"; |
| 74 | } | 79 | } |
| 75 | }, [enabledMethods]); | 80 | }, [enabledMethods]); |
| 76 | 81 | ||
| 77 | |||
| 78 | |||
| 79 | const filteredRequests = useMemo(() => { | 82 | const filteredRequests = useMemo(() => { |
| 80 | let searchRegex = new RegExp(''); | 83 | let searchRegex = new RegExp(""); |
| 81 | try { | 84 | try { |
| 82 | searchRegex = new RegExp( | 85 | searchRegex = new RegExp( |
| 83 | enableRegex | 86 | enableRegex ? search : search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), |
| 84 | ? search | 87 | "i" |
| 85 | : search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), | 88 | ); |
| 86 | 'i'); | 89 | } catch {} |
| 87 | } catch { | ||
| 88 | |||
| 89 | } | ||
| 90 | 90 | ||
| 91 | return ( | 91 | return ( |
| 92 | ( | 92 | requests.map((request, index) => [index, request]).reverse() as [ |
| 93 | requests | 93 | number, |
| 94 | .map((request, index) => [index, request]) | 94 | RequestResponse |
| 95 | .reverse() as [number, RequestResponse][] | 95 | ][] |
| 96 | ) | ||
| 97 | .filter(([index, request]) => ( | ||
| 98 | enabledMethods.length > 0 === null || enabledMethods.includes(request.request.method) | ||
| 99 | )) | ||
| 100 | .filter(([index, request]) => ( | ||
| 101 | search === '' || searchRegex.test(`${request.request.method} ${request.request.path}`) | ||
| 102 | )) | ||
| 103 | ) | 96 | ) |
| 104 | }, [requests, search, enabledMethods, enableRegex]) | 97 | .filter( |
| 98 | ([index, request]) => | ||
| 99 | enabledMethods.length > 0 === null || | ||
| 100 | enabledMethods.includes(request.request.method) | ||
| 101 | ) | ||
| 102 | .filter( | ||
| 103 | ([index, request]) => | ||
| 104 | search === "" || | ||
| 105 | searchRegex.test(`${request.request.method} ${request.request.path}`) | ||
| 106 | ); | ||
| 107 | }, [requests, search, enabledMethods, enableRegex]); | ||
| 105 | 108 | ||
| 106 | return ( | 109 | return ( |
| 107 | <div className={classNames(styles.listContainer, 'd-flex')}> | 110 | <div className={classNames(styles.listContainer, "d-flex")}> |
| 108 | <ListGroup variant="flush" | 111 | <ListGroup |
| 109 | className={classNames('flex-grow-0', 'border-bottom')}> | 112 | variant="flush" |
| 113 | className={classNames("flex-grow-0", "border-bottom")} | ||
| 114 | > | ||
| 110 | <ListGroup.Item> | 115 | <ListGroup.Item> |
| 111 | <InputGroup> | 116 | <InputGroup> |
| 112 | <DropdownButton variant="primary" title={methodLabel}> | 117 | <DropdownButton variant="primary" title={methodLabel}> |
| 113 | <Dropdown.Item | 118 | <Dropdown.Item onClick={() => toggleMethods(null)}> |
| 114 | onClick={() => toggleMethods(null)}>ANY</Dropdown.Item> | 119 | ANY |
| 115 | <Dropdown.Divider/> | 120 | </Dropdown.Item> |
| 116 | <Dropdown.Item | 121 | <Dropdown.Divider /> |
| 117 | onClick={() => toggleMethods('GET')}>GET</Dropdown.Item> | 122 | <Dropdown.Item onClick={() => toggleMethods("GET")}> |
| 118 | <Dropdown.Item | 123 | GET |
| 119 | onClick={() => toggleMethods('POST')}>POST</Dropdown.Item> | 124 | </Dropdown.Item> |
| 120 | <Dropdown.Item | 125 | <Dropdown.Item onClick={() => toggleMethods("POST")}> |
| 121 | onClick={() => toggleMethods('PUT')}>PUT</Dropdown.Item> | 126 | POST |
| 122 | <Dropdown.Item | 127 | </Dropdown.Item> |
| 123 | onClick={() => toggleMethods('PATCH')}>PATCH</Dropdown.Item> | 128 | <Dropdown.Item onClick={() => toggleMethods("PUT")}> |
| 124 | <Dropdown.Item | 129 | PUT |
| 125 | onClick={() => toggleMethods('DELETE')}>DELETE</Dropdown.Item> | 130 | </Dropdown.Item> |
| 131 | <Dropdown.Item onClick={() => toggleMethods("PATCH")}> | ||
| 132 | PATCH | ||
| 133 | </Dropdown.Item> | ||
| 134 | <Dropdown.Item onClick={() => toggleMethods("DELETE")}> | ||
| 135 | DELETE | ||
| 136 | </Dropdown.Item> | ||
| 126 | </DropdownButton> | 137 | </DropdownButton> |
| 127 | <Form.Control type="text" className='border-secondary' | 138 | <Form.Control |
| 128 | value={search} placeholder="Search..." | 139 | type="text" |
| 129 | onChange={({target}) => setSearch(target.value)}/> | 140 | className="border-secondary" |
| 141 | value={search} | ||
| 142 | placeholder="Search..." | ||
| 143 | onChange={({ target }) => setSearch(target.value)} | ||
| 144 | /> | ||
| 130 | <Button | 145 | <Button |
| 131 | variant={showFilterOptions ? 'secondary' : 'outline-secondary'} | 146 | variant={showFilterOptions ? "secondary" : "outline-secondary"} |
| 132 | onClick={() => setShowFilterOptions(true)}> | 147 | onClick={() => setShowFilterOptions(true)} |
| 133 | <Filter/> | 148 | > |
| 149 | <Filter /> | ||
| 134 | </Button> | 150 | </Button> |
| 135 | </InputGroup> | 151 | </InputGroup> |
| 136 | </ListGroup.Item> | 152 | </ListGroup.Item> |
| 137 | </ListGroup> | 153 | </ListGroup> |
| 138 | <ListGroup | 154 | <ListGroup |
| 139 | variant='flush' | 155 | variant="flush" |
| 140 | as="ul" | 156 | as="ul" |
| 141 | className={classNames(styles.list, 'flex-grow-1')} | 157 | className={classNames(styles.list, "flex-grow-1")} |
| 142 | > | 158 | > |
| 143 | { | 159 | {filteredRequests.length > 0 ? ( |
| 144 | filteredRequests.length > 0 | 160 | filteredRequests.map(([index, requestResponse]) => { |
| 145 | ? ( | 161 | const selected = selectedRequestIndex === index; |
| 146 | filteredRequests.map(([index, requestResponse]) => { | 162 | return ( |
| 147 | const selected = selectedRequestIndex === index; | 163 | <ListGroup.Item |
| 148 | return ( | 164 | as="li" |
| 149 | <ListGroup.Item | 165 | onClick={() => setSelectedRequestIndex(index)} |
| 150 | as="li" | 166 | key={`request-${index}`} |
| 151 | onClick={() => setSelectedRequestIndex(index)} | 167 | className={classNames({ |
| 152 | key={`request-${index}`} | 168 | "bg-primary": selected, |
| 153 | className={classNames({ | 169 | "text-light": selected, |
| 154 | 'bg-primary': selected, | 170 | "border-bottom": true, |
| 155 | 'text-light': selected, | 171 | })} |
| 156 | 'border-bottom': true | 172 | > |
| 157 | })} | 173 | <RequestSummary |
| 158 | > | 174 | requestResponse={requestResponse} |
| 159 | <RequestSummary | 175 | selected={selected} |
| 160 | requestResponse={requestResponse} | 176 | /> |
| 161 | selected={selected} | 177 | </ListGroup.Item> |
| 162 | /> | 178 | ); |
| 163 | </ListGroup.Item> | 179 | }) |
| 164 | ) | 180 | ) : ( |
| 165 | }) | 181 | <div className={styles.noRequest}> |
| 166 | ) | 182 | <p>No requests</p> |
| 167 | : ( | 183 | </div> |
| 168 | <div className={styles.noRequest}> | 184 | )} |
| 169 | <p>No requests</p> | ||
| 170 | </div> | ||
| 171 | ) | ||
| 172 | } | ||
| 173 | </ListGroup> | 185 | </ListGroup> |
| 174 | <Offcanvas className={ | 186 | <Offcanvas |
| 175 | classNames({ | 187 | className={classNames({ |
| 176 | 'bg-dark': darkMode | 188 | "bg-dark": darkMode, |
| 177 | }) | 189 | })} |
| 178 | } | 190 | show={showFilterOptions} |
| 179 | show={showFilterOptions} | 191 | onHide={() => setShowFilterOptions(false)} |
| 180 | onHide={() => setShowFilterOptions(false)}> | 192 | > |
| 181 | <Offcanvas.Header closeButton> | 193 | <Offcanvas.Header closeButton> |
| 182 | <Offcanvas.Title> | 194 | <Offcanvas.Title>Filter Options</Offcanvas.Title> |
| 183 | Filter Options | ||
| 184 | </Offcanvas.Title> | ||
| 185 | </Offcanvas.Header> | 195 | </Offcanvas.Header> |
| 186 | <Offcanvas.Body> | 196 | <Offcanvas.Body> |
| 187 | <Form.Group className="mb-4"> | 197 | <Form.Group className="mb-4"> |
| 188 | <Form.Label className="fw-bold">Search</Form.Label> | 198 | <Form.Label className="fw-bold">Search</Form.Label> |
| 189 | <Form.Control type="text" value={search} placeholder="Search..." | 199 | <Form.Control |
| 190 | onChange={({target}) => setSearch(target.value)}/> | 200 | type="text" |
| 191 | <Form.Check className="mt-2" type='switch' label='Regex search' checked={enableRegex} onChange={() => setEnableRegex(!enableRegex)} /> | 201 | value={search} |
| 202 | placeholder="Search..." | ||
| 203 | onChange={({ target }) => setSearch(target.value)} | ||
| 204 | /> | ||
| 205 | <Form.Check | ||
| 206 | className="mt-2" | ||
| 207 | type="switch" | ||
| 208 | label="Regex search" | ||
| 209 | checked={enableRegex} | ||
| 210 | onChange={() => setEnableRegex(!enableRegex)} | ||
| 211 | /> | ||
| 192 | </Form.Group> | 212 | </Form.Group> |
| 193 | <Form.Group> | 213 | <Form.Group> |
| 194 | <Form.Label className="fw-bold">Method</Form.Label> | 214 | <Form.Label className="fw-bold">Method</Form.Label> |
| 195 | {Object.entries(methods).map(([method, enabled]) => ( | 215 | {Object.entries(methods).map(([method, enabled]) => ( |
| 196 | <Form.Check type='switch' label={method} checked={enabled} onChange={() => setMethods({...methods, [method]: !enabled})} /> | 216 | <Form.Check |
| 217 | type="switch" | ||
| 218 | label={method} | ||
| 219 | checked={enabled} | ||
| 220 | onChange={() => setMethods({ ...methods, [method]: !enabled })} | ||
| 221 | /> | ||
| 197 | ))} | 222 | ))} |
| 198 | </Form.Group> | 223 | </Form.Group> |
| 199 | </Offcanvas.Body> | 224 | </Offcanvas.Body> |
| 200 | </Offcanvas> | 225 | </Offcanvas> |
| 201 | </div> | 226 | </div> |
| 202 | ) | 227 | ); |
| 203 | } | 228 | } |
diff --git a/src/components/RequestSummary/RequestSummary.tsx b/src/components/RequestSummary/RequestSummary.tsx index b3e7ddc..db2f69c 100644 --- a/src/components/RequestSummary/RequestSummary.tsx +++ b/src/components/RequestSummary/RequestSummary.tsx | |||
| @@ -1,49 +1,54 @@ | |||
| 1 | import {RequestResponse} from "~hooks/useRequests"; | 1 | import { RequestResponse } from "~hooks/useRequests"; |
| 2 | import * as React from "react"; | 2 | import * as React from "react"; |
| 3 | import classNames from "classnames"; | 3 | import classNames from "classnames"; |
| 4 | 4 | ||
| 5 | import {Badge, Card, Col, Row} from "react-bootstrap"; | 5 | import { Badge, Card, Col, Row } from "react-bootstrap"; |
| 6 | 6 | ||
| 7 | interface RequestSummaryProps { | 7 | interface RequestSummaryProps { |
| 8 | selected?: boolean | 8 | selected?: boolean; |
| 9 | requestResponse: RequestResponse | 9 | requestResponse: RequestResponse; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | function isBetween(value: number, min: number, max: number) { | 12 | function isBetween(value: number, min: number, max: number) { |
| 13 | return value >= min && value <= max | 13 | return value >= min && value <= max; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | function calcBadgeVariant(statusCode: number | undefined): string { | 16 | function calcBadgeVariant(statusCode: number | undefined): string { |
| 17 | if (statusCode === undefined) { | 17 | if (statusCode === undefined) { |
| 18 | return 'secondary'; | 18 | return "secondary"; |
| 19 | } else if (isBetween(statusCode, 100, 199)) { | 19 | } else if (isBetween(statusCode, 100, 199)) { |
| 20 | return 'info'; | 20 | return "info"; |
| 21 | } else if (isBetween(statusCode, 200, 299)) { | 21 | } else if (isBetween(statusCode, 200, 299)) { |
| 22 | return 'success'; | 22 | return "success"; |
| 23 | } else if (isBetween(statusCode, 300, 399)) { | 23 | } else if (isBetween(statusCode, 300, 399)) { |
| 24 | return 'primary'; | 24 | return "primary"; |
| 25 | } else if (isBetween(statusCode, 400, 499)) { | 25 | } else if (isBetween(statusCode, 400, 499)) { |
| 26 | return 'danger'; | 26 | return "danger"; |
| 27 | } else if (isBetween(statusCode, 500, 599)) { | 27 | } else if (isBetween(statusCode, 500, 599)) { |
| 28 | return 'warning'; | 28 | return "warning"; |
| 29 | } | 29 | } |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | export default function RequestSummary({ requestResponse: { request, response }, selected = false }: RequestSummaryProps) { | 32 | export default function RequestSummary({ |
| 33 | requestResponse: { request, response }, | ||
| 34 | selected = false, | ||
| 35 | }: RequestSummaryProps) { | ||
| 33 | return ( | 36 | return ( |
| 34 | <Row> | 37 | <Row> |
| 35 | <Col className='flex-grow-0 d-flex align-items-center'>{request.method}</Col> | 38 | <Col className="flex-grow-0 d-flex align-items-center"> |
| 36 | <Col className='flex-grow-1'>{ request.path }</Col> | 39 | {request.method} |
| 37 | <Col className='flex-grow-0 d-flex align-items-center'> | 40 | </Col> |
| 41 | <Col className="flex-grow-1">{request.path}</Col> | ||
| 42 | <Col className="flex-grow-0 d-flex align-items-center"> | ||
| 38 | <Badge | 43 | <Badge |
| 39 | className={classNames({ | 44 | className={classNames({ |
| 40 | 'border': selected, | 45 | border: selected, |
| 41 | })} | 46 | })} |
| 42 | bg={calcBadgeVariant(response?.status)} | 47 | bg={calcBadgeVariant(response?.status)} |
| 43 | > | 48 | > |
| 44 | { response?.status ?? 'Loading...'} | 49 | {response?.status ?? "Loading..."} |
| 45 | </Badge> | 50 | </Badge> |
| 46 | </Col> | 51 | </Col> |
| 47 | </Row> | 52 | </Row> |
| 48 | ) | 53 | ); |
| 49 | } | 54 | } |
diff --git a/src/contexts/DarkMode.tsx b/src/contexts/DarkMode.tsx index 6dc1c31..90278ff 100644 --- a/src/contexts/DarkMode.tsx +++ b/src/contexts/DarkMode.tsx | |||
| @@ -3,39 +3,43 @@ import { | |||
| 3 | Context, | 3 | Context, |
| 4 | createContext, | 4 | createContext, |
| 5 | ReactElement, | 5 | ReactElement, |
| 6 | ReactNode, useEffect, | 6 | ReactNode, |
| 7 | useEffect, | ||
| 7 | useMemo, | 8 | useMemo, |
| 8 | useState | 9 | useState, |
| 9 | } from "react"; | 10 | } from "react"; |
| 10 | import {ThemeConfig} from "bootstrap-darkmode"; | 11 | import { ThemeConfig } from "bootstrap-darkmode"; |
| 11 | 12 | ||
| 12 | interface DarkModeApi { | 13 | interface DarkModeApi { |
| 13 | darkMode: boolean | 14 | darkMode: boolean; |
| 14 | toggle: () => void | 15 | toggle: () => void; |
| 15 | } | 16 | } |
| 16 | 17 | ||
| 17 | export const DarkModeContext = createContext<Partial<DarkModeApi>>({}) as Context<DarkModeApi>; | 18 | export const DarkModeContext = createContext<Partial<DarkModeApi>>( |
| 19 | {} | ||
| 20 | ) as Context<DarkModeApi>; | ||
| 18 | interface DarkModeProviderProps { | 21 | interface DarkModeProviderProps { |
| 19 | children: ReactNode | 22 | children: ReactNode; |
| 20 | } | 23 | } |
| 21 | 24 | ||
| 22 | export default function DarkModeProvider({ children }: DarkModeProviderProps) { | 25 | export default function DarkModeProvider({ children }: DarkModeProviderProps) { |
| 23 | const themeConfig = useMemo(() => new ThemeConfig(), []); | 26 | const themeConfig = useMemo(() => new ThemeConfig(), []); |
| 24 | const [darkMode, setDarkMode] = useState(() => themeConfig.getTheme() === 'dark') | 27 | const [darkMode, setDarkMode] = useState( |
| 28 | () => themeConfig.getTheme() === "dark" | ||
| 29 | ); | ||
| 25 | 30 | ||
| 26 | useEffect(() => { | 31 | useEffect(() => { |
| 27 | themeConfig.setTheme(darkMode ? 'dark' : 'light'); | 32 | themeConfig.setTheme(darkMode ? "dark" : "light"); |
| 28 | }, [darkMode]) | 33 | }, [darkMode]); |
| 29 | 34 | ||
| 30 | return ( | 35 | return ( |
| 31 | <DarkModeContext.Provider | 36 | <DarkModeContext.Provider |
| 32 | value={{ | 37 | value={{ |
| 33 | darkMode, | 38 | darkMode, |
| 34 | toggle: () => setDarkMode(dm => !dm), | 39 | toggle: () => setDarkMode((dm) => !dm), |
| 35 | }} | 40 | }} |
| 36 | > | 41 | > |
| 37 | |||
| 38 | {children} | 42 | {children} |
| 39 | </DarkModeContext.Provider> | 43 | </DarkModeContext.Provider> |
| 40 | ) | 44 | ); |
| 41 | } | 45 | } |
diff --git a/src/hooks/useRequests.tsx b/src/hooks/useRequests.tsx index c3fc2be..108232f 100644 --- a/src/hooks/useRequests.tsx +++ b/src/hooks/useRequests.tsx | |||
| @@ -1,43 +1,43 @@ | |||
| 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 | ||
| 4 | export type Headers = [string, string][] | 4 | export type Headers = [string, string][]; |
| 5 | export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 5 | export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; |
| 6 | 6 | ||
| 7 | export interface RequestPayload { | 7 | export interface RequestPayload { |
| 8 | id: string | 8 | id: string; |
| 9 | body: string | 9 | body: string; |
| 10 | headers: Headers | 10 | headers: Headers; |
| 11 | method: Method | 11 | method: Method; |
| 12 | path: string | 12 | path: string; |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | interface Request { | 15 | interface Request { |
| 16 | type: 'request', | 16 | type: "request"; |
| 17 | payload: RequestPayload, | 17 | payload: RequestPayload; |
| 18 | } | 18 | } |
| 19 | 19 | ||
| 20 | export interface ResponsePayload { | 20 | export interface ResponsePayload { |
| 21 | id: string | 21 | id: string; |
| 22 | timing: number | 22 | timing: number; |
| 23 | body: string | 23 | body: string; |
| 24 | headers: Headers | 24 | headers: Headers; |
| 25 | status: number | 25 | status: number; |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | interface Response { | 28 | interface Response { |
| 29 | type: 'response', | 29 | type: "response"; |
| 30 | payload: ResponsePayload, | 30 | payload: ResponsePayload; |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | interface Historic { | 33 | interface Historic { |
| 34 | type: 'historic' | 34 | type: "historic"; |
| 35 | payload: (Request | Response)[] | 35 | payload: (Request | Response)[]; |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | export interface RequestResponse { | 38 | export interface RequestResponse { |
| 39 | request: RequestPayload | 39 | request: RequestPayload; |
| 40 | response?: ResponsePayload | 40 | response?: ResponsePayload; |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | export enum ReadyState { | 43 | export enum ReadyState { |
| @@ -48,26 +48,28 @@ export enum ReadyState { | |||
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | export interface useRequestsProps { | 50 | export interface useRequestsProps { |
| 51 | onConnect: () => Promise<void> | 51 | onConnect: () => Promise<void>; |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | export interface UseRequests { | 54 | export interface UseRequests { |
| 55 | calls: RequestResponse[] | 55 | calls: RequestResponse[]; |
| 56 | readyState: ReadyState | 56 | readyState: ReadyState; |
| 57 | clear: () => void | 57 | clear: () => void; |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | export default function useRequests({ onConnect }: useRequestsProps): UseRequests { | 60 | export default function useRequests({ |
| 61 | onConnect, | ||
| 62 | }: useRequestsProps): UseRequests { | ||
| 61 | const wsHost = useMemo(getHost, []); | 63 | const wsHost = useMemo(getHost, []); |
| 62 | 64 | ||
| 63 | const [initialConnection, setInitialConnection] = useState(true); | 65 | const [initialConnection, setInitialConnection] = useState(true); |
| 64 | const [requests, setRequests] = useState<RequestPayload[]>([]); | 66 | const [requests, setRequests] = useState<RequestPayload[]>([]); |
| 65 | const [responses, setResponses] = useState<ResponsePayload[]>([]); | 67 | const [responses, setResponses] = useState<ResponsePayload[]>([]); |
| 66 | 68 | ||
| 67 | 69 | const connect = useCallback( | |
| 68 | const connect = useCallback(() => ( | 70 | () => new WebSocket(`ws://${wsHost}/inspect/`), |
| 69 | new WebSocket(`ws://${wsHost}/inspect/`) | 71 | [wsHost] |
| 70 | ), [wsHost]); | 72 | ); |
| 71 | 73 | ||
| 72 | const [ws, setWs] = useState<WebSocket>(() => connect()); | 74 | const [ws, setWs] = useState<WebSocket>(() => connect()); |
| 73 | const [readyState, setReadyState] = useState<ReadyState>(ws.readyState); | 75 | const [readyState, setReadyState] = useState<ReadyState>(ws.readyState); |
| @@ -78,53 +80,70 @@ export default function useRequests({ onConnect }: useRequestsProps): UseRequest | |||
| 78 | const onClose = () => { | 80 | const onClose = () => { |
| 79 | setReadyState(ws.readyState); | 81 | setReadyState(ws.readyState); |
| 80 | setWs(connect()); | 82 | setWs(connect()); |
| 81 | } | 83 | }; |
| 82 | const onOpen = () => { | 84 | const onOpen = () => { |
| 83 | onConnect(); | 85 | onConnect(); |
| 84 | setInitialConnection(false); | 86 | setInitialConnection(false); |
| 85 | setReadyState(ws.readyState); | 87 | setReadyState(ws.readyState); |
| 86 | } | 88 | }; |
| 87 | const onMessage = ({ data }) => { | 89 | const onMessage = ({ data }) => { |
| 88 | const { type, payload } = JSON.parse(data) as Historic | Request | Response | 90 | const { type, payload } = JSON.parse(data) as |
| 91 | | Historic | ||
| 92 | | Request | ||
| 93 | | Response; | ||
| 89 | 94 | ||
| 90 | switch (type) { | 95 | switch (type) { |
| 91 | case 'historic': | 96 | case "historic": |
| 92 | if (initialConnection) { | 97 | if (initialConnection) { |
| 93 | const requests = (payload as (Request | Response)[]).filter(({ type }) => type === 'request'); | 98 | const requests = (payload as (Request | Response)[]).filter( |
| 94 | const responses = (payload as (Request | Response)[]).filter(({ type }) => type === 'response'); | 99 | ({ type }) => type === "request" |
| 95 | setRequests((rqs) => [...rqs, ...requests.map(({ payload }) => payload as RequestPayload)]); | 100 | ); |
| 96 | setResponses((rps) => [...rps, ...responses.map(({ payload }) => payload as ResponsePayload)]); | 101 | const responses = (payload as (Request | Response)[]).filter( |
| 102 | ({ type }) => type === "response" | ||
| 103 | ); | ||
| 104 | setRequests((rqs) => [ | ||
| 105 | ...rqs, | ||
| 106 | ...requests.map(({ payload }) => payload as RequestPayload), | ||
| 107 | ]); | ||
| 108 | setResponses((rps) => [ | ||
| 109 | ...rps, | ||
| 110 | ...responses.map(({ payload }) => payload as ResponsePayload), | ||
| 111 | ]); | ||
| 97 | } | 112 | } |
| 98 | break | 113 | break; |
| 99 | case 'request': | 114 | case "request": |
| 100 | setRequests((rqs) => [...rqs, payload as RequestPayload]) | 115 | setRequests((rqs) => [...rqs, payload as RequestPayload]); |
| 101 | break | 116 | break; |
| 102 | case 'response': | 117 | case "response": |
| 103 | setResponses((rps) => [...rps, payload as ResponsePayload]) | 118 | setResponses((rps) => [...rps, payload as ResponsePayload]); |
| 104 | break | 119 | break; |
| 105 | } | 120 | } |
| 106 | } | 121 | }; |
| 107 | 122 | ||
| 108 | ws.addEventListener('message', onMessage) | 123 | ws.addEventListener("message", onMessage); |
| 109 | ws.addEventListener('close', onClose) | 124 | ws.addEventListener("close", onClose); |
| 110 | ws.addEventListener('open', onOpen) | 125 | ws.addEventListener("open", onOpen); |
| 111 | 126 | ||
| 112 | return () => { | 127 | return () => { |
| 113 | ws.removeEventListener('message', onMessage); | 128 | ws.removeEventListener("message", onMessage); |
| 114 | ws.removeEventListener('close', onClose); | 129 | ws.removeEventListener("close", onClose); |
| 115 | ws.removeEventListener('open', onOpen) | 130 | ws.removeEventListener("open", onOpen); |
| 116 | } | 131 | }; |
| 117 | }, [ws]) | 132 | }, [ws]); |
| 118 | 133 | ||
| 119 | return { | 134 | return { |
| 120 | calls: useMemo<RequestResponse[]>(() => requests.map((request) => ({ | 135 | calls: useMemo<RequestResponse[]>( |
| 121 | request: request, | 136 | () => |
| 122 | response: responses.find(({id}) => id === request.id) | 137 | requests.map((request) => ({ |
| 123 | })), [requests, responses]), | 138 | request: request, |
| 139 | response: responses.find(({ id }) => id === request.id), | ||
| 140 | })), | ||
| 141 | [requests, responses] | ||
| 142 | ), | ||
| 124 | readyState, | 143 | readyState, |
| 125 | clear: () => { | 144 | clear: () => { |
| 126 | setRequests([]) | 145 | setRequests([]); |
| 127 | setResponses([]) | 146 | setResponses([]); |
| 128 | } | 147 | }, |
| 129 | } | 148 | }; |
| 130 | } | 149 | } |
diff --git a/src/index.scss b/src/index.scss index e8236c1..1572e11 100644 --- a/src/index.scss +++ b/src/index.scss | |||
| @@ -6,7 +6,9 @@ | |||
| 6 | box-sizing: border-box; | 6 | box-sizing: border-box; |
| 7 | } | 7 | } |
| 8 | 8 | ||
| 9 | html, body, #root { | 9 | html, |
| 10 | body, | ||
| 11 | #root { | ||
| 10 | margin: 0; | 12 | margin: 0; |
| 11 | padding: 0; | 13 | padding: 0; |
| 12 | 14 | ||
diff --git a/src/index.tsx b/src/index.tsx index 037092d..a9fdbd7 100644 --- a/src/index.tsx +++ b/src/index.tsx | |||
| @@ -2,11 +2,12 @@ import * as React from "react"; | |||
| 2 | import * as ReactDOM from "react-dom"; | 2 | import * as ReactDOM from "react-dom"; |
| 3 | import App from "./components/App/App"; | 3 | import App from "./components/App/App"; |
| 4 | 4 | ||
| 5 | import './index.scss'; | 5 | import "./index.scss"; |
| 6 | import DarkModeProvider from "./contexts/DarkMode"; | 6 | import DarkModeProvider from "./contexts/DarkMode"; |
| 7 | 7 | ||
| 8 | ReactDOM.render(( | 8 | ReactDOM.render( |
| 9 | <DarkModeProvider> | 9 | <DarkModeProvider> |
| 10 | <App /> | 10 | <App /> |
| 11 | </DarkModeProvider> | 11 | </DarkModeProvider>, |
| 12 | ), document.getElementById("root")); | 12 | document.getElementById("root") |
| 13 | ); | ||
diff --git a/src/utils.ts b/src/utils.ts index dcf010a..2beebec 100644 --- a/src/utils.ts +++ b/src/utils.ts | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | export function getHost() { | 1 | export function getHost() { |
| 2 | // @ts-ignore | 2 | // @ts-ignore |
| 3 | return import.meta.env.DEV ? 'localhost:4040' : window.location.host | 3 | return import.meta.env.DEV ? "localhost:4040" : window.location.host; |
| 4 | } | 4 | } |
diff --git a/tsconfig.json b/tsconfig.json index 986e0c9..a8bbe91 100644 --- a/tsconfig.json +++ b/tsconfig.json | |||
| @@ -1,17 +1,13 @@ | |||
| 1 | { | 1 | { |
| 2 | "compilerOptions": { | 2 | "compilerOptions": { |
| 3 | "baseUrl": "./src", | 3 | "baseUrl": "./src", |
| 4 | "jsx": "react", | 4 | "jsx": "react", |
| 5 | "moduleResolution": "Node", | 5 | "moduleResolution": "Node", |
| 6 | "paths": { | 6 | "paths": { |
| 7 | "~*": [ | 7 | "~*": ["./*"] |
| 8 | "./*" | ||
| 9 | ] | ||
| 10 | }, | ||
| 11 | "strict": true, | ||
| 12 | "target": "ES2015" | ||
| 13 | }, | 8 | }, |
| 14 | "include": [ | 9 | "strict": true, |
| 15 | "src/**/*" | 10 | "target": "ES2015" |
| 16 | ] | 11 | }, |
| 17 | } \ No newline at end of file | 12 | "include": ["src/**/*"] |
| 13 | } | ||
diff --git a/ttun/__main__.py b/ttun/__main__.py index 52bf2fb..cee662a 100644 --- a/ttun/__main__.py +++ b/ttun/__main__.py | |||
| @@ -1,30 +1,31 @@ | |||
| 1 | import asyncio | 1 | import asyncio |
| 2 | from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter | 2 | from argparse import ArgumentDefaultsHelpFormatter |
| 3 | from argparse import ArgumentParser | ||
| 3 | from asyncio import FIRST_EXCEPTION | 4 | from asyncio import FIRST_EXCEPTION |
| 5 | from asyncio.exceptions import CancelledError | ||
| 6 | from asyncio.exceptions import TimeoutError | ||
| 4 | 7 | ||
| 5 | from ttun.client import Client | 8 | from ttun.client import Client |
| 6 | from asyncio.exceptions import TimeoutError, CancelledError | ||
| 7 | |||
| 8 | from ttun.inspect_server import Server | 9 | from ttun.inspect_server import Server |
| 9 | from ttun.settings import SERVER_HOSTNAME, SERVER_USING_SSL | 10 | from ttun.settings import SERVER_HOSTNAME |
| 11 | from ttun.settings import SERVER_USING_SSL | ||
| 10 | 12 | ||
| 11 | inspect_queue = asyncio.Queue() | 13 | inspect_queue = asyncio.Queue() |
| 12 | 14 | ||
| 15 | |||
| 13 | def main(): | 16 | def main(): |
| 14 | parser = ArgumentParser(prog='ttun', formatter_class=ArgumentDefaultsHelpFormatter) | 17 | parser = ArgumentParser(prog="ttun", formatter_class=ArgumentDefaultsHelpFormatter) |
| 15 | parser.add_argument( | 18 | parser.add_argument("port", help="The local port to expose") |
| 16 | 'port', | ||
| 17 | help='The local port to expose' | ||
| 18 | ) | ||
| 19 | parser.add_argument( | 19 | parser.add_argument( |
| 20 | '--server', | 20 | "--server", |
| 21 | default=f'{"wss" if SERVER_USING_SSL else "ws"}://{SERVER_HOSTNAME}', | 21 | default=f'{"wss" if SERVER_USING_SSL else "ws"}://{SERVER_HOSTNAME}', |
| 22 | help='The hostname of the ttun server', | 22 | help="The hostname of the ttun server", |
| 23 | ) | 23 | ) |
| 24 | parser.add_argument( | 24 | parser.add_argument( |
| 25 | '-s', '--subdomain', | 25 | "-s", |
| 26 | "--subdomain", | ||
| 26 | default=None, | 27 | default=None, |
| 27 | help = 'The subdomain of the ttun tunnel', | 28 | help="The subdomain of the ttun tunnel", |
| 28 | ) | 29 | ) |
| 29 | args = parser.parse_args() | 30 | args = parser.parse_args() |
| 30 | 31 | ||
| @@ -43,11 +44,11 @@ def main(): | |||
| 43 | loop.run_until_complete(client.connect()) | 44 | loop.run_until_complete(client.connect()) |
| 44 | 45 | ||
| 45 | def print_info(server: Server): | 46 | def print_info(server: Server): |
| 46 | print('Tunnel created:') | 47 | print("Tunnel created:") |
| 47 | print(f'{client.config["url"]} -> http://localhost:{args.port}') | 48 | print(f'{client.config["url"]} -> http://localhost:{args.port}') |
| 48 | print('') | 49 | print("") |
| 49 | print(f'Inspect requests:') | 50 | print(f"Inspect requests:") |
| 50 | print(f'http://localhost:{server.port}') | 51 | print(f"http://localhost:{server.port}") |
| 51 | 52 | ||
| 52 | server = Server( | 53 | server = Server( |
| 53 | config=client.config, | 54 | config=client.config, |
| @@ -55,11 +56,7 @@ def main(): | |||
| 55 | on_started=print_info, | 56 | on_started=print_info, |
| 56 | ) | 57 | ) |
| 57 | 58 | ||
| 58 | tasks = { | 59 | tasks = {loop.create_task(client.handle_messages()), loop.create_task(server.run())} |
| 59 | loop.create_task(client.handle_messages()), | ||
| 60 | loop.create_task(server.run()) | ||
| 61 | } | ||
| 62 | |||
| 63 | 60 | ||
| 64 | try: | 61 | try: |
| 65 | loop.run_until_complete(asyncio.wait(tasks, return_when=FIRST_EXCEPTION)) | 62 | loop.run_until_complete(asyncio.wait(tasks, return_when=FIRST_EXCEPTION)) |
| @@ -70,5 +67,6 @@ def main(): | |||
| 70 | except KeyboardInterrupt: | 67 | except KeyboardInterrupt: |
| 71 | pass | 68 | pass |
| 72 | 69 | ||
| 73 | if __name__ == '__main__': | 70 | |
| 71 | if __name__ == "__main__": | ||
| 74 | main() | 72 | main() |
diff --git a/ttun/client.py b/ttun/client.py index b4be4e7..41d1a86 100644 --- a/ttun/client.py +++ b/ttun/client.py | |||
| @@ -1,17 +1,24 @@ | |||
| 1 | import asyncio | 1 | import asyncio |
| 2 | import json | 2 | import json |
| 3 | from base64 import b64encode, b64decode | 3 | from base64 import b64decode |
| 4 | from base64 import b64encode | ||
| 4 | from time import perf_counter | 5 | from time import perf_counter |
| 5 | from typing import Optional, Callable, Coroutine, Awaitable | 6 | from typing import Awaitable |
| 7 | from typing import Callable | ||
| 8 | from typing import Coroutine | ||
| 9 | from typing import Optional | ||
| 6 | from uuid import uuid4 | 10 | from uuid import uuid4 |
| 7 | 11 | ||
| 8 | import websockets | 12 | import websockets |
| 9 | from aiohttp import ClientSession, DummyCookieJar | 13 | from aiohttp import ClientSession |
| 14 | from aiohttp import DummyCookieJar | ||
| 10 | from websockets import WebSocketClientProtocol | 15 | from websockets import WebSocketClientProtocol |
| 11 | from websockets.exceptions import ConnectionClosed | 16 | from websockets.exceptions import ConnectionClosed |
| 12 | 17 | ||
| 13 | from ttun.pubsub import PubSub | 18 | from ttun.pubsub import PubSub |
| 14 | from ttun.types import Config, RequestData, ResponseData | 19 | from ttun.types import Config |
| 20 | from ttun.types import RequestData | ||
| 21 | from ttun.types import ResponseData | ||
| 15 | 22 | ||
| 16 | 23 | ||
| 17 | class Client: | 24 | class Client: |
| @@ -41,14 +48,13 @@ class Client: | |||
| 41 | 48 | ||
| 42 | except ConnectionClosed: | 49 | except ConnectionClosed: |
| 43 | break | 50 | break |
| 51 | |||
| 44 | return wrapper | 52 | return wrapper |
| 45 | 53 | ||
| 46 | async def connect(self) -> WebSocketClientProtocol: | 54 | async def connect(self) -> WebSocketClientProtocol: |
| 47 | self.connection = await websockets.connect(f'{self.server}/tunnel/') | 55 | self.connection = await websockets.connect(f"{self.server}/tunnel/") |
| 48 | 56 | ||
| 49 | await self.send({ | 57 | await self.send({"subdomain": self.subdomain}) |
| 50 | 'subdomain': self.subdomain | ||
| 51 | }) | ||
| 52 | 58 | ||
| 53 | self.config = await self.receive() | 59 | self.config = await self.receive() |
| 54 | 60 | ||
| @@ -60,48 +66,49 @@ class Client: | |||
| 60 | try: | 66 | try: |
| 61 | request: RequestData = await self.receive() | 67 | request: RequestData = await self.receive() |
| 62 | await self.proxyRequest( | 68 | await self.proxyRequest( |
| 63 | request=request, | 69 | request=request, on_response=lambda response: self.send(response) |
| 64 | on_response=lambda response: self.send(response) | ||
| 65 | ) | 70 | ) |
| 66 | 71 | ||
| 67 | except ConnectionClosed: | 72 | except ConnectionClosed: |
| 68 | break | 73 | break |
| 69 | 74 | ||
| 70 | async def proxyRequest(self, request: RequestData, on_response: Callable[[ResponseData], Awaitable] = None): | 75 | async def proxyRequest( |
| 76 | self, | ||
| 77 | request: RequestData, | ||
| 78 | on_response: Callable[[ResponseData], Awaitable] = None, | ||
| 79 | ): | ||
| 71 | async with ClientSession(cookie_jar=DummyCookieJar()) as session: | 80 | async with ClientSession(cookie_jar=DummyCookieJar()) as session: |
| 72 | request_id = uuid4() | 81 | request_id = uuid4() |
| 73 | await PubSub.publish({ | 82 | await PubSub.publish( |
| 74 | "type": "request", | 83 | {"type": "request", "payload": {"id": request_id.hex, **request}} |
| 75 | "payload": { | 84 | ) |
| 76 | "id": request_id.hex, | ||
| 77 | **request | ||
| 78 | } | ||
| 79 | }) | ||
| 80 | 85 | ||
| 81 | start = perf_counter() | 86 | start = perf_counter() |
| 82 | response = await session.request( | 87 | response = await session.request( |
| 83 | method=request['method'], | 88 | method=request["method"], |
| 84 | url=f'http://localhost:{self.port}{request["path"]}', | 89 | url=f'http://localhost:{self.port}{request["path"]}', |
| 85 | headers=request['headers'], | 90 | headers=request["headers"], |
| 86 | data=b64decode(request['body'].encode()), | 91 | data=b64decode(request["body"].encode()), |
| 87 | allow_redirects=False | 92 | allow_redirects=False, |
| 88 | ) | 93 | ) |
| 89 | end = perf_counter() | 94 | end = perf_counter() |
| 90 | 95 | ||
| 91 | response_data = ResponseData( | 96 | response_data = ResponseData( |
| 92 | status=response.status, | 97 | status=response.status, |
| 93 | headers=list(response.headers.items()), | 98 | headers=list(response.headers.items()), |
| 94 | body=b64encode(await response.read()).decode() | 99 | body=b64encode(await response.read()).decode(), |
| 95 | ) | 100 | ) |
| 96 | 101 | ||
| 97 | if on_response is not None: | 102 | if on_response is not None: |
| 98 | await on_response(response_data) | 103 | await on_response(response_data) |
| 99 | 104 | ||
| 100 | await PubSub.publish({ | 105 | await PubSub.publish( |
| 101 | "type": "response", | 106 | { |
| 102 | "payload": { | 107 | "type": "response", |
| 103 | "id": request_id.hex, | 108 | "payload": { |
| 104 | "timing": end - start, | 109 | "id": request_id.hex, |
| 105 | **response_data, | 110 | "timing": end - start, |
| 111 | **response_data, | ||
| 112 | }, | ||
| 106 | } | 113 | } |
| 107 | }) | 114 | ) |
diff --git a/ttun/inspect_server.py b/ttun/inspect_server.py index ec5b58d..1cd8809 100644 --- a/ttun/inspect_server.py +++ b/ttun/inspect_server.py | |||
| @@ -1,36 +1,43 @@ | |||
| 1 | from importlib import resources | 1 | from importlib import resources |
| 2 | from pathlib import Path | 2 | from pathlib import Path |
| 3 | from typing import Callable, Awaitable, Optional | 3 | from typing import Awaitable |
| 4 | from typing import Callable | ||
| 5 | from typing import Optional | ||
| 4 | 6 | ||
| 5 | from aiohttp import web | 7 | from aiohttp import web |
| 6 | 8 | ||
| 7 | from ttun.pubsub import PubSub | 9 | from ttun.pubsub import PubSub |
| 8 | from ttun.types import Config, RequestData | 10 | from ttun.types import Config |
| 11 | from ttun.types import RequestData | ||
| 9 | 12 | ||
| 10 | BASE_DIR = Path(__file__).resolve().parent | 13 | BASE_DIR = Path(__file__).resolve().parent |
| 11 | 14 | ||
| 12 | 15 | ||
| 13 | class Server: | 16 | class Server: |
| 14 | def __init__(self, config: Config, on_resend: Callable[[RequestData], Awaitable], on_started: Callable[['Server'], None]): | 17 | def __init__( |
| 18 | self, | ||
| 19 | config: Config, | ||
| 20 | on_resend: Callable[[RequestData], Awaitable], | ||
| 21 | on_started: Callable[["Server"], None], | ||
| 22 | ): | ||
| 15 | self.port = 4040 | 23 | self.port = 4040 |
| 16 | 24 | ||
| 17 | self.config = { | 25 | self.config = {**config, "assets": "/assets/"} |
| 18 | **config, | ||
| 19 | 'assets': '/assets/' | ||
| 20 | } | ||
| 21 | 26 | ||
| 22 | self.on_resend = on_resend | 27 | self.on_resend = on_resend |
| 23 | self.on_started = lambda *args, **kwargs: on_started(self) | 28 | self.on_started = lambda *args, **kwargs: on_started(self) |
| 24 | 29 | ||
| 25 | self.app = web.Application() | 30 | self.app = web.Application() |
| 26 | with resources.path(__package__, 'staticfiles') as staticfiles: | 31 | with resources.path(__package__, "staticfiles") as staticfiles: |
| 27 | self.app.add_routes([ | 32 | self.app.add_routes( |
| 28 | web.get('/inspect/', self.inspect_socket), | 33 | [ |
| 29 | web.get('/config/', self.get_config), | 34 | web.get("/inspect/", self.inspect_socket), |
| 30 | web.post('/resend/', self.resend), | 35 | web.get("/config/", self.get_config), |
| 31 | web.get('/', self.root), | 36 | web.post("/resend/", self.resend), |
| 32 | web.static('/', staticfiles), | 37 | web.get("/", self.root), |
| 33 | ]) | 38 | web.static("/", staticfiles), |
| 39 | ] | ||
| 40 | ) | ||
| 34 | 41 | ||
| 35 | async def run(self): | 42 | async def run(self): |
| 36 | while True: | 43 | while True: |
| @@ -40,18 +47,15 @@ class Server: | |||
| 40 | self.port += 1 | 47 | self.port += 1 |
| 41 | 48 | ||
| 42 | async def root(self, request: web.Request): | 49 | async def root(self, request: web.Request): |
| 43 | with resources.path(__package__, 'staticfiles') as staticfiles: | 50 | with resources.path(__package__, "staticfiles") as staticfiles: |
| 44 | return web.Response( | 51 | return web.Response( |
| 45 | body=resources.read_text(f'{__package__}.staticfiles', f'index.html'), | 52 | body=resources.read_text(f"{__package__}.staticfiles", f"index.html"), |
| 46 | content_type='text/html' | 53 | content_type="text/html", |
| 47 | ) | 54 | ) |
| 48 | 55 | ||
| 49 | async def get_config(self, request: web.Request): | 56 | async def get_config(self, request: web.Request): |
| 50 | return web.json_response( | 57 | return web.json_response( |
| 51 | self.config, | 58 | self.config, headers={"Access-Control-Allow-Origin": "*"} |
| 52 | headers={ | ||
| 53 | 'Access-Control-Allow-Origin': '*' | ||
| 54 | } | ||
| 55 | ) | 59 | ) |
| 56 | 60 | ||
| 57 | async def resend(self, request: web.Request): | 61 | async def resend(self, request: web.Request): |
| @@ -62,10 +66,7 @@ class Server: | |||
| 62 | ws = web.WebSocketResponse() | 66 | ws = web.WebSocketResponse() |
| 63 | await ws.prepare(request) | 67 | await ws.prepare(request) |
| 64 | 68 | ||
| 65 | await ws.send_json({ | 69 | await ws.send_json({"type": "historic", "payload": PubSub.history}) |
| 66 | 'type': 'historic', | ||
| 67 | 'payload': PubSub.history | ||
| 68 | }) | ||
| 69 | 70 | ||
| 70 | with PubSub.subscribe() as subscription: | 71 | with PubSub.subscribe() as subscription: |
| 71 | while message := await subscription.get(): | 72 | while message := await subscription.get(): |
diff --git a/ttun/pubsub.py b/ttun/pubsub.py index 64dd2b8..0ba964e 100644 --- a/ttun/pubsub.py +++ b/ttun/pubsub.py | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | import asyncio | 1 | import asyncio |
| 2 | from contextlib import contextmanager | 2 | from contextlib import contextmanager |
| 3 | from typing import Any, Generator, Iterator | 3 | from typing import Any |
| 4 | from typing import Generator | ||
| 5 | from typing import Iterator | ||
| 4 | 6 | ||
| 5 | 7 | ||
| 6 | class PubSub: | 8 | class PubSub: |
| @@ -8,10 +10,10 @@ class PubSub: | |||
| 8 | 10 | ||
| 9 | def __init__(self): | 11 | def __init__(self): |
| 10 | self.queues: list[asyncio.Queue] = [] | 12 | self.queues: list[asyncio.Queue] = [] |
| 11 | self._history= [] | 13 | self._history = [] |
| 12 | 14 | ||
| 13 | @classmethod | 15 | @classmethod |
| 14 | def instance(cls) -> 'PubSub': | 16 | def instance(cls) -> "PubSub": |
| 15 | if cls._instance == None: | 17 | if cls._instance == None: |
| 16 | cls._instance = cls() | 18 | cls._instance = cls() |
| 17 | return cls._instance | 19 | return cls._instance |
diff --git a/ttun/settings.py b/ttun/settings.py index a20b6c9..9c15f15 100644 --- a/ttun/settings.py +++ b/ttun/settings.py | |||
| @@ -5,12 +5,14 @@ from pathlib import Path | |||
| 5 | 5 | ||
| 6 | import appdirs | 6 | import appdirs |
| 7 | 7 | ||
| 8 | config_file = Path(appdirs.user_config_dir('ttun-client', 'ttun', roaming=True)) / 'config.ini' | 8 | config_file = ( |
| 9 | Path(appdirs.user_config_dir("ttun-client", "ttun", roaming=True)) / "config.ini" | ||
| 10 | ) | ||
| 9 | 11 | ||
| 10 | config= ConfigParser() | 12 | config = ConfigParser() |
| 11 | config['server'] = { | 13 | config["server"] = { |
| 12 | 'hostname': '', | 14 | "hostname": "", |
| 13 | 'using_ssl': 'yes', | 15 | "using_ssl": "yes", |
| 14 | } | 16 | } |
| 15 | 17 | ||
| 16 | if not config_file.parent.exists(): | 18 | if not config_file.parent.exists(): |
| @@ -18,14 +20,14 @@ if not config_file.parent.exists(): | |||
| 18 | else: | 20 | else: |
| 19 | config.read(filenames=config_file) | 21 | config.read(filenames=config_file) |
| 20 | 22 | ||
| 21 | with open(config_file, 'w') as f: | 23 | with open(config_file, "w") as f: |
| 22 | config.write(f) | 24 | config.write(f) |
| 23 | 25 | ||
| 24 | SERVER_HOSTNAME = config['server'].get('hostname') | 26 | SERVER_HOSTNAME = config["server"].get("hostname") |
| 25 | SERVER_USING_SSL = config['server'].get('using_ssl').lower() in ['yes', 'true'] | 27 | SERVER_USING_SSL = config["server"].get("using_ssl").lower() in ["yes", "true"] |
| 26 | 28 | ||
| 27 | try: | 29 | try: |
| 28 | assert SERVER_HOSTNAME != '' | 30 | assert SERVER_HOSTNAME != "" |
| 29 | except AssertionError: | 31 | except AssertionError: |
| 30 | print(f'Please add a valid server hostname in {config_file}') | 32 | print(f"Please add a valid server hostname in {config_file}") |
| 31 | sys.exit(1) | 33 | sys.exit(1) |
diff --git a/ttun/types.py b/ttun/types.py index 640ac5d..3f4d9c9 100644 --- a/ttun/types.py +++ b/ttun/types.py | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | from typing import TypedDict, Optional | 1 | from typing import Optional |
| 2 | from typing import TypedDict | ||
| 2 | 3 | ||
| 3 | 4 | ||
| 4 | class Message(TypedDict): | 5 | class Message(TypedDict): |
| @@ -897,6 +897,11 @@ postcss@^8.4.5: | |||
| 897 | picocolors "^1.0.0" | 897 | picocolors "^1.0.0" |
| 898 | source-map-js "^1.0.1" | 898 | source-map-js "^1.0.1" |
| 899 | 899 | ||
| 900 | prettier@2.5.1: | ||
| 901 | version "2.5.1" | ||
| 902 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" | ||
| 903 | integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== | ||
| 904 | |||
| 900 | promise@^7.1.1: | 905 | promise@^7.1.1: |
| 901 | version "7.3.1" | 906 | version "7.3.1" |
| 902 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" | 907 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" |
