aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2022-02-12 10:59:19 +0100
committerGravatar Tom van der Lee <tom@vanderlee.io>2022-02-12 11:07:50 +0100
commit7c48533571e9f9d3731a59433a56cc8d6e008123 (patch)
treef86868dfea251fa3b198ffcf3e158a3ed354ff9d /src/components
parent5dbf880ce3cdb227a85dbb0015609c210557c60b (diff)
downloadclient-7c48533571e9f9d3731a59433a56cc8d6e008123.tar.gz
client-7c48533571e9f9d3731a59433a56cc8d6e008123.tar.bz2
client-7c48533571e9f9d3731a59433a56cc8d6e008123.zip
Added search and filter options
Diffstat (limited to 'src/components')
-rw-r--r--src/components/App/App.module.scss9
-rw-r--r--src/components/App/App.tsx54
-rw-r--r--src/components/Content/Content.tsx1
-rw-r--r--src/components/Icons/Filter.tsx10
-rw-r--r--src/components/RequestDetails/RequestDetails.module.scss (renamed from src/components/Details/Details.module.scss)8
-rw-r--r--src/components/RequestDetails/RequestDetails.tsx (renamed from src/components/Details/Details.tsx)16
-rw-r--r--src/components/RequestList/RequestList.module.scss20
-rw-r--r--src/components/RequestList/RequestList.tsx203
8 files changed, 257 insertions, 64 deletions
diff --git a/src/components/App/App.module.scss b/src/components/App/App.module.scss
index 088a4f6..61e399e 100644
--- a/src/components/App/App.module.scss
+++ b/src/components/App/App.module.scss
@@ -16,7 +16,6 @@
16.sidebar { 16.sidebar {
17 width: calc((6 / 16) * 100%); 17 width: calc((6 / 16) * 100%);
18 height: 100%; 18 height: 100%;
19 overflow-y: auto;
20} 19}
21 20
22.details { 21.details {
@@ -24,11 +23,3 @@
24 overflow: hidden; 23 overflow: hidden;
25 height: 100%; 24 height: 100%;
26} 25}
27
28.noRequest, .noRequestSelected {
29 width: 100%;
30 height: 100%;
31 display: flex;
32 justify-content: center;
33 align-items: center;
34}
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 55ab5b4..e08c84f 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -6,8 +6,7 @@ import useRequests, {
6} from "../../hooks/useRequests"; 6} from "../../hooks/useRequests";
7 7
8import styles from './App.module.scss'; 8import styles from './App.module.scss';
9import Details from "../Details/Details"; 9import RequestDetails from "../RequestDetails/RequestDetails";
10import RequestSummary from "../RequestSummary/RequestSummary";
11import {getHost} from "../../utils"; 10import {getHost} from "../../utils";
12import {Container, ListGroup, Nav, Navbar, NavDropdown} from "react-bootstrap"; 11import {Container, ListGroup, Nav, Navbar, NavDropdown} from "react-bootstrap";
13import classNames from "classnames"; 12import classNames from "classnames";
@@ -16,6 +15,7 @@ import {Sun} from "../Icons/Sun";
16import {Moon} from "../Icons/Moon"; 15import {Moon} from "../Icons/Moon";
17import Trash from "../Icons/Trash"; 16import Trash from "../Icons/Trash";
18import {DarkModeContext} from "../../contexts/DarkMode"; 17import {DarkModeContext} from "../../contexts/DarkMode";
18import RequestList from "../RequestList/RequestList";
19 19
20interface Config { 20interface Config {
21 url: string 21 url: string
@@ -143,53 +143,11 @@ export default function App() {
143 </Navbar> 143 </Navbar>
144 144
145 <main className={styles.main}> 145 <main className={styles.main}>
146 <ListGroup 146 <div className={classNames("border-end", styles.sidebar)}>
147 variant='flush' 147 <RequestList requests={calls} selectedRequestIndex={selectedRequestIndex} setSelectedRequestIndex={setSelectedRequestIndex}/>
148 as="ul" 148 </div>
149 className={classNames("border-end", styles.sidebar)}
150 >
151 {
152 calls.length > 0
153 ? (
154 calls.slice(0).reverse().map((requestResponse, index) => {
155 const selected = selectedRequestIndex === calls.length - index - 1;
156 return (
157 <ListGroup.Item
158 as="li"
159 onClick={() => setSelectedRequestIndex(calls.length - index - 1)}
160 key={`request-${index}`}
161 className={classNames({
162 'bg-primary': selected,
163 'text-light': selected,
164 'border-bottom': true
165 })}
166 >
167 <RequestSummary
168 requestResponse={requestResponse}
169 selected={selected}
170 />
171 </ListGroup.Item>
172 )
173 })
174 )
175 : (
176 <div className={styles.noRequest}>
177 <p>No requests</p>
178 </div>
179 )
180 }
181 </ListGroup>
182 <div className={styles.details}> 149 <div className={styles.details}>
183 { 150 <RequestDetails requestResponse={selectedRequest} />
184 selectedRequest !== null
185 ? (
186 <Details requestResponse={selectedRequest} />
187 ) : (
188 <div className={styles.noRequestSelected}>
189 <p>Select a request to inspect it</p>
190 </div>
191 )
192 }
193 </div> 151 </div>
194 </main> 152 </main>
195 </div> 153 </div>
diff --git a/src/components/Content/Content.tsx b/src/components/Content/Content.tsx
index 3d747b2..c7a5f58 100644
--- a/src/components/Content/Content.tsx
+++ b/src/components/Content/Content.tsx
@@ -7,7 +7,6 @@ import {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(headers: Headers, key: string, unit?: string): string | null {
10 console.log(headers, key)
11 try { 10 try {
12 const [_, value] = headers.find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase()) 11 const [_, value] = headers.find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase())
13 return unit !== undefined 12 return unit !== undefined
diff --git a/src/components/Icons/Filter.tsx b/src/components/Icons/Filter.tsx
new file mode 100644
index 0000000..a14183d
--- /dev/null
+++ b/src/components/Icons/Filter.tsx
@@ -0,0 +1,10 @@
1import * as React from "react";
2
3export function Filter() {
4 return <svg xmlns="http://www.w3.org/2000/svg" width="16"
5 height="16" fill="currentColor" className="bi bi-filter"
6 viewBox="0 0 16 16">
7 <path
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"/>
9 </svg>;
10}
diff --git a/src/components/Details/Details.module.scss b/src/components/RequestDetails/RequestDetails.module.scss
index 146b5d8..8a872f7 100644
--- a/src/components/Details/Details.module.scss
+++ b/src/components/RequestDetails/RequestDetails.module.scss
@@ -16,3 +16,11 @@
16 flex-shrink: 1; 16 flex-shrink: 1;
17 overflow-y: auto; 17 overflow-y: auto;
18} 18}
19
20.noRequestSelected {
21 width: 100%;
22 height: 100%;
23 display: flex;
24 justify-content: center;
25 align-items: center;
26}
diff --git a/src/components/Details/Details.tsx b/src/components/RequestDetails/RequestDetails.tsx
index 96f0790..34e572c 100644
--- a/src/components/Details/Details.tsx
+++ b/src/components/RequestDetails/RequestDetails.tsx
@@ -1,7 +1,7 @@
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 "./Details.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";
@@ -62,16 +62,16 @@ function Headers({ title, headers }: HeadersProps) {
62} 62}
63 63
64interface DetailsProps { 64interface DetailsProps {
65 requestResponse: RequestResponse 65 requestResponse: RequestResponse | null
66} 66}
67 67
68type Tab = 'headers' | 'request' | 'response' 68type Tab = 'headers' | 'request' | 'response'
69 69
70export default function Details({ requestResponse }: DetailsProps) { 70export default function RequestDetails({ requestResponse }: DetailsProps) {
71 const [tab, selectTab] = useState<Tab>('headers'); 71 const [tab, selectTab] = useState<Tab>('headers');
72 const [raw, setRaw] = useState(false); 72 const [raw, setRaw] = useState(false);
73 73
74 const resend = useCallback(async () => fetch( 74 const resend = useCallback(async () => requestResponse !== null && fetch(
75 `http://${getHost()}/resend/`, 75 `http://${getHost()}/resend/`,
76 { 76 {
77 method: 'POST', 77 method: 'POST',
@@ -82,7 +82,7 @@ export default function Details({ requestResponse }: DetailsProps) {
82 } 82 }
83 ), [requestResponse]); 83 ), [requestResponse]);
84 84
85 return ( 85 return requestResponse !== null ? (
86 <div className={styles.details}> 86 <div className={styles.details}>
87 <div className={styles.header}> 87 <div className={styles.header}>
88 <Row> 88 <Row>
@@ -158,5 +158,9 @@ export default function Details({ requestResponse }: DetailsProps) {
158 } 158 }
159 </div> 159 </div>
160 </div> 160 </div>
161 ) : (
162 <div className={styles.noRequestSelected}>
163 <p>Select a request to inspect it</p>
164 </div>
161 ) 165 )
162} 166}
diff --git a/src/components/RequestList/RequestList.module.scss b/src/components/RequestList/RequestList.module.scss
new file mode 100644
index 0000000..47f36c8
--- /dev/null
+++ b/src/components/RequestList/RequestList.module.scss
@@ -0,0 +1,20 @@
1.noRequest {
2 width: 100%;
3 height: 100%;
4 display: flex;
5 justify-content: center;
6 align-items: center;
7}
8
9.list {
10 //height: 100%;
11 overflow-y: auto;
12}
13
14.listContainer {
15 height: 100%;
16 overflow-y: hidden;
17 display: flex;
18 flex-flow: column nowrap;
19
20}
diff --git a/src/components/RequestList/RequestList.tsx b/src/components/RequestList/RequestList.tsx
new file mode 100644
index 0000000..95c5b75
--- /dev/null
+++ b/src/components/RequestList/RequestList.tsx
@@ -0,0 +1,203 @@
1import {
2 Button,
3 Dropdown,
4 DropdownButton,
5 Form,
6 InputGroup,
7 ListGroup,
8 Offcanvas,
9} from "react-bootstrap";
10import classNames from "classnames";
11import styles from "./RequestList.module.scss";
12import RequestSummary from "../RequestSummary/RequestSummary";
13import * as React from "react";
14import {useCallback, useContext, useMemo, useState} from "react";
15import {Method, RequestResponse} from "../../hooks/useRequests";
16import {DarkModeContext} from "../../contexts/DarkMode";
17import {Filter} from "../Icons/Filter";
18
19interface ListProps {
20 requests: RequestResponse[],
21 selectedRequestIndex: number | null
22 setSelectedRequestIndex: (index: number) => void
23}
24
25type EnabledMethods = {
26 [method in Method]: boolean
27}
28
29
30export default function RequestList({ requests, selectedRequestIndex, setSelectedRequestIndex }: ListProps) {
31 const {darkMode} = useContext(DarkModeContext)
32 const [showFilterOptions, setShowFilterOptions] = useState(false);
33 const [search, setSearch] = useState('');
34 const [enableRegex, setEnableRegex] = useState(false);
35
36 const [methods, setMethods] = useState<EnabledMethods>({
37 GET: true,
38 POST: true,
39 PUT: true,
40 PATCH: true,
41 DELETE: true,
42 });
43
44 const toggleMethods = useCallback((method: Method | null) => {
45 const enabled = method == null;
46 const methods = {
47 GET: enabled,
48 POST: enabled,
49 PUT: enabled,
50 PATCH: enabled,
51 DELETE: enabled,
52 }
53
54 if (!enabled) {
55 methods[method] = true;
56 }
57
58 setMethods(methods);
59 }, []);
60
61 const enabledMethods: Method[] = useMemo(() => (Object
62 .entries(methods)
63 .filter(([method, enabled]) => enabled)
64 .map(([method]) => method)
65 ), [methods]);
66
67 const methodLabel = useMemo(() => {
68 if (enabledMethods.length == 1) {
69 return enabledMethods[0]
70 } else if (enabledMethods.length === 5) {
71 return 'ANY'
72 } else {
73 return 'CUSTOM'
74 }
75 }, [enabledMethods]);
76
77
78
79 const filteredRequests = useMemo(() => {
80 let searchRegex = new RegExp('');
81 try {
82 searchRegex = new RegExp(
83 enableRegex
84 ? search
85 : search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
86 'i');
87 } catch {
88
89 }
90
91 return (
92 (
93 requests
94 .map((request, index) => [index, request])
95 .reverse() as [number, RequestResponse][]
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 )
104 }, [requests, search, enabledMethods, enableRegex])
105
106 return (
107 <div className={classNames(styles.listContainer, 'd-flex')}>
108 <ListGroup variant="flush"
109 className={classNames('flex-grow-0', 'border-bottom')}>
110 <ListGroup.Item>
111 <InputGroup>
112 <DropdownButton variant="primary" title={methodLabel}>
113 <Dropdown.Item
114 onClick={() => toggleMethods(null)}>ANY</Dropdown.Item>
115 <Dropdown.Divider/>
116 <Dropdown.Item
117 onClick={() => toggleMethods('GET')}>GET</Dropdown.Item>
118 <Dropdown.Item
119 onClick={() => toggleMethods('POST')}>POST</Dropdown.Item>
120 <Dropdown.Item
121 onClick={() => toggleMethods('PUT')}>PUT</Dropdown.Item>
122 <Dropdown.Item
123 onClick={() => toggleMethods('PATCH')}>PATCH</Dropdown.Item>
124 <Dropdown.Item
125 onClick={() => toggleMethods('DELETE')}>DELETE</Dropdown.Item>
126 </DropdownButton>
127 <Form.Control type="text" className='border-secondary'
128 value={search} placeholder="Search..."
129 onChange={({target}) => setSearch(target.value)}/>
130 <Button
131 variant={showFilterOptions ? 'secondary' : 'outline-secondary'}
132 onClick={() => setShowFilterOptions(true)}>
133 <Filter/>
134 </Button>
135 </InputGroup>
136 </ListGroup.Item>
137 </ListGroup>
138 <ListGroup
139 variant='flush'
140 as="ul"
141 className={classNames(styles.list, 'flex-grow-1')}
142 >
143 {
144 filteredRequests.length > 0
145 ? (
146 filteredRequests.map(([index, requestResponse]) => {
147 const selected = selectedRequestIndex === index;
148 return (
149 <ListGroup.Item
150 as="li"
151 onClick={() => setSelectedRequestIndex(index)}
152 key={`request-${index}`}
153 className={classNames({
154 'bg-primary': selected,
155 'text-light': selected,
156 'border-bottom': true
157 })}
158 >
159 <RequestSummary
160 requestResponse={requestResponse}
161 selected={selected}
162 />
163 </ListGroup.Item>
164 )
165 })
166 )
167 : (
168 <div className={styles.noRequest}>
169 <p>No requests</p>
170 </div>
171 )
172 }
173 </ListGroup>
174 <Offcanvas className={
175 classNames({
176 'bg-dark': darkMode
177 })
178 }
179 show={showFilterOptions}
180 onHide={() => setShowFilterOptions(false)}>
181 <Offcanvas.Header closeButton>
182 <Offcanvas.Title>
183 Filter Options
184 </Offcanvas.Title>
185 </Offcanvas.Header>
186 <Offcanvas.Body>
187 <Form.Group className="mb-4">
188 <Form.Label className="fw-bold">Search</Form.Label>
189 <Form.Control type="text" value={search} placeholder="Search..."
190 onChange={({target}) => setSearch(target.value)}/>
191 <Form.Check className="mt-2" type='switch' label='Regex search' checked={enableRegex} onChange={() => setEnableRegex(!enableRegex)} />
192 </Form.Group>
193 <Form.Group>
194 <Form.Label className="fw-bold">Method</Form.Label>
195 {Object.entries(methods).map(([method, enabled]) => (
196 <Form.Check type='switch' label={method} checked={enabled} onChange={() => setMethods({...methods, [method]: !enabled})} />
197 ))}
198 </Form.Group>
199 </Offcanvas.Body>
200 </Offcanvas>
201 </div>
202 )
203}