aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2022-02-12 11:33:52 +0100
committerGravatar Tom van der Lee <tom@vanderlee.io>2022-02-12 11:33:52 +0100
commit871a85d9343cf49646d13dc455c062efadcb959a (patch)
tree7d36b14b237719901d71d58c88f74b87b875ee61
parent7c48533571e9f9d3731a59433a56cc8d6e008123 (diff)
downloadclient-871a85d9343cf49646d13dc455c062efadcb959a.tar.gz
client-871a85d9343cf49646d13dc455c062efadcb959a.tar.bz2
client-871a85d9343cf49646d13dc455c062efadcb959a.zip
Beatified codev1.3.0
-rw-r--r--.pre-commit-config.yaml28
-rw-r--r--.prettierignore5
-rw-r--r--dev_requirements.txt (renamed from build_requirements.txt)1
-rw-r--r--requirements.txt2
-rw-r--r--src/components/App/App.tsx189
-rw-r--r--src/components/Content/Content.tsx167
-rw-r--r--src/components/Icons/Filter.tsx18
-rw-r--r--src/components/Icons/Moon.tsx19
-rw-r--r--src/components/Icons/Sliders.tsx21
-rw-r--r--src/components/Icons/Sun.tsx17
-rw-r--r--src/components/Icons/Trash.tsx19
-rw-r--r--src/components/RequestDetails/RequestDetails.tsx159
-rw-r--r--src/components/RequestList/RequestList.module.scss1
-rw-r--r--src/components/RequestList/RequestList.tsx251
-rw-r--r--src/components/RequestSummary/RequestSummary.tsx41
-rw-r--r--src/contexts/DarkMode.tsx30
-rw-r--r--src/hooks/useRequests.tsx145
-rw-r--r--src/index.scss4
-rw-r--r--src/index.tsx9
-rw-r--r--src/utils.ts2
-rw-r--r--tsconfig.json26
-rw-r--r--ttun/__main__.py44
-rw-r--r--ttun/client.py67
-rw-r--r--ttun/inspect_server.py53
-rw-r--r--ttun/pubsub.py8
-rw-r--r--ttun/settings.py22
-rw-r--r--ttun/types.py3
-rw-r--r--yarn.lock5
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 @@
1repos:
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 @@
1ttun/
2ttun.egg-info/
3.github/
4dist/
5node_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 @@
1setuptools 1setuptools
2build 2build
3setuptools-git-versioning 3setuptools-git-versioning
4pre-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
2websockets ~= 10.0 2websockets ~= 10.0
3aiohttp ~= 3.8 3aiohttp ~= 3.8
4appdirs ~= 1.4 4appdirs ~= 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 @@
1import * as React from "react"; 1import * as React from "react";
2import {ReactElement, useContext, useEffect, useMemo, useState} from "react"; 2import { ReactElement, useContext, useEffect, useMemo, useState } from "react";
3import useRequests, { 3import useRequests, {
4 ReadyState, 4 ReadyState,
5 RequestResponse 5 RequestResponse,
6} from "../../hooks/useRequests"; 6} from "../../hooks/useRequests";
7 7
8import styles from './App.module.scss'; 8import styles from "./App.module.scss";
9import RequestDetails from "../RequestDetails/RequestDetails"; 9import RequestDetails from "../RequestDetails/RequestDetails";
10import {getHost} from "../../utils"; 10import { getHost } from "../../utils";
11import {Container, ListGroup, Nav, Navbar, NavDropdown} from "react-bootstrap"; 11import {
12 Container,
13 ListGroup,
14 Nav,
15 Navbar,
16 NavDropdown,
17} from "react-bootstrap";
12import classNames from "classnames"; 18import classNames from "classnames";
13import {Sliders} from "../Icons/Sliders"; 19import { Sliders } from "../Icons/Sliders";
14import {Sun} from "../Icons/Sun"; 20import { Sun } from "../Icons/Sun";
15import {Moon} from "../Icons/Moon"; 21import { Moon } from "../Icons/Moon";
16import Trash from "../Icons/Trash"; 22import Trash from "../Icons/Trash";
17import {DarkModeContext} from "../../contexts/DarkMode"; 23import { DarkModeContext } from "../../contexts/DarkMode";
18import RequestList from "../RequestList/RequestList"; 24import RequestList from "../RequestList/RequestList";
19 25
20interface Config { 26interface Config {
21 url: string 27 url: string;
22} 28}
23 29
24interface SettingsMenu { 30interface SettingsMenu {
25 icon: ReactElement, 31 icon: ReactElement;
26 label: string, 32 label: string;
27 onClick: () => void, 33 onClick: () => void;
28} 34}
29 35
30type ReadyStateMap = { 36type 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
37const statusIconMap: ReadyStateMap = { 43const 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
44const statusTextMap: ReadyStateMap = { 50const 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
51export default function App() { 57export 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 @@
1import * as React from "react"; 1import * as React from "react";
2import {Dispatch, SetStateAction, useContext, useMemo} from "react"; 2import { Dispatch, SetStateAction, useContext, useMemo } from "react";
3import {RequestPayload, ResponsePayload, Headers} from "~hooks/useRequests"; 3import { RequestPayload, ResponsePayload, Headers } from "~hooks/useRequests";
4import ReactJson from 'react-json-view'; 4import ReactJson from "react-json-view";
5import styles from './Content.module.scss'; 5import styles from "./Content.module.scss";
6import {Button, Col, Container, Row} from "react-bootstrap"; 6import { Button, Col, Container, Row } from "react-bootstrap";
7import {DarkModeContext} from "../../contexts/DarkMode"; 7import { DarkModeContext } from "../../contexts/DarkMode";
8 8
9function getHeader(headers: Headers, key: string, unit?: string): string | null { 9function 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
20interface ContentProps { 24interface 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
26export default function Content({ raw, setRaw, data }: ContentProps): JSX.Element { 30export 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
63function 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
82function 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 @@
1import * as React from "react"; 1import * as React from "react";
2 2
3export function Filter() { 3export 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 @@
1import * as React from "react"; 1import * as React from "react";
2 2
3export function Moon() { 3export 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 @@
1import * as React from "react"; 1import * as React from "react";
2 2
3export function Sliders() { 3export 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 @@
1import * as React from "react"; 1import * as React from "react";
2 2
3export function Sun() { 3export 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
3export default function Trash() { 3export 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 @@
1import * as React from "react"; 1import * as React from "react";
2import {useCallback, useMemo, useState} from "react"; 2import { useCallback, useMemo, useState } from "react";
3import {RequestResponse, Headers} from "../../hooks/useRequests"; 3import { RequestResponse, Headers } from "../../hooks/useRequests";
4import styles from "./RequestDetails.module.scss"; 4import styles from "./RequestDetails.module.scss";
5import RequestSummary from "../RequestSummary/RequestSummary"; 5import RequestSummary from "../RequestSummary/RequestSummary";
6import Content from "../Content/Content"; 6import Content from "../Content/Content";
7import {getHost} from "../../utils"; 7import { getHost } from "../../utils";
8import {Button, Card, Col, Container, Nav, Row, Table} from "react-bootstrap"; 8import { Button, Card, Col, Container, Nav, Row, Table } from "react-bootstrap";
9
10 9
11interface TimingProps { 10interface TimingProps {
12 timing: number; 11 timing: number;
13} 12}
14 13
15
16function Timing({ timing }: TimingProps) { 14function 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
29interface HeadersProps { 23interface HeadersProps {
30 title: string 24 title: string;
31 headers: Headers 25 headers: Headers;
32} 26}
33 27
34function Headers({ title, headers }: HeadersProps) { 28function 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
64interface DetailsProps { 52interface DetailsProps {
65 requestResponse: RequestResponse | null 53 requestResponse: RequestResponse | null;
66} 54}
67 55
68type Tab = 'headers' | 'request' | 'response' 56type Tab = "headers" | "request" | "response";
69 57
70export default function RequestDetails({ requestResponse }: DetailsProps) { 58export 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";
11import styles from "./RequestList.module.scss"; 11import styles from "./RequestList.module.scss";
12import RequestSummary from "../RequestSummary/RequestSummary"; 12import RequestSummary from "../RequestSummary/RequestSummary";
13import * as React from "react"; 13import * as React from "react";
14import {useCallback, useContext, useMemo, useState} from "react"; 14import { useCallback, useContext, useMemo, useState } from "react";
15import {Method, RequestResponse} from "../../hooks/useRequests"; 15import { Method, RequestResponse } from "../../hooks/useRequests";
16import {DarkModeContext} from "../../contexts/DarkMode"; 16import { DarkModeContext } from "../../contexts/DarkMode";
17import {Filter} from "../Icons/Filter"; 17import { Filter } from "../Icons/Filter";
18 18
19interface ListProps { 19interface 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
25type EnabledMethods = { 25type EnabledMethods = {
26 [method in Method]: boolean 26 [method in Method]: boolean;
27} 27};
28
29 28
30export default function RequestList({ requests, selectedRequestIndex, setSelectedRequestIndex }: ListProps) { 29export 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 @@
1import {RequestResponse} from "~hooks/useRequests"; 1import { RequestResponse } from "~hooks/useRequests";
2import * as React from "react"; 2import * as React from "react";
3import classNames from "classnames"; 3import classNames from "classnames";
4 4
5import {Badge, Card, Col, Row} from "react-bootstrap"; 5import { Badge, Card, Col, Row } from "react-bootstrap";
6 6
7interface RequestSummaryProps { 7interface RequestSummaryProps {
8 selected?: boolean 8 selected?: boolean;
9 requestResponse: RequestResponse 9 requestResponse: RequestResponse;
10} 10}
11 11
12function isBetween(value: number, min: number, max: number) { 12function isBetween(value: number, min: number, max: number) {
13 return value >= min && value <= max 13 return value >= min && value <= max;
14} 14}
15 15
16function calcBadgeVariant(statusCode: number | undefined): string { 16function 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
32export default function RequestSummary({ requestResponse: { request, response }, selected = false }: RequestSummaryProps) { 32export 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";
10import {ThemeConfig} from "bootstrap-darkmode"; 11import { ThemeConfig } from "bootstrap-darkmode";
11 12
12interface DarkModeApi { 13interface DarkModeApi {
13 darkMode: boolean 14 darkMode: boolean;
14 toggle: () => void 15 toggle: () => void;
15} 16}
16 17
17export const DarkModeContext = createContext<Partial<DarkModeApi>>({}) as Context<DarkModeApi>; 18export const DarkModeContext = createContext<Partial<DarkModeApi>>(
19 {}
20) as Context<DarkModeApi>;
18interface DarkModeProviderProps { 21interface DarkModeProviderProps {
19 children: ReactNode 22 children: ReactNode;
20} 23}
21 24
22export default function DarkModeProvider({ children }: DarkModeProviderProps) { 25export 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 @@
1import {useCallback, useEffect, useMemo, useState} from "react"; 1import { useCallback, useEffect, useMemo, useState } from "react";
2import {getHost} from "../utils"; 2import { getHost } from "../utils";
3 3
4export type Headers = [string, string][] 4export type Headers = [string, string][];
5export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' 5export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
6 6
7export interface RequestPayload { 7export 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
15interface Request { 15interface Request {
16 type: 'request', 16 type: "request";
17 payload: RequestPayload, 17 payload: RequestPayload;
18} 18}
19 19
20export interface ResponsePayload { 20export 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
28interface Response { 28interface Response {
29 type: 'response', 29 type: "response";
30 payload: ResponsePayload, 30 payload: ResponsePayload;
31} 31}
32 32
33interface Historic { 33interface Historic {
34 type: 'historic' 34 type: "historic";
35 payload: (Request | Response)[] 35 payload: (Request | Response)[];
36} 36}
37 37
38export interface RequestResponse { 38export interface RequestResponse {
39 request: RequestPayload 39 request: RequestPayload;
40 response?: ResponsePayload 40 response?: ResponsePayload;
41} 41}
42 42
43export enum ReadyState { 43export enum ReadyState {
@@ -48,26 +48,28 @@ export enum ReadyState {
48} 48}
49 49
50export interface useRequestsProps { 50export interface useRequestsProps {
51 onConnect: () => Promise<void> 51 onConnect: () => Promise<void>;
52} 52}
53 53
54export interface UseRequests { 54export interface UseRequests {
55 calls: RequestResponse[] 55 calls: RequestResponse[];
56 readyState: ReadyState 56 readyState: ReadyState;
57 clear: () => void 57 clear: () => void;
58} 58}
59 59
60export default function useRequests({ onConnect }: useRequestsProps): UseRequests { 60export 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
9html, body, #root { 9html,
10body,
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";
2import * as ReactDOM from "react-dom"; 2import * as ReactDOM from "react-dom";
3import App from "./components/App/App"; 3import App from "./components/App/App";
4 4
5import './index.scss'; 5import "./index.scss";
6import DarkModeProvider from "./contexts/DarkMode"; 6import DarkModeProvider from "./contexts/DarkMode";
7 7
8ReactDOM.render(( 8ReactDOM.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 @@
1export function getHost() { 1export 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 @@
1import asyncio 1import asyncio
2from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 2from argparse import ArgumentDefaultsHelpFormatter
3from argparse import ArgumentParser
3from asyncio import FIRST_EXCEPTION 4from asyncio import FIRST_EXCEPTION
5from asyncio.exceptions import CancelledError
6from asyncio.exceptions import TimeoutError
4 7
5from ttun.client import Client 8from ttun.client import Client
6from asyncio.exceptions import TimeoutError, CancelledError
7
8from ttun.inspect_server import Server 9from ttun.inspect_server import Server
9from ttun.settings import SERVER_HOSTNAME, SERVER_USING_SSL 10from ttun.settings import SERVER_HOSTNAME
11from ttun.settings import SERVER_USING_SSL
10 12
11inspect_queue = asyncio.Queue() 13inspect_queue = asyncio.Queue()
12 14
15
13def main(): 16def 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
73if __name__ == '__main__': 70
71if __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 @@
1import asyncio 1import asyncio
2import json 2import json
3from base64 import b64encode, b64decode 3from base64 import b64decode
4from base64 import b64encode
4from time import perf_counter 5from time import perf_counter
5from typing import Optional, Callable, Coroutine, Awaitable 6from typing import Awaitable
7from typing import Callable
8from typing import Coroutine
9from typing import Optional
6from uuid import uuid4 10from uuid import uuid4
7 11
8import websockets 12import websockets
9from aiohttp import ClientSession, DummyCookieJar 13from aiohttp import ClientSession
14from aiohttp import DummyCookieJar
10from websockets import WebSocketClientProtocol 15from websockets import WebSocketClientProtocol
11from websockets.exceptions import ConnectionClosed 16from websockets.exceptions import ConnectionClosed
12 17
13from ttun.pubsub import PubSub 18from ttun.pubsub import PubSub
14from ttun.types import Config, RequestData, ResponseData 19from ttun.types import Config
20from ttun.types import RequestData
21from ttun.types import ResponseData
15 22
16 23
17class Client: 24class 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 @@
1from importlib import resources 1from importlib import resources
2from pathlib import Path 2from pathlib import Path
3from typing import Callable, Awaitable, Optional 3from typing import Awaitable
4from typing import Callable
5from typing import Optional
4 6
5from aiohttp import web 7from aiohttp import web
6 8
7from ttun.pubsub import PubSub 9from ttun.pubsub import PubSub
8from ttun.types import Config, RequestData 10from ttun.types import Config
11from ttun.types import RequestData
9 12
10BASE_DIR = Path(__file__).resolve().parent 13BASE_DIR = Path(__file__).resolve().parent
11 14
12 15
13class Server: 16class 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 @@
1import asyncio 1import asyncio
2from contextlib import contextmanager 2from contextlib import contextmanager
3from typing import Any, Generator, Iterator 3from typing import Any
4from typing import Generator
5from typing import Iterator
4 6
5 7
6class PubSub: 8class 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
6import appdirs 6import appdirs
7 7
8config_file = Path(appdirs.user_config_dir('ttun-client', 'ttun', roaming=True)) / 'config.ini' 8config_file = (
9 Path(appdirs.user_config_dir("ttun-client", "ttun", roaming=True)) / "config.ini"
10)
9 11
10config= ConfigParser() 12config = ConfigParser()
11config['server'] = { 13config["server"] = {
12 'hostname': '', 14 "hostname": "",
13 'using_ssl': 'yes', 15 "using_ssl": "yes",
14} 16}
15 17
16if not config_file.parent.exists(): 18if not config_file.parent.exists():
@@ -18,14 +20,14 @@ if not config_file.parent.exists():
18else: 20else:
19 config.read(filenames=config_file) 21 config.read(filenames=config_file)
20 22
21with open(config_file, 'w') as f: 23with open(config_file, "w") as f:
22 config.write(f) 24 config.write(f)
23 25
24SERVER_HOSTNAME = config['server'].get('hostname') 26SERVER_HOSTNAME = config["server"].get("hostname")
25SERVER_USING_SSL = config['server'].get('using_ssl').lower() in ['yes', 'true'] 27SERVER_USING_SSL = config["server"].get("using_ssl").lower() in ["yes", "true"]
26 28
27try: 29try:
28 assert SERVER_HOSTNAME != '' 30 assert SERVER_HOSTNAME != ""
29except AssertionError: 31except 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 @@
1from typing import TypedDict, Optional 1from typing import Optional
2from typing import TypedDict
2 3
3 4
4class Message(TypedDict): 5class Message(TypedDict):
diff --git a/yarn.lock b/yarn.lock
index 315c7a8..a65d0d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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
900prettier@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
900promise@^7.1.1: 905promise@^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"