aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
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 /src/components
parent7c48533571e9f9d3731a59433a56cc8d6e008123 (diff)
downloadclient-1.3.0.tar.gz
client-1.3.0.tar.bz2
client-1.3.0.zip
Beatified codev1.3.0
Diffstat (limited to 'src/components')
-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
11 files changed, 487 insertions, 415 deletions
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}