diff options
| -rw-r--r-- | index.html | 2 | ||||
| -rw-r--r-- | src/components/App/App.tsx | 46 | ||||
| -rw-r--r-- | src/hooks/useRequests.tsx | 69 | ||||
| -rw-r--r-- | ttun/__main__.py | 16 | ||||
| -rw-r--r-- | ttun/inspect_server.py | 19 | ||||
| -rw-r--r-- | ttun/pubsub.py | 21 |
6 files changed, 136 insertions, 37 deletions
| @@ -1,7 +1,7 @@ | |||
| 1 | <!DOCTYPE html> | 1 | <!DOCTYPE html> |
| 2 | <html> | 2 | <html> |
| 3 | <head> | 3 | <head> |
| 4 | <title>React Vite Micro Typescript App</title> | 4 | <title></title> |
| 5 | </head> | 5 | </head> |
| 6 | <body> | 6 | <body> |
| 7 | <div id="root"></div> | 7 | <div id="root"></div> |
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 3a7fe9b..e361aa5 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | import * as React from "react"; | 1 | import * as React from "react"; |
| 2 | import useRequests, {RequestResponse} from "../../hooks/useRequests"; | 2 | import useRequests, {RequestResponse, ReadyState} from "../../hooks/useRequests"; |
| 3 | import {useEffect, useMemo, useState} from "react"; | 3 | import {useEffect, useMemo, useState} from "react"; |
| 4 | 4 | ||
| 5 | import styles from './App.module.scss'; | 5 | import styles from './App.module.scss'; |
| @@ -11,34 +11,56 @@ interface Config { | |||
| 11 | url: string | 11 | url: string |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | type ReadyStateMap = { | ||
| 15 | [ReadyState.CONNECTING]: string, | ||
| 16 | [ReadyState.OPEN]: string, | ||
| 17 | [ReadyState.CLOSING]: string, | ||
| 18 | [ReadyState.CLOSED]: string, | ||
| 19 | } | ||
| 20 | |||
| 21 | const statusMap: ReadyStateMap = { | ||
| 22 | [ReadyState.CONNECTING]: '🔴', | ||
| 23 | [ReadyState.OPEN]: '🟢', | ||
| 24 | [ReadyState.CLOSING]: '🔴', | ||
| 25 | [ReadyState.CLOSED]: '🔴', | ||
| 26 | } | ||
| 27 | |||
| 14 | export default function App() { | 28 | export default function App() { |
| 29 | |||
| 15 | const [config, setConfig]= useState<Config | null>(null) | 30 | const [config, setConfig]= useState<Config | null>(null) |
| 31 | |||
| 32 | const { calls, readyState } = useRequests({ | ||
| 33 | onConnect: async () => { | ||
| 34 | const response = await fetch(`http://${getHost()}/config/`) | ||
| 35 | const config = await response.json() | ||
| 36 | setConfig(config) | ||
| 37 | } | ||
| 38 | }); | ||
| 39 | |||
| 16 | useEffect(() => { | 40 | useEffect(() => { |
| 17 | fetch(`http://${getHost()}/config/`) | 41 | const url = new URL(config?.url ?? 'https://loading...'); |
| 18 | .then(response => response.json() as Promise<Config>) | 42 | document.title = `${statusMap[readyState]} ${url.host} | TTUN`; |
| 19 | .then(setConfig) | 43 | }, [readyState, config?.url]) |
| 20 | }, []) | ||
| 21 | 44 | ||
| 22 | const requests = useRequests(); | ||
| 23 | const [selectedRequestIndex, setSelectedRequestIndex] = useState<number | null>(null); | 45 | const [selectedRequestIndex, setSelectedRequestIndex] = useState<number | null>(null); |
| 24 | const selectedRequest = useMemo<RequestResponse | null>(() => ( | 46 | const selectedRequest = useMemo<RequestResponse | null>(() => ( |
| 25 | selectedRequestIndex === null | 47 | selectedRequestIndex === null |
| 26 | ? null | 48 | ? null |
| 27 | : requests[selectedRequestIndex] | 49 | : calls[selectedRequestIndex] |
| 28 | ), [selectedRequestIndex, requests]); | 50 | ), [selectedRequestIndex, calls]); |
| 29 | 51 | ||
| 30 | return config && ( | 52 | return config && ( |
| 31 | <div className={styles.app}> | 53 | <div className={styles.app}> |
| 32 | <header className={styles.header}> | 54 | <header className={styles.header}> |
| 33 | TTUN | 55 | {statusMap[readyState]} TTUN |
| 34 | <a href={config.url} target="_blank">{config.url}</a> | 56 | <a href={config.url} target="_blank">{config.url}</a> |
| 35 | </header> | 57 | </header> |
| 36 | <main className={styles.main}> | 58 | <main className={styles.main}> |
| 37 | <ul className={styles.sidebar}> | 59 | <ul className={styles.sidebar}> |
| 38 | { | 60 | { |
| 39 | requests.length > 0 | 61 | calls.length > 0 |
| 40 | ? requests.slice(0).reverse().map((requestResponse, index) => ( | 62 | ? calls.slice(0).reverse().map((requestResponse, index) => ( |
| 41 | <li onClick={() => setSelectedRequestIndex(requests.length - index - 1)} key={`request-${index}`}> | 63 | <li onClick={() => setSelectedRequestIndex(calls.length - index - 1)} key={`request-${index}`}> |
| 42 | <RequestSummary requestResponse={requestResponse} /> | 64 | <RequestSummary requestResponse={requestResponse} /> |
| 43 | </li> | 65 | </li> |
| 44 | )) | 66 | )) |
diff --git a/src/hooks/useRequests.tsx b/src/hooks/useRequests.tsx index 3ad5cc7..2b8393e 100644 --- a/src/hooks/useRequests.tsx +++ b/src/hooks/useRequests.tsx | |||
| @@ -33,25 +33,67 @@ interface Response { | |||
| 33 | payload: ResponsePayload, | 33 | payload: ResponsePayload, |
| 34 | } | 34 | } |
| 35 | 35 | ||
| 36 | interface Historic { | ||
| 37 | type: 'historic' | ||
| 38 | payload: (Request | Response)[] | ||
| 39 | } | ||
| 40 | |||
| 36 | export interface RequestResponse { | 41 | export interface RequestResponse { |
| 37 | request: RequestPayload | 42 | request: RequestPayload |
| 38 | response?: ResponsePayload | 43 | response?: ResponsePayload |
| 39 | } | 44 | } |
| 40 | 45 | ||
| 41 | export default function useRequests(): RequestResponse[] { | 46 | export enum ReadyState { |
| 47 | CONNECTING = 0, | ||
| 48 | OPEN = 1, | ||
| 49 | CLOSING = 2, | ||
| 50 | CLOSED = 3, | ||
| 51 | } | ||
| 52 | |||
| 53 | export interface useRequestsProps { | ||
| 54 | onConnect: () => Promise<void> | ||
| 55 | } | ||
| 56 | |||
| 57 | export interface UseRequests { | ||
| 58 | calls: RequestResponse[] | ||
| 59 | readyState: ReadyState | ||
| 60 | } | ||
| 61 | |||
| 62 | export default function useRequests({ onConnect }: useRequestsProps): UseRequests { | ||
| 42 | const wsHost = useMemo(getHost, []); | 63 | const wsHost = useMemo(getHost, []); |
| 43 | const connect = useCallback(() => new WebSocket(`ws://${wsHost}/inspect/`), [wsHost]) | ||
| 44 | 64 | ||
| 45 | const [requests, setRequests] = useState<RequestPayload[]>([]); | 65 | const [requests, setRequests] = useState<RequestPayload[]>([]); |
| 46 | const [responses, setResponses] = useState<ResponsePayload[]>([]); | 66 | const [responses, setResponses] = useState<ResponsePayload[]>([]); |
| 47 | const [ws, setWs] = useState(() => connect()); | 67 | |
| 68 | |||
| 69 | const connect = useCallback(() => ( | ||
| 70 | new WebSocket(`ws://${wsHost}/inspect/`) | ||
| 71 | ), [wsHost]); | ||
| 72 | |||
| 73 | const [ws, setWs] = useState<WebSocket>(() => connect()); | ||
| 74 | const [readyState, setReadyState] = useState<ReadyState>(ws.readyState); | ||
| 48 | 75 | ||
| 49 | useEffect(() => { | 76 | useEffect(() => { |
| 50 | const reconnect = () => setWs(() => connect()) | 77 | setReadyState(ws.readyState); |
| 78 | |||
| 79 | const onClose = () => { | ||
| 80 | setReadyState(ws.readyState); | ||
| 81 | setWs(connect()); | ||
| 82 | } | ||
| 83 | const onOpen = () => { | ||
| 84 | onConnect(); | ||
| 85 | setReadyState(ws.readyState); | ||
| 86 | } | ||
| 51 | const onMessage = ({ data }) => { | 87 | const onMessage = ({ data }) => { |
| 52 | const { type, payload } = JSON.parse(data) as Request | Response | 88 | const { type, payload } = JSON.parse(data) as Historic | Request | Response |
| 53 | 89 | ||
| 54 | switch (type) { | 90 | switch (type) { |
| 91 | case 'historic': | ||
| 92 | const requests = (payload as (Request | Response)[]).filter(({ type }) => type === 'request'); | ||
| 93 | const responses = (payload as (Request | Response)[]).filter(({ type }) => type === 'response'); | ||
| 94 | setRequests((rqs) => [...rqs, ...requests.map(({ payload }) => payload as RequestPayload)]); | ||
| 95 | setResponses((rps) => [...rps, ...responses.map(({ payload }) => payload as ResponsePayload)]); | ||
| 96 | break | ||
| 55 | case 'request': | 97 | case 'request': |
| 56 | setRequests((rqs) => [...rqs, payload as RequestPayload]) | 98 | setRequests((rqs) => [...rqs, payload as RequestPayload]) |
| 57 | break | 99 | break |
| @@ -62,16 +104,21 @@ export default function useRequests(): RequestResponse[] { | |||
| 62 | } | 104 | } |
| 63 | 105 | ||
| 64 | ws.addEventListener('message', onMessage) | 106 | ws.addEventListener('message', onMessage) |
| 65 | ws.addEventListener('close', reconnect) | 107 | ws.addEventListener('close', onClose) |
| 108 | ws.addEventListener('open', onOpen) | ||
| 66 | 109 | ||
| 67 | return () => { | 110 | return () => { |
| 68 | ws.removeEventListener('message', onMessage); | 111 | ws.removeEventListener('message', onMessage); |
| 69 | ws.removeEventListener('close', reconnect); | 112 | ws.removeEventListener('close', onClose); |
| 113 | ws.removeEventListener('open', onOpen) | ||
| 70 | } | 114 | } |
| 71 | }, [ws]) | 115 | }, [ws]) |
| 72 | 116 | ||
| 73 | return useMemo<RequestResponse[]>(() => requests.map((request) => ({ | 117 | return { |
| 74 | request: request, | 118 | calls: useMemo<RequestResponse[]>(() => requests.map((request) => ({ |
| 75 | response: responses.find(({ id }) => id === request.id) | 119 | request: request, |
| 76 | })), [requests, responses]) | 120 | response: responses.find(({id}) => id === request.id) |
| 121 | })), [requests, responses]), | ||
| 122 | readyState, | ||
| 123 | } | ||
| 77 | } | 124 | } |
diff --git a/ttun/__main__.py b/ttun/__main__.py index 1a3ca69..52bf2fb 100644 --- a/ttun/__main__.py +++ b/ttun/__main__.py | |||
| @@ -42,13 +42,25 @@ def main(): | |||
| 42 | 42 | ||
| 43 | loop.run_until_complete(client.connect()) | 43 | loop.run_until_complete(client.connect()) |
| 44 | 44 | ||
| 45 | server = Server(config=client.config, on_resend=client.proxyRequest) | 45 | def print_info(server: Server): |
| 46 | print('Tunnel created:') | ||
| 47 | print(f'{client.config["url"]} -> http://localhost:{args.port}') | ||
| 48 | print('') | ||
| 49 | print(f'Inspect requests:') | ||
| 50 | print(f'http://localhost:{server.port}') | ||
| 51 | |||
| 52 | server = Server( | ||
| 53 | config=client.config, | ||
| 54 | on_resend=client.proxyRequest, | ||
| 55 | on_started=print_info, | ||
| 56 | ) | ||
| 57 | |||
| 46 | tasks = { | 58 | tasks = { |
