diff options
| author | 2022-02-12 11:33:52 +0100 | |
|---|---|---|
| committer | 2022-02-12 11:33:52 +0100 | |
| commit | 871a85d9343cf49646d13dc455c062efadcb959a (patch) | |
| tree | 7d36b14b237719901d71d58c88f74b87b875ee61 /src/components | |
| parent | 7c48533571e9f9d3731a59433a56cc8d6e008123 (diff) | |
| download | client-871a85d9343cf49646d13dc455c062efadcb959a.tar.gz client-871a85d9343cf49646d13dc455c062efadcb959a.tar.bz2 client-871a85d9343cf49646d13dc455c062efadcb959a.zip | |
Beatified codev1.3.0
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/App/App.tsx | 189 | ||||
| -rw-r--r-- | src/components/Content/Content.tsx | 167 | ||||
| -rw-r--r-- | src/components/Icons/Filter.tsx | 18 | ||||
| -rw-r--r-- | src/components/Icons/Moon.tsx | 19 | ||||
| -rw-r--r-- | src/components/Icons/Sliders.tsx | 21 | ||||
| -rw-r--r-- | src/components/Icons/Sun.tsx | 17 | ||||
| -rw-r--r-- | src/components/Icons/Trash.tsx | 19 | ||||
| -rw-r--r-- | src/components/RequestDetails/RequestDetails.tsx | 159 | ||||
| -rw-r--r-- | src/components/RequestList/RequestList.module.scss | 1 | ||||
| -rw-r--r-- | src/components/RequestList/RequestList.tsx | 251 | ||||
| -rw-r--r-- | src/components/RequestSummary/RequestSummary.tsx | 41 |
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 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import {ReactElement, useContext, useEffect, useMemo, useState} from "react"; | 2 | import { ReactElement, useContext, useEffect, useMemo, useState } from "react"; |
| 3 | import useRequests, { | 3 | import useRequests, { |
| 4 | ReadyState, | 4 | ReadyState, |
| 5 | RequestResponse | 5 | RequestResponse, |
| 6 | } from "../../hooks/useRequests"; | 6 | } from "../../hooks/useRequests"; |
| 7 | 7 | ||
| 8 | import styles from './App.module.scss'; | 8 | import styles from "./App.module.scss"; |
| 9 | import RequestDetails from "../RequestDetails/RequestDetails"; | 9 | import RequestDetails from "../RequestDetails/RequestDetails"; |
| 10 | import {getHost} from "../../utils"; | 10 | import { getHost } from "../../utils"; |
| 11 | import {Container, ListGroup, Nav, Navbar, NavDropdown} from "react-bootstrap"; | 11 | import { |
| 12 | Container, | ||
| 13 | ListGroup, | ||
| 14 | Nav, | ||
| 15 | Navbar, | ||
| 16 | NavDropdown, | ||
| 17 | } from "react-bootstrap"; | ||
| 12 | import classNames from "classnames"; | 18 | import classNames from "classnames"; |
| 13 | import {Sliders} from "../Icons/Sliders"; | 19 | import { Sliders } from "../Icons/Sliders"; |
| 14 | import {Sun} from "../Icons/Sun"; | 20 | import { Sun } from "../Icons/Sun"; |
| 15 | import {Moon} from "../Icons/Moon"; | 21 | import { Moon } from "../Icons/Moon"; |
| 16 | import Trash from "../Icons/Trash"; | 22 | import Trash from "../Icons/Trash"; |
| 17 | import {DarkModeContext} from "../../contexts/DarkMode"; | 23 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 18 | import RequestList from "../RequestList/RequestList"; | 24 | import RequestList from "../RequestList/RequestList"; |
| 19 | 25 | ||
| 20 | interface Config { | 26 | interface Config { |
| 21 | url: string | 27 | url: string; |
| 22 | } | 28 | } |
| 23 | 29 | ||
| 24 | interface SettingsMenu { | 30 | interface SettingsMenu { |
| 25 | icon: ReactElement, | 31 | icon: ReactElement; |
| 26 | label: string, | 32 | label: string; |
| 27 | onClick: () => void, | 33 | onClick: () => void; |
| 28 | } | 34 | } |
| 29 | 35 | ||
| 30 | type ReadyStateMap = { | 36 | type ReadyStateMap = { |
| 31 | [ReadyState.CONNECTING]: string, | 37 | [ReadyState.CONNECTING]: string; |
| 32 | [ReadyState.OPEN]: string, | 38 | [ReadyState.OPEN]: string; |
| 33 | [ReadyState.CLOSING]: string, | 39 | [ReadyState.CLOSING]: string; |
| 34 | [ReadyState.CLOSED]: string, | 40 | [ReadyState.CLOSED]: string; |
| 35 | } | 41 | }; |
| 36 | 42 | ||
| 37 | const statusIconMap: ReadyStateMap = { | 43 | const statusIconMap: ReadyStateMap = { |
| 38 | [ReadyState.CONNECTING]: '🔴', | 44 | [ReadyState.CONNECTING]: "🔴", |
| 39 | [ReadyState.OPEN]: '🟢', | 45 | [ReadyState.OPEN]: "🟢", |
| 40 | [ReadyState.CLOSING]: '🔴', | 46 | [ReadyState.CLOSING]: "🔴", |
| 41 | [ReadyState.CLOSED]: '🔴', | 47 | [ReadyState.CLOSED]: "🔴", |
| 42 | } | 48 | }; |
| 43 | 49 | ||
| 44 | const statusTextMap: ReadyStateMap = { | 50 | const statusTextMap: ReadyStateMap = { |
| 45 | [ReadyState.CONNECTING]: 'Connecting...', | 51 | [ReadyState.CONNECTING]: "Connecting...", |
| 46 | [ReadyState.OPEN]: 'Connected', | 52 | [ReadyState.OPEN]: "Connected", |
| 47 | [ReadyState.CLOSING]: 'Closing...', | 53 | [ReadyState.CLOSING]: "Closing...", |
| 48 | [ReadyState.CLOSED]: 'Closed', | 54 | [ReadyState.CLOSED]: "Closed", |
| 49 | } | 55 | }; |
| 50 | 56 | ||
| 51 | export default function App() { | 57 | export default function App() { |
| 52 | const { darkMode, toggle } = useContext(DarkModeContext); | 58 | const { darkMode, toggle } = useContext(DarkModeContext); |
| 53 | const [config, setConfig]= useState<Config | null>(null) | 59 | const [config, setConfig] = useState<Config | null>(null); |
| 54 | 60 | ||
| 55 | const { calls, readyState, clear } = useRequests({ | 61 | const { calls, readyState, clear } = useRequests({ |
| 56 | onConnect: async () => { | 62 | onConnect: async () => { |
| 57 | const response = await fetch(`http://${getHost()}/config/`) | 63 | const response = await fetch(`http://${getHost()}/config/`); |
| 58 | const config = await response.json() | 64 | const config = await response.json(); |
| 59 | setConfig(config) | 65 | setConfig(config); |
| 60 | } | 66 | }, |
| 61 | }); | 67 | }); |
| 62 | 68 | ||
| 63 | useEffect(() => { | 69 | useEffect(() => { |
| 64 | const url = new URL(config?.url ?? 'https://loading...'); | 70 | const url = new URL(config?.url ?? "https://loading..."); |
| 65 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; | 71 | document.title = `${statusIconMap[readyState]} ${url.host} | TTUN`; |
| 66 | }, [readyState, config?.url]) | 72 | }, [readyState, config?.url]); |
| 67 | 73 | ||
| 68 | const [selectedRequestIndex, setSelectedRequestIndex] = useState<number | null>(null); | 74 | const [selectedRequestIndex, setSelectedRequestIndex] = useState< |
| 69 | const selectedRequest = useMemo<RequestResponse | null>(() => ( | 75 | number | null |
| 70 | selectedRequestIndex === null | 76 | >(null); |
| 71 | ? null | 77 | const selectedRequest = useMemo<RequestResponse | null>( |
| 72 | : calls[selectedRequestIndex] | 78 | () => (selectedRequestIndex === null ? null : calls[selectedRequestIndex]), |
| 73 | ), [selectedRequestIndex, calls]); | 79 | [selectedRequestIndex, calls] |
| 80 | ); | ||
| 74 | 81 | ||
| 75 | const settingsMenu: (SettingsMenu | null)[] = [ | 82 | const settingsMenu: (SettingsMenu | null)[] = [ |
| 76 | { | 83 | { |
| 77 | onClick: toggle, | 84 | onClick: toggle, |
| 78 | icon: darkMode ? <Sun />: <Moon />, | 85 | icon: darkMode ? <Sun /> : <Moon />, |
| 79 | label: darkMode ? "Light mode" : "Dark mode", | 86 | label: darkMode ? "Light mode" : "Dark mode", |
| 80 | }, | 87 | }, |
| 81 | null, | 88 | null, |
| @@ -85,40 +92,32 @@ export default function App() { | |||
| 85 | clear(); | 92 | clear(); |
| 86 | }, | 93 | }, |
| 87 | icon: <Trash />, | 94 | icon: <Trash />, |
| 88 | label: "Clear" | 95 | label: "Clear", |
| 89 | } | 96 | }, |
| 90 | ]; | 97 | ]; |
| 91 | 98 | ||
| 92 | return config && ( | 99 | return ( |
| 93 | <div className={styles.app}> | 100 | config && ( |
| 94 | <Navbar | 101 | <div className={styles.app}> |
| 95 | bg="dark" | 102 | <Navbar bg="dark" variant="dark" expand as="header"> |
| 96 | variant="dark" | 103 | <Container fluid> |
| 97 | expand | 104 | <div> |
| 98 | as="header" | 105 | <Navbar.Brand>TTUN</Navbar.Brand> |
| 99 | > | 106 | <Navbar.Text> |
| 100 | <Container fluid> | 107 | {`${statusIconMap[readyState]} ${statusTextMap[readyState]}`} |
| 101 | <div> | 108 | </Navbar.Text> |
| 102 | <Navbar.Brand> | 109 | </div> |
| 103 | TTUN | 110 | <div className="d-flex"> |
| 104 | </Navbar.Brand> | 111 | <Navbar.Text> |
| 105 | <Navbar.Text> | 112 | <a href={config.url} target="_blank"> |
| 106 | {`${statusIconMap[readyState]} ${statusTextMap[readyState]}`} | 113 | {config.url} |
| 107 | </Navbar.Text> | 114 | </a> |
| 108 | </div> | 115 | </Navbar.Text> |
| 109 | <div className="d-flex"> | 116 | <Navbar.Toggle aria-controls="settings" /> |
| 110 | <Navbar.Text> | 117 | <Navbar.Collapse id="settings" className="ms-2"> |
| 111 | <a href={config.url} target="_blank">{config.url}</a> | 118 | <Nav> |
| 112 | </Navbar.Text> | 119 | <NavDropdown align="end" title={<Sliders />}> |
| 113 | <Navbar.Toggle aria-controls="settings"/> | 120 | {settingsMenu.map((item) => { |
| 114 | <Navbar.Collapse id="settings" className="ms-2"> | ||
| 115 | <Nav> | ||
| 116 | <NavDropdown | ||
| 117 | align="end" | ||
| 118 | title={<Sliders/>} | ||
| 119 | > | ||
| 120 | { | ||
| 121 | settingsMenu.map((item) => { | ||
| 122 | if (item !== null) { | 121 | if (item !== null) { |
| 123 | const { onClick, icon, label } = item; | 122 | const { onClick, icon, label } = item; |
| 124 | return ( | 123 | return ( |
| @@ -129,27 +128,31 @@ export default function App() { | |||
| 129 | {icon} | 128 | {icon} |
| 130 | <span className="ms-3">{label}</span> | 129 | <span className="ms-3">{label}</span> |
| 131 | </NavDropdown.Item> | 130 | </NavDropdown.Item> |
| 132 | ) | 131 | ); |
| 133 | } else { | 132 | } else { |
| 134 | return <NavDropdown.Divider /> | 133 | return <NavDropdown.Divider />; |
| 135 | } | 134 | } |
| 136 | }) | 135 | })} |
| 137 | } | 136 | </NavDropdown> |
| 138 | </NavDropdown> | 137 | </Nav> |
| 139 | </Nav> | 138 | </Navbar.Collapse> |
| 140 | </Navbar.Collapse> | 139 | </div> |
| 141 | </div> | 140 | </Container> |
| 142 | </Container> | 141 | </Navbar> |
| 143 | </Navbar> | ||
| 144 | 142 | ||
| 145 | <main className={styles.main}> | 143 | <main className={styles.main}> |
| 146 | <div className={classNames("border-end", styles.sidebar)}> | 144 | <div className={classNames("border-end", styles.sidebar)}> |
| 147 | <RequestList requests={calls} selectedRequestIndex={selectedRequestIndex} setSelectedRequestIndex={setSelectedRequestIndex}/> | 145 | <RequestList |
| 148 | </div> | 146 | requests={calls} |
| 149 | <div className={styles.details}> | 147 | selectedRequestIndex={selectedRequestIndex} |
| 150 | <RequestDetails requestResponse={selectedRequest} /> | 148 | setSelectedRequestIndex={setSelectedRequestIndex} |
| 151 | </div> | 149 | /> |
| 152 | </main> | 150 | </div> |
| 153 | </div> | 151 | <div className={styles.details}> |
| 152 | <RequestDetails requestResponse={selectedRequest} /> | ||
| 153 | </div> | ||
| 154 | </main> | ||
| 155 | </div> | ||
| 156 | ) | ||
| 154 | ); | 157 | ); |
| 155 | } | 158 | } |
diff --git a/src/components/Content/Content.tsx b/src/components/Content/Content.tsx index c7a5f58..56a802f 100644 --- a/src/components/Content/Content.tsx +++ b/src/components/Content/Content.tsx | |||
| @@ -1,120 +1,141 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import {Dispatch, SetStateAction, useContext, useMemo} from "react"; | 2 | import { Dispatch, SetStateAction, useContext, useMemo } from "react"; |
| 3 | import {RequestPayload, ResponsePayload, Headers} from "~hooks/useRequests"; | 3 | import { RequestPayload, ResponsePayload, Headers } from "~hooks/useRequests"; |
| 4 | import ReactJson from 'react-json-view'; | 4 | import ReactJson from "react-json-view"; |
| 5 | import styles from './Content.module.scss'; | 5 | import styles from "./Content.module.scss"; |
| 6 | import {Button, Col, Container, Row} from "react-bootstrap"; | 6 | import { Button, Col, Container, Row } from "react-bootstrap"; |
| 7 | import {DarkModeContext} from "../../contexts/DarkMode"; | 7 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 8 | 8 | ||
| 9 | function getHeader(headers: Headers, key: string, unit?: string): string | null { | 9 | function getHeader( |
| 10 | headers: Headers, | ||
| 11 | key: string, | ||
| 12 | unit?: string | ||
| 13 | ): string | null { | ||
| 10 | try { | 14 | try { |
| 11 | const [_, value] = headers.find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase()) | 15 | const [_, value] = headers.find( |
| 12 | return unit !== undefined | 16 | ([headerKey]) => headerKey.toLowerCase() === key.toLowerCase() |
| 13 | ? `${value}${unit}` | 17 | ); |
| 14 | : value | 18 | return unit !== undefined ? `${value}${unit}` : value; |
| 15 | } catch { | 19 | } catch { |
| 16 | return null; | 20 | return null; |
| 17 | } | 21 | } |
| 18 | } | 22 | } |
| 19 | 23 | ||
| 20 | interface ContentProps { | 24 | interface ContentProps { |
| 21 | data: RequestPayload | ResponsePayload | 25 | data: RequestPayload | ResponsePayload; |
| 22 | setRaw: Dispatch<SetStateAction<boolean>> | 26 | setRaw: Dispatch<SetStateAction<boolean>>; |
| 23 | raw?: boolean | 27 | raw?: boolean; |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 26 | export default function Content({ raw, setRaw, data }: ContentProps): JSX.Element { | 30 | export default function Content({ |
| 31 | raw, | ||
| 32 | setRaw, | ||
| 33 | data, | ||
| 34 | }: ContentProps): JSX.Element { | ||
| 27 | return ( | 35 | return ( |
| 28 | <div className={styles.content}> | 36 | <div className={styles.content}> |
| 29 | <Container fluid className="border-bottom"> | 37 | <Container fluid className="border-bottom"> |
| 30 | <Row className="py-3"> | 38 | <Row className="py-3"> |
| 31 | <Col className="form-check form-switch ms-3"> | 39 | <Col className="form-check form-switch ms-3"> |
| 32 | <input className="form-check-input" id='raw' type='checkbox' checked={raw} role="switch" onChange={() => setRaw(!raw)}/> | 40 | <input |
| 33 | <label htmlFor='raw' className="form-check-label">Raw</label> | 41 | className="form-check-input" |
| 34 | </Col> | 42 | id="raw" |
| 43 | type="checkbox" | ||
| 44 | checked={raw} | ||
| 45 | role="switch" | ||
| 46 | onChange={() => setRaw(!raw)} | ||
| 47 | /> | ||
| 48 | <label htmlFor="raw" className="form-check-label"> | ||
| 49 | Raw | ||
| 50 | </label> | ||
| 51 | </Col> | ||
| 35 | <Col xs="auto"> | 52 | <Col xs="auto"> |
| 36 | { | 53 | {[ |
| 37 | [ | 54 | getHeader(data.headers, "content-length", "bytes"), |
| 38 | getHeader(data.headers, 'content-length', 'bytes'), | 55 | getHeader(data.headers, "content-type"), |
| 39 | getHeader(data.headers, 'content-type'), | 56 | ] |
| 40 | ].filter(x => x !== null).join('; ') | 57 | .filter((x) => x !== null) |
| 41 | } | 58 | .join("; ")} |
| 42 | </Col> | 59 | </Col> |
| 43 | </Row> | 60 | </Row> |
| 44 | </Container> | 61 | </Container> |
| 45 | <Row className={styles.body}> | 62 | <Row className={styles.body}> |
| 46 | {(() => { | 63 | {(() => { |
| 47 | try { | 64 | try { |
| 48 | return ContentBody({ data, raw }) | 65 | return ContentBody({ data, raw }); |
| 49 | } catch { | 66 | } catch { |
| 50 | return ( | 67 | return ( |
| 51 | <div className={styles.renderError}> | 68 | <div className={styles.renderError}> |
| 52 | <p>Body could not be rendered</p> | 69 | <p>Body could not be rendered</p> |
| 53 | <Button variant="link" onClick={() => setRaw(true)}>View raw</Button> | 70 | <Button variant="link" onClick={() => setRaw(true)}> |
| 71 | View raw | ||
| 72 | </Button> | ||
| 54 | </div> | 73 | </div> |
| 55 | ) | 74 | ); |
| 56 | } | 75 | } |
| 57 | })()} | 76 | })()} |
| 58 | </Row> | 77 | </Row> |
| 59 | </div > | 78 | </div> |
| 60 | ) | 79 | ); |
| 61 | }; | 80 | } |
| 62 | |||
| 63 | function ContentBody({ data, raw = false }: Omit<ContentProps, 'setRaw'>) { | ||
| 64 | const { darkMode } = useContext(DarkModeContext); | ||
| 65 | const contentType = useMemo(() => { | ||
| 66 | if (raw) { | ||
| 67 | return ''; | ||
| 68 | } | ||
| 69 | |||
| 70 | const type = getHeader(data.headers, 'content-type'); | ||
| 71 | return type.toLowerCase().split(';')[0]; | ||
| 72 | }, [data, raw]); | ||
| 73 | 81 | ||
| 82 | function ContentBody({ data, raw = false }: Omit<ContentProps, "setRaw">) { | ||
| 83 | const { darkMode } = useContext(DarkModeContext); | ||
| 84 | const contentType = useMemo(() => { | ||
| 74 | if (raw) { | 85 | if (raw) { |
| 75 | return ( | 86 | return ""; |
| 76 | <pre className="mb-0"> | ||
| 77 | <code> | ||
| 78 | {atob(data.body)} | ||
| 79 | </code> | ||
| 80 | </pre> | ||
| 81 | ) | ||
| 82 | } | 87 | } |
| 83 | 88 | ||
| 84 | if (['application/pdf', 'text/html'].includes(contentType)) { | 89 | const type = getHeader(data.headers, "content-type"); |
| 85 | return <iframe | 90 | return type.toLowerCase().split(";")[0]; |
| 91 | }, [data, raw]); | ||
| 92 | |||
| 93 | if (raw) { | ||
| 94 | return ( | ||
| 95 | <pre className="mb-0"> | ||
| 96 | <code>{atob(data.body)}</code> | ||
| 97 | </pre> | ||
| 98 | ); | ||
| 99 | } | ||
| 100 | |||
| 101 | if (["application/pdf", "text/html"].includes(contentType)) { | ||
| 102 | return ( | ||
| 103 | <iframe | ||
| 86 | className="bg-white" | 104 | className="bg-white" |
| 87 | src={`data:${contentType};base64,${data.body}`} | 105 | src={`data:${contentType};base64,${data.body}`} |
| 88 | srcDoc={contentType === 'text/html' ? atob(data.body) : undefined} | 106 | srcDoc={contentType === "text/html" ? atob(data.body) : undefined} |
| 89 | loading='lazy' | 107 | loading="lazy" |
| 90 | sandbox='' | 108 | sandbox="" |
| 91 | /> | 109 | /> |
| 92 | } | 110 | ); |
| 111 | } | ||
| 93 | 112 | ||
| 94 | if (contentType.startsWith('application/json')) { | 113 | if (contentType.startsWith("application/json")) { |
| 95 | return <ReactJson | 114 | return ( |
| 115 | <ReactJson | ||
| 96 | src={JSON.parse(atob(data.body))} | 116 | src={JSON.parse(atob(data.body))} |
| 97 | theme={darkMode ? "monokai" : undefined} | 117 | theme={darkMode ? "monokai" : undefined} |
| 98 | style={{ | 118 | style={{ |
| 99 | padding: '1em', | 119 | padding: "1em", |
| 100 | width: '100%', | 120 | width: "100%", |
| 101 | height: '100%', | 121 | height: "100%", |
| 102 | overflowY: 'auto', | 122 | overflowY: "auto", |
| 103 | }} | 123 | }} |
| 104 | /> | 124 | /> |
| 105 | } | 125 | ); |
| 126 | } | ||
| 106 | 127 | ||
| 107 | if (contentType.startsWith('audio')) { | 128 | if (contentType.startsWith("audio")) { |
| 108 | return <audio src={`data:${contentType};base64,${data.body}`} /> | 129 | return <audio src={`data:${contentType};base64,${data.body}`} />; |
| 109 | } | 130 | } |
| 110 | 131 | ||
| 111 | if (contentType.startsWith('video')) { | 132 | if (contentType.startsWith("video")) { |
| 112 | return <video src={`data:${contentType};base64,${data.body}`} /> | 133 | return <video src={`data:${contentType};base64,${data.body}`} />; |
| 113 | } | 134 | } |
| 114 | 135 | ||
| 115 | if (contentType.startsWith('image')) { | 136 | if (contentType.startsWith("image")) { |
| 116 | return <img src={`data:${contentType};base64,${data.body}`} alt=''/> | 137 | return <img src={`data:${contentType};base64,${data.body}`} alt="" />; |
| 117 | } | 138 | } |
| 118 | 139 | ||
| 119 | throw new Error('Not Rendered'); | 140 | throw new Error("Not Rendered"); |
| 120 | }; | 141 | } |
diff --git a/src/components/Icons/Filter.tsx b/src/components/Icons/Filter.tsx index a14183d..07796df 100644 --- a/src/components/Icons/Filter.tsx +++ b/src/components/Icons/Filter.tsx | |||
| @@ -1,10 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Filter() { | 3 | export function Filter() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" | 4 | return ( |
| 5 | height="16" fill="currentColor" className="bi bi-filter" | 5 | <svg |
| 6 | viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | <path | 7 | width="16" |
| 8 | d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/> | 8 | height="16" |
| 9 | </svg>; | 9 | fill="currentColor" |
| 10 | className="bi bi-filter" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 10 | } | 16 | } |
diff --git a/src/components/Icons/Moon.tsx b/src/components/Icons/Moon.tsx index 7e3e680..303da04 100644 --- a/src/components/Icons/Moon.tsx +++ b/src/components/Icons/Moon.tsx | |||
| @@ -1,11 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Moon() { | 3 | export function Moon() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" | 4 | return ( |
| 5 | width="16" height="16" fill="currentColor" | 5 | <svg |
| 6 | className="bi bi-moon" | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | viewBox="0 0 16 16"> | 7 | width="16" |
| 8 | <path | 8 | height="16" |
| 9 | d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"/> | 9 | fill="currentColor" |
| 10 | </svg>; | 10 | className="bi bi-moon" |
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 11 | } | 16 | } |
diff --git a/src/components/Icons/Sliders.tsx b/src/components/Icons/Sliders.tsx index 9feef34..36c632f 100644 --- a/src/components/Icons/Sliders.tsx +++ b/src/components/Icons/Sliders.tsx | |||
| @@ -1,10 +1,19 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Sliders() { | 3 | export function Sliders() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" | 4 | return ( |
| 5 | height="16" fill="currentColor" | 5 | <svg |
| 6 | className="bi bi-sliders" viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | <path fill-rule="evenodd" | 7 | width="16" |
| 8 | d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z"/> | 8 | height="16" |
| 9 | </svg>; | 9 | fill="currentColor" |
| 10 | className="bi bi-sliders" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path | ||
| 14 | fill-rule="evenodd" | ||
| 15 | d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z" | ||
| 16 | /> | ||
| 17 | </svg> | ||
| 18 | ); | ||
| 10 | } | 19 | } |
diff --git a/src/components/Icons/Sun.tsx b/src/components/Icons/Sun.tsx index 93a5187..f1e37a6 100644 --- a/src/components/Icons/Sun.tsx +++ b/src/components/Icons/Sun.tsx | |||
| @@ -1,9 +1,16 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | 2 | ||
| 3 | export function Sun() { | 3 | export function Sun() { |
| 4 | return <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" | 4 | return ( |
| 5 | fill="currentColor" className="bi bi-sun" viewBox="0 0 16 16"> | 5 | <svg |
| 6 | <path | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/> | 7 | width="16" |
| 8 | </svg>; | 8 | height="16" |
| 9 | fill="currentColor" | ||
| 10 | className="bi bi-sun" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" /> | ||
| 14 | </svg> | ||
| 15 | ); | ||
| 9 | } | 16 | } |
diff --git a/src/components/Icons/Trash.tsx b/src/components/Icons/Trash.tsx index b1fb2c3..6c0039b 100644 --- a/src/components/Icons/Trash.tsx +++ b/src/components/Icons/Trash.tsx | |||
| @@ -2,12 +2,19 @@ import * as React from "react"; | |||
| 2 | 2 | ||
| 3 | export default function Trash() { | 3 | export default function Trash() { |
| 4 | return ( | 4 | return ( |
| 5 | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" | 5 | <svg |
| 6 | fill="currentColor" className="bi bi-trash" viewBox="0 0 16 16"> | 6 | xmlns="http://www.w3.org/2000/svg" |
| 7 | width="16" | ||
| 8 | height="16" | ||
| 9 | fill="currentColor" | ||
| 10 | className="bi bi-trash" | ||
| 11 | viewBox="0 0 16 16" | ||
| 12 | > | ||
| 13 | <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" /> | ||
| 7 | <path | 14 | <path |
| 8 | d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/> | 15 | fill-rule="evenodd" |
| 9 | <path fill-rule="evenodd" | 16 | d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" |
| 10 | d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/> | 17 | /> |
| 11 | </svg> | 18 | </svg> |
| 12 | ) | 19 | ); |
| 13 | } | 20 | } |
diff --git a/src/components/RequestDetails/RequestDetails.tsx b/src/components/RequestDetails/RequestDetails.tsx index 34e572c..693f691 100644 --- a/src/components/RequestDetails/RequestDetails.tsx +++ b/src/components/RequestDetails/RequestDetails.tsx | |||
| @@ -1,94 +1,87 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import {useCallback, useMemo, useState} from "react"; | 2 | import { useCallback, useMemo, useState } from "react"; |
| 3 | import {RequestResponse, Headers} from "../../hooks/useRequests"; | 3 | import { RequestResponse, Headers } from "../../hooks/useRequests"; |
| 4 | import styles from "./RequestDetails.module.scss"; | 4 | import styles from "./RequestDetails.module.scss"; |
| 5 | import RequestSummary from "../RequestSummary/RequestSummary"; | 5 | import RequestSummary from "../RequestSummary/RequestSummary"; |
| 6 | import Content from "../Content/Content"; | 6 | import Content from "../Content/Content"; |
| 7 | import {getHost} from "../../utils"; | 7 | import { getHost } from "../../utils"; |
| 8 | import {Button, Card, Col, Container, Nav, Row, Table} from "react-bootstrap"; | 8 | import { Button, Card, Col, Container, Nav, Row, Table } from "react-bootstrap"; |
| 9 | |||
| 10 | 9 | ||
| 11 | interface TimingProps { | 10 | interface TimingProps { |
| 12 | timing: number; | 11 | timing: number; |
| 13 | } | 12 | } |
| 14 | 13 | ||
| 15 | |||
| 16 | function Timing({ timing }: TimingProps) { | 14 | function Timing({ timing }: TimingProps) { |
| 17 | const value = useMemo(() => (Math.round(timing * 1000) / 1000), [timing]); | 15 | const value = useMemo(() => Math.round(timing * 1000) / 1000, [timing]); |
| 18 | const showSeconds = useMemo(() => value > 1, [value]); | 16 | const showSeconds = useMemo(() => value > 1, [value]); |
| 19 | 17 | ||
| 20 | return !Number.isNaN(value) | 18 | return !Number.isNaN(value) ? ( |
| 21 | ? ( | 19 | <>{`${showSeconds ? value : value * 1000}${showSeconds ? "s" : "ms"}`}</> |
| 22 | <> | 20 | ) : null; |
| 23 | {`${showSeconds ? value : value * 1000}${ showSeconds ? 's' : 'ms' }`} | ||
| 24 | </> | ||
| 25 | ) | ||
| 26 | : null; | ||
| 27 | } | 21 | } |
| 28 | 22 | ||
| 29 | interface HeadersProps { | 23 | interface HeadersProps { |
| 30 | title: string | 24 | title: string; |
| 31 | headers: Headers | 25 | headers: Headers; |
| 32 | } | 26 | } |
| 33 | 27 | ||
| 34 | function Headers({ title, headers }: HeadersProps) { | 28 | function Headers({ title, headers }: HeadersProps) { |
| 35 | return ( | 29 | return ( |
| 36 | <Card className="m-3"> | 30 | <Card className="m-3"> |
| 37 | <Table | 31 | <Table striped responsive borderless hover className="mb-0"> |
| 38 | striped | 32 | <thead> |
| 39 | responsive | 33 | <tr> |
| 40 | borderless | 34 | <th colSpan={2} className="bg-dark text-white rounded-top"> |
| 41 | hover | 35 | {title} |
| 42 | className='mb-0' | 36 | </th> |
| 43 | > | 37 | </tr> |
| 44 | <thead> | 38 | </thead> |
| 39 | <tbody> | ||
| 40 | {headers.map(([key, value]) => ( | ||
| 45 | <tr> | 41 | <tr> |
| 46 | <th colSpan={2} className="bg-dark text-white rounded-top">{ title }</th> | 42 | <td>{key}</td> |
| 43 | <td>{value}</td> | ||
| 47 | </tr> | 44 | </tr> |
| 48 | </thead> | 45 | ))} |
| 49 | <tbody> | 46 | </tbody> |
| 50 | { | 47 | </Table> |
| 51 | headers.map(([key, value]) => ( | ||
| 52 | <tr> | ||
| 53 | <td>{key}</td> | ||
| 54 | <td>{value}</td> | ||
| 55 | </tr> | ||
| 56 | )) | ||
| 57 | } | ||
| 58 | </tbody> | ||
| 59 | </Table> | ||
| 60 | </Card> | 48 | </Card> |
| 61 | ) | 49 | ); |
| 62 | } | 50 | } |
| 63 | 51 | ||
| 64 | interface DetailsProps { | 52 | interface DetailsProps { |
| 65 | requestResponse: RequestResponse | null | 53 | requestResponse: RequestResponse | null; |
| 66 | } | 54 | } |
| 67 | 55 | ||
| 68 | type Tab = 'headers' | 'request' | 'response' | 56 | type Tab = "headers" | "request" | "response"; |
| 69 | 57 | ||
| 70 | export default function RequestDetails({ requestResponse }: DetailsProps) { | 58 | export default function RequestDetails({ requestResponse }: DetailsProps) { |
| 71 | const [tab, selectTab] = useState<Tab>('headers'); | 59 | const [tab, selectTab] = useState<Tab>("headers"); |
| 72 | const [raw, setRaw] = useState(false); | 60 | const [raw, setRaw] = useState(false); |
| 73 | 61 | ||
| 74 | const resend = useCallback(async () => requestResponse !== null && fetch( | 62 | const resend = useCallback( |
| 75 | `http://${getHost()}/resend/`, | 63 | async () => |
| 76 | { | 64 | requestResponse !== null && |
| 77 | method: 'POST', | 65 | fetch(`http://${getHost()}/resend/`, { |
| 78 | body: JSON.stringify({ | 66 | method: "POST", |
| 79 | ...requestResponse.request, | 67 | body: JSON.stringify({ |
| 80 | id: undefined | 68 | ...requestResponse.request, |
| 81 | }) | 69 | id: undefined, |
| 82 | } | 70 | }), |
| 83 | ), [requestResponse]); | 71 | }), |
| 72 | [requestResponse] | ||
| 73 | ); | ||
| 84 | 74 | ||
| 85 | return requestResponse !== null ? ( | 75 | return requestResponse !== null ? ( |
| 86 | <div className={styles.details}> | 76 | <div className={styles.details}> |
| 87 | <div className={styles.header}> | 77 | <div className={styles.header}> |
| 88 | <Row> | 78 | <Row> |
| 89 | <Col> | 79 | <Col> |
| 90 | <Container fluid style={{ fontSize: '1.5em' }} className="py-3"> | 80 | <Container fluid style={{ fontSize: "1.5em" }} className="py-3"> |
| 91 | <RequestSummary requestResponse={requestResponse} className={styles.summary}/> | 81 | <RequestSummary |
| 82 | requestResponse={requestResponse} | ||
| 83 | className={styles.summary} | ||
| 84 | /> | ||
| 92 | </Container> | 85 | </Container> |
| 93 | </Col> | 86 | </Col> |
| 94 | </Row> | 87 | </Row> |
| @@ -100,14 +93,10 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 100 | onSelect={(tab) => selectTab(tab as Tab)} | 93 | onSelect={(tab) => selectTab(tab as Tab)} |
| 101 | > | 94 | > |
| 102 | <Nav.Item> | 95 | <Nav.Item> |
| 103 | <Nav.Link eventKey="headers"> | 96 | <Nav.Link eventKey="headers">Headers</Nav.Link> |
| 104 | Headers | ||
| 105 | </Nav.Link > | ||
| 106 | </Nav.Item> | 97 | </Nav.Item> |
| 107 | <Nav.Item> | 98 | <Nav.Item> |
| 108 | <Nav.Link eventKey="request"> | 99 | <Nav.Link eventKey="request">Request</Nav.Link> |
| 109 | Request | ||
| 110 | </Nav.Link> | ||
| 111 | </Nav.Item> | 100 | </Nav.Item> |
| 112 | <Nav.Item> | 101 | <Nav.Item> |
| 113 | <Nav.Link | 102 | <Nav.Link |
| @@ -121,46 +110,42 @@ export default function RequestDetails({ requestResponse }: DetailsProps) { | |||
| 121 | </Col> | 110 | </Col> |
| 122 | <Col className="border-bottom px-3 " xs="auto"> | 111 | <Col className="border-bottom px-3 " xs="auto"> |
| 123 | <Timing timing={requestResponse.response?.timing ?? NaN} /> | 112 | <Timing timing={requestResponse.response?.timing ?? NaN} /> |
| 124 | <Button variant="outline-primary" onClick={() => resend()} className="ms-3"> | 113 | <Button |
| 114 | variant="outline-primary" | ||
| 115 | onClick={() => resend()} | ||
| 116 | className="ms-3" | ||
| 117 | > | ||
| 125 | Resend | 118 | Resend |
| 126 | </Button> | 119 | </Button> |
| 127 | </Col> | 120 | </Col> |
| 128 | </Row> | 121 | </Row> |
| 129 | </div> | 122 | </div> |
| 130 | <div className={styles.content}> | 123 | <div className={styles.content}> |
| 131 | { | 124 | {tab === "headers" && ( |
| 132 | tab === 'headers' && ( | 125 | <> |
| 133 | <> | 126 | <Headers |
| 127 | title="request headers" | ||
| 128 | headers={requestResponse.request.headers} | ||
| 129 | /> | ||
| 130 | {requestResponse.response && ( | ||
| 134 | <Headers | 131 | <Headers |
| 135 | title="request headers" | 132 | title="response headers" |
| 136 | headers={requestResponse.request.headers} | 133 | headers={requestResponse.response.headers} |
| 137 | /> | 134 | /> |
| 138 | { | 135 | )} |
| 139 | requestResponse.response && ( | 136 | </> |
| 140 | <Headers | 137 | )} |
| 141 | title="response headers" | 138 | {tab === "request" && ( |
| 142 | headers={requestResponse.response.headers} | 139 | <Content data={requestResponse.request} raw={raw} setRaw={setRaw} /> |
| 143 | /> | 140 | )} |
| 144 | ) | 141 | {tab === "response" && requestResponse.response !== undefined && ( |
| 145 | } | 142 | <Content data={requestResponse.response} raw={raw} setRaw={setRaw} /> |
| 146 | </> | 143 | )} |
| 147 | ) | ||
| 148 | } | ||
| 149 | { | ||
| 150 | tab === 'request' && ( | ||
| 151 | <Content data={requestResponse.request} raw={raw} setRaw={setRaw}/> | ||
| 152 | ) | ||
| 153 | } | ||
| 154 | { | ||
| 155 | tab === 'response' && requestResponse.response !== undefined && ( | ||
| 156 | <Content data={requestResponse.response} raw={raw} setRaw={setRaw}/> | ||
| 157 | ) | ||
| 158 | } | ||
| 159 | </div> | 144 | </div> |
| 160 | </div> | 145 | </div> |
| 161 | ) : ( | 146 | ) : ( |
| 162 | <div className={styles.noRequestSelected}> | 147 | <div className={styles.noRequestSelected}> |
| 163 | <p>Select a request to inspect it</p> | 148 | <p>Select a request to inspect it</p> |
| 164 | </div> | 149 | </div> |
| 165 | ) | 150 | ); |
| 166 | } | 151 | } |
diff --git a/src/components/RequestList/RequestList.module.scss b/src/components/RequestList/RequestList.module.scss index 47f36c8..8022978 100644 --- a/src/components/RequestList/RequestList.module.scss +++ b/src/components/RequestList/RequestList.module.scss | |||
| @@ -16,5 +16,4 @@ | |||
| 16 | overflow-y: hidden; | 16 | overflow-y: hidden; |
| 17 | display: flex; | 17 | display: flex; |
| 18 | flex-flow: column nowrap; | 18 | flex-flow: column nowrap; |
| 19 | |||
| 20 | } | 19 | } |
diff --git a/src/components/RequestList/RequestList.tsx b/src/components/RequestList/RequestList.tsx index 95c5b75..6980738 100644 --- a/src/components/RequestList/RequestList.tsx +++ b/src/components/RequestList/RequestList.tsx | |||
| @@ -11,26 +11,29 @@ import classNames from "classnames"; | |||
| 11 | import styles from "./RequestList.module.scss"; | 11 | import styles from "./RequestList.module.scss"; |
| 12 | import RequestSummary from "../RequestSummary/RequestSummary"; | 12 | import RequestSummary from "../RequestSummary/RequestSummary"; |
| 13 | import * as React from "react"; | 13 | import * as React from "react"; |
| 14 | import {useCallback, useContext, useMemo, useState} from "react"; | 14 | import { useCallback, useContext, useMemo, useState } from "react"; |
| 15 | import {Method, RequestResponse} from "../../hooks/useRequests"; | 15 | import { Method, RequestResponse } from "../../hooks/useRequests"; |
| 16 | import {DarkModeContext} from "../../contexts/DarkMode"; | 16 | import { DarkModeContext } from "../../contexts/DarkMode"; |
| 17 | import {Filter} from "../Icons/Filter"; | 17 | import { Filter } from "../Icons/Filter"; |
| 18 | 18 | ||
| 19 | interface ListProps { | 19 | interface ListProps { |
| 20 | requests: RequestResponse[], | 20 | requests: RequestResponse[]; |
| 21 | selectedRequestIndex: number | null | 21 | selectedRequestIndex: number | null; |
| 22 | setSelectedRequestIndex: (index: number) => void | 22 | setSelectedRequestIndex: (index: number) => void; |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | type EnabledMethods = { | 25 | type EnabledMethods = { |
| 26 | [method in Method]: boolean | 26 | [method in Method]: boolean; |
| 27 | } | 27 | }; |
| 28 | |||
| 29 | 28 | ||
| 30 | export default function RequestList({ requests, selectedRequestIndex, setSelectedRequestIndex }: ListProps) { | 29 | export default function RequestList({ |
| 31 | const {darkMode} = useContext(DarkModeContext) | 30 | requests, |
| 31 | selectedRequestIndex, | ||
| 32 | setSelectedRequestIndex, | ||
| 33 | }: ListProps) { | ||
| 34 | const { darkMode } = useContext(DarkModeContext); | ||
| 32 | const [showFilterOptions, setShowFilterOptions] = useState(false); | 35 | const [showFilterOptions, setShowFilterOptions] = useState(false); |
| 33 | const [search, setSearch] = useState(''); | 36 | const [search, setSearch] = useState(""); |
| 34 | const [enableRegex, setEnableRegex] = useState(false); | 37 | const [enableRegex, setEnableRegex] = useState(false); |
| 35 | 38 | ||
| 36 | const [methods, setMethods] = useState<EnabledMethods>({ | 39 | const [methods, setMethods] = useState<EnabledMethods>({ |
| @@ -49,7 +52,7 @@ export default function RequestList({ requests, selectedRequestIndex, setSelecte | |||
| 49 | PUT: enabled, | 52 | PUT: enabled, |
| 50 | PATCH: enabled, | 53 | PATCH: enabled, |
| 51 | DELETE: enabled, | 54 | DELETE: enabled, |
| 52 | } | 55 | }; |
| 53 | 56 | ||
| 54 | if (!enabled) { | 57 | if (!enabled) { |
| 55 | methods[method] = true; | 58 | methods[method] = true; |
| @@ -58,146 +61,168 @@ export default function RequestList({ requests, selectedRequestIndex, setSelecte | |||
| 58 | setMethods(methods); | 61 | setMethods(methods); |
| 59 | }, []); | 62 | }, []); |
| 60 | 63 | ||
| 61 | const enabledMethods: Method[] = useMemo(() => (Object | 64 | const enabledMethods: Method[] = useMemo( |
| 62 | .entries(methods) | 65 | () => |
| 63 | .filter(([method, enabled]) => enabled) | 66 | Object.entries(methods) |
| 64 | .map(([method]) => method) | 67 | .filter(([method, enabled]) => enabled) |
| 65 | ), [methods]); | 68 | .map(([method]) => method), |
| 69 | [methods] | ||
| 70 | ); | ||
| 66 | 71 | ||
| 67 | const methodLabel = useMemo(() => { | 72 | const methodLabel = useMemo(() => { |
| 68 | if (enabledMethods.length == 1) { | 73 | if (enabledMethods.length == 1) { |
| 69 | return enabledMethods[0] | 74 | return enabledMethods[0]; |
| 70 | } else if (enabledMethods.length === 5) { | 75 | } else if (enabledMethods.length === 5) { |
| 71 | return 'ANY' | 76 | return "ANY"; |
| 72 | } else { | 77 | } else { |
| 73 | return 'CUSTOM' | 78 | return "CUSTOM"; |
| 74 | } | 79 | } |
| 75 | }, [enabledMethods]); | 80 | }, [enabledMethods]); |
| 76 | 81 | ||
| 77 | |||
| 78 | |||
| 79 | const filteredRequests = useMemo(() => { | 82 | const filteredRequests = useMemo(() => { |
| 80 | let searchRegex = new RegExp(''); | 83 | let searchRegex = new RegExp(""); |
| 81 | try { | 84 | try { |
| 82 | searchRegex = new RegExp( | 85 | searchRegex = new RegExp( |
| 83 | enableRegex | 86 | enableRegex ? search : search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), |
| 84 | ? search | 87 | "i" |
| 85 | : search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), | 88 | ); |
| 86 | 'i'); | 89 | } catch {} |
| 87 | } catch { | ||
| 88 | |||
| 89 | } | ||
| 90 | 90 | ||
| 91 | return ( | 91 | return ( |
| 92 | ( | 92 | requests.map((request, index) => [index, request]).reverse() as [ |
| 93 | requests | 93 | number, |
| 94 | .map((request, index) => [index, request]) | 94 | RequestResponse |
| 95 | .reverse() as [number, RequestResponse][] | 95 | ][] |
| 96 | ) | ||
| 97 | .filter(([index, request]) => ( | ||
| 98 | enabledMethods.length > 0 === null || enabledMethods.includes(request.request.method) | ||
| 99 | )) | ||
| 100 | .filter(([index, request]) => ( | ||
| 101 | search === '' || searchRegex.test(`${request.request.method} ${request.request.path}`) | ||
| 102 | )) | ||
| 103 | ) | 96 | ) |
| 104 | }, [requests, search, enabledMethods, enableRegex]) | 97 | .filter( |
| 98 | ([index, request]) => | ||
| 99 | enabledMethods.length > 0 === null || | ||
| 100 | enabledMethods.includes(request.request.method) | ||
| 101 | ) | ||
| 102 | .filter( | ||
| 103 | ([index, request]) => | ||
| 104 | search === "" || | ||
| 105 | searchRegex.test(`${request.request.method} ${request.request.path}`) | ||
| 106 | ); | ||
| 107 | }, [requests, search, enabledMethods, enableRegex]); | ||
| 105 | 108 | ||
| 106 | return ( | 109 | return ( |
| 107 | <div className={classNames(styles.listContainer, 'd-flex')}> | 110 | <div className={classNames(styles.listContainer, "d-flex")}> |
| 108 | <ListGroup variant="flush" | 111 | <ListGroup |
| 109 | className={classNames('flex-grow-0', 'border-bottom')}> | 112 | variant="flush" |
| 113 | className={classNames("flex-grow-0", "border-bottom")} | ||
| 114 | > | ||
| 110 | <ListGroup.Item> | 115 | <ListGroup.Item> |
| 111 | <InputGroup> | 116 | <InputGroup> |
| 112 | <DropdownButton variant="primary" title={methodLabel}> | 117 | <DropdownButton variant="primary" title={methodLabel}> |
| 113 | <Dropdown.Item | 118 | <Dropdown.Item onClick={() => toggleMethods(null)}> |
| 114 | onClick={() => toggleMethods(null)}>ANY</Dropdown.Item> | 119 | ANY |
| 115 | <Dropdown.Divider/> | 120 | </Dropdown.Item> |
| 116 | <Dropdown.Item | 121 | <Dropdown.Divider /> |
| 117 | onClick={() => toggleMethods('GET')}>GET</Dropdown.Item> | 122 | <Dropdown.Item onClick={() => toggleMethods("GET")}> |
| 118 | <Dropdown.Item | 123 | GET |
| 119 | onClick={() => toggleMethods('POST')}>POST</Dropdown.Item> | 124 | </Dropdown.Item> |
| 120 | <Dropdown.Item | 125 | <Dropdown.Item onClick={() => toggleMethods("POST")}> |
| 121 | onClick={() => toggleMethods('PUT')}>PUT</Dropdown.Item> | 126 | POST |
| 122 | <Dropdown.Item | 127 | </Dropdown.Item> |
| 123 | onClick={() => toggleMethods('PATCH')}>PATCH</Dropdown.Item> | 128 | <Dropdown.Item onClick={() => toggleMethods("PUT")}> |
| 124 | <Dropdown.Item | 129 | PUT |
| 125 | onClick={() => toggleMethods('DELETE')}>DELETE</Dropdown.Item> | 130 | </Dropdown.Item> |
| 131 | <Dropdown.Item onClick={() => toggleMethods("PATCH")}> | ||
| 132 | PATCH | ||
| 133 | </Dropdown.Item> | ||
| 134 | <Dropdown.Item onClick={() => toggleMethods("DELETE")}> | ||
| 135 | DELETE | ||
| 136 | </Dropdown.Item> | ||
| 126 | </DropdownButton> | 137 | </DropdownButton> |
| 127 | <Form.Control type="text" className='border-secondary' | 138 | <Form.Control |
| 128 | value={search} placeholder="Search..." | 139 | type="text" |
| 129 | onChange={({target}) => setSearch(target.value)}/> | 140 | className="border-secondary" |
| 141 | value={search} | ||
| 142 | placeholder="Search..." | ||
| 143 | onChange={({ target }) => setSearch(target.value)} | ||
| 144 | /> | ||
| 130 | <Button | 145 | <Button |
| 131 | variant={showFilterOptions ? 'secondary' : 'outline-secondary'} | 146 | variant={showFilterOptions ? "secondary" : "outline-secondary"} |
| 132 | onClick={() => setShowFilterOptions(true)}> | 147 | onClick={() => setShowFilterOptions(true)} |
| 133 | <Filter/> | 148 | > |
| 149 | <Filter /> | ||
| 134 | </Button> | 150 | </Button> |
| 135 | </InputGroup> | 151 | </InputGroup> |
| 136 | </ListGroup.Item> | 152 | </ListGroup.Item> |
| 137 | </ListGroup> | 153 | </ListGroup> |
| 138 | <ListGroup | 154 | <ListGroup |
| 139 | variant='flush' | 155 | variant="flush" |
| 140 | as="ul" | 156 | as="ul" |
| 141 | className={classNames(styles.list, 'flex-grow-1')} | 157 | className={classNames(styles.list, "flex-grow-1")} |
| 142 | > | 158 | > |
| 143 | { | 159 | {filteredRequests.length > 0 ? ( |
| 144 | filteredRequests.length > 0 | 160 | filteredRequests.map(([index, requestResponse]) => { |
| 145 | ? ( | 161 | const selected = selectedRequestIndex === index; |
| 146 | filteredRequests.map(([index, requestResponse]) => { | 162 | return ( |
| 147 | const selected = selectedRequestIndex === index; | 163 | <ListGroup.Item |
| 148 | return ( | 164 | as="li" |
| 149 | <ListGroup.Item | 165 | onClick={() => setSelectedRequestIndex(index)} |
| 150 | as="li" | 166 | key={`request-${index}`} |
| 151 | onClick={() => setSelectedRequestIndex(index)} | 167 | className={classNames({ |
| 152 | key={`request-${index}`} | 168 | "bg-primary": selected, |
| 153 | className={classNames({ | 169 | "text-light": selected, |
| 154 | 'bg-primary': selected, | 170 | "border-bottom": true, |
| 155 | 'text-light': selected, | 171 | })} |
| 156 | 'border-bottom': true | 172 | > |
| 157 | })} | 173 | <RequestSummary |
| 158 | > | 174 | requestResponse={requestResponse} |
| 159 | <RequestSummary | 175 | selected={selected} |
| 160 | requestResponse={requestResponse} | 176 | /> |
| 161 | selected={selected} | 177 | </ListGroup.Item> |
| 162 | /> | 178 | ); |
| 163 | </ListGroup.Item> | 179 | }) |
| 164 | ) | 180 | ) : ( |
| 165 | }) | 181 | <div className={styles.noRequest}> |
| 166 | ) | 182 | <p>No requests</p> |
| 167 | : ( | 183 | </div> |
| 168 | <div className={styles.noRequest}> | 184 | )} |
| 169 | <p>No requests</p> | ||
| 170 | </div> | ||
| 171 | ) | ||
| 172 | } | ||
| 173 | </ListGroup> | 185 | </ListGroup> |
| 174 | <Offcanvas className={ | 186 | <Offcanvas |
| 175 | classNames({ | 187 | className={classNames({ |
| 176 | 'bg-dark': darkMode | 188 | "bg-dark": darkMode, |
| 177 | }) | 189 | })} |
| 178 | } | 190 | show={showFilterOptions} |
| 179 | show={showFilterOptions} | 191 | onHide={() => setShowFilterOptions(false)} |
| 180 | onHide={() => setShowFilterOptions(false)}> | 192 | > |
| 181 | <Offcanvas.Header closeButton> | 193 | <Offcanvas.Header closeButton> |
| 182 | <Offcanvas.Title> | 194 | <Offcanvas.Title>Filter Options</Offcanvas.Title> |
| 183 | Filter Options | ||
| 184 | </Offcanvas.Title> | ||
| 185 | </Offcanvas.Header> | 195 | </Offcanvas.Header> |
| 186 | <Offcanvas.Body> | 196 | <Offcanvas.Body> |
| 187 | <Form.Group className="mb-4"> | 197 | <Form.Group className="mb-4"> |
| 188 | <Form.Label className="fw-bold">Search</Form.Label> | 198 | <Form.Label className="fw-bold">Search</Form.Label> |
| 189 | <Form.Control type="text" value={search} placeholder="Search..." | 199 | <Form.Control |
| 190 | onChange={({target}) => setSearch(target.value)}/> | 200 | type="text" |
| 191 | <Form.Check className="mt-2" type='switch' label='Regex search' checked={enableRegex} onChange={() => setEnableRegex(!enableRegex)} /> | 201 | value={search} |
| 202 | placeholder="Search..." | ||
| 203 | onChange={({ target }) => setSearch(target.value)} | ||
| 204 | /> | ||
| 205 | <Form.Check | ||
| 206 | className="mt-2" | ||
| 207 | type="switch" | ||
| 208 | label="Regex search" | ||
| 209 | checked={enableRegex} | ||
| 210 | onChange={() => setEnableRegex(!enableRegex)} | ||
| 211 | /> | ||
| 192 | </Form.Group> | 212 | </Form.Group> |
| 193 | <Form.Group> | 213 | <Form.Group> |
| 194 | <Form.Label className="fw-bold">Method</Form.Label> | 214 | <Form.Label className="fw-bold">Method</Form.Label> |
| 195 | {Object.entries(methods).map(([method, enabled]) => ( | 215 | {Object.entries(methods).map(([method, enabled]) => ( |
| 196 | <Form.Check type='switch' label={method} checked={enabled} onChange={() => setMethods({...methods, [method]: !enabled})} /> | 216 | <Form.Check |
| 217 | type="switch" | ||
| 218 | label={method} | ||
| 219 | checked={enabled} | ||
| 220 | onChange={() => setMethods({ ...methods, [method]: !enabled })} | ||
| 221 | /> | ||
| 197 | ))} | 222 | ))} |
| 198 | </Form.Group> | 223 | </Form.Group> |
| 199 | </Offcanvas.Body> | 224 | </Offcanvas.Body> |
| 200 | </Offcanvas> | 225 | </Offcanvas> |
| 201 | </div> | 226 | </div> |
| 202 | ) | 227 | ); |
| 203 | } | 228 | } |
diff --git a/src/components/RequestSummary/RequestSummary.tsx b/src/components/RequestSummary/RequestSummary.tsx index b3e7ddc..db2f69c 100644 --- a/src/components/RequestSummary/RequestSummary.tsx +++ b/src/components/RequestSummary/RequestSummary.tsx | |||
| @@ -1,49 +1,54 @@ | |||
| 1 | import {RequestResponse} from "~hooks/useRequests"; | 1 | import { RequestResponse } from "~hooks/useRequests"; |
| 2 | import * as React from "react"; | 2 | import * as React from "react"; |
| 3 | import classNames from "classnames"; | 3 | import classNames from "classnames"; |
| 4 | 4 | ||
| 5 | import {Badge, Card, Col, Row} from "react-bootstrap"; | 5 | import { Badge, Card, Col, Row } from "react-bootstrap"; |
| 6 | 6 | ||
| 7 | interface RequestSummaryProps { | 7 | interface RequestSummaryProps { |
| 8 | selected?: boolean | 8 | selected?: boolean; |
| 9 | requestResponse: RequestResponse | 9 | requestResponse: RequestResponse; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | function isBetween(value: number, min: number, max: number) { | 12 | function isBetween(value: number, min: number, max: number) { |
| 13 | return value >= min && value <= max | 13 | return value >= min && value <= max; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | function calcBadgeVariant(statusCode: number | undefined): string { | 16 | function calcBadgeVariant(statusCode: number | undefined): string { |
| 17 | if (statusCode === undefined) { | 17 | if (statusCode === undefined) { |
| 18 | return 'secondary'; | 18 | return "secondary"; |
| 19 | } else if (isBetween(statusCode, 100, 199)) { | 19 | } else if (isBetween(statusCode, 100, 199)) { |
| 20 | return 'info'; | 20 | return "info"; |
| 21 | } else if (isBetween(statusCode, 200, 299)) { | 21 | } else if (isBetween(statusCode, 200, 299)) { |
| 22 | return 'success'; | 22 | return "success"; |
| 23 | } else if (isBetween(statusCode, 300, 399)) { | 23 | } else if (isBetween(statusCode, 300, 399)) { |
| 24 | return 'primary'; | 24 | return "primary"; |
| 25 | } else if (isBetween(statusCode, 400, 499)) { | 25 | } else if (isBetween(statusCode, 400, 499)) { |
| 26 | return 'danger'; | 26 | return "danger"; |
| 27 | } else if (isBetween(statusCode, 500, 599)) { | 27 | } else if (isBetween(statusCode, 500, 599)) { |
| 28 | return 'warning'; | 28 | return "warning"; |
| 29 | } | 29 | } |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | export default function RequestSummary({ requestResponse: { request, response }, selected = false }: RequestSummaryProps) { | 32 | export default function RequestSummary({ |
| 33 | requestResponse: { request, response }, | ||
| 34 | selected = false, | ||
| 35 | }: RequestSummaryProps) { | ||
| 33 | return ( | 36 | return ( |
| 34 | <Row> | 37 | <Row> |
| 35 | <Col className='flex-grow-0 d-flex align-items-center'>{request.method}</Col> | 38 | <Col className="flex-grow-0 d-flex align-items-center"> |
| 36 | <Col className='flex-grow-1'>{ request.path }</Col> | 39 | {request.method} |
| 37 | <Col className='flex-grow-0 d-flex align-items-center'> | 40 | </Col> |
| 41 | <Col className="flex-grow-1">{request.path}</Col> | ||
| 42 | <Col className="flex-grow-0 d-flex align-items-center"> | ||
| 38 | <Badge | 43 | <Badge |
| 39 | className={classNames({ | 44 | className={classNames({ |
| 40 | 'border': selected, | 45 | border: selected, |
| 41 | })} | 46 | })} |
| 42 | bg={calcBadgeVariant(response?.status)} | 47 | bg={calcBadgeVariant(response?.status)} |
| 43 | > | 48 | > |
| 44 | { response?.status ?? 'Loading...'} | 49 | {response?.status ?? "Loading..."} |
| 45 | </Badge> | 50 | </Badge> |
| 46 | </Col> | 51 | </Col> |
| 47 | </Row> | 52 | </Row> |
| 48 | ) | 53 | ); |
| 49 | } | 54 | } |
