You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
import React from 'react'
|
|
import Table from 'react-bootstrap/Table'
|
|
import Form from 'react-bootstrap/Form'
|
|
import Button from 'react-bootstrap/Button'
|
|
import './App.css'
|
|
|
|
import BreakPoint from './components/BreakPoint'
|
|
import FlowPreview from './components/FlowPreview'
|
|
import ViewFlow from './components/ViewFlow'
|
|
|
|
import { FlowManager } from './flow'
|
|
import { parseMessage, SendMessageType, buildMessageMeta, Flow, MessageType } from './message'
|
|
import { isInViewPort } from './utils'
|
|
|
|
interface IState {
|
|
flows: Flow[]
|
|
flow: Flow | null
|
|
flowTab: 'Headers' | 'Preview' | 'Response'
|
|
wsStatus: 'open' | 'close' | 'connecting'
|
|
}
|
|
|
|
const wsReconnIntervals = [1, 1, 2, 2, 4, 4, 8, 8, 16, 16, 32, 32]
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
interface IProps {}
|
|
|
|
class App extends React.Component<IProps, IState> {
|
|
private flowMgr: FlowManager
|
|
private ws: WebSocket | null
|
|
private wsUnmountClose: boolean
|
|
private tableBottomRef: React.RefObject<HTMLDivElement>
|
|
|
|
private wsReconnCount = -1
|
|
|
|
constructor(props: IProps) {
|
|
super(props)
|
|
|
|
this.flowMgr = new FlowManager()
|
|
|
|
this.state = {
|
|
flows: this.flowMgr.showList(),
|
|
flow: null,
|
|
flowTab: 'Headers', // Headers, Preview, Response
|
|
wsStatus: 'close',
|
|
}
|
|
|
|
this.ws = null
|
|
this.wsUnmountClose = false
|
|
this.tableBottomRef = React.createRef<HTMLDivElement>()
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initWs()
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.ws) {
|
|
this.wsUnmountClose = true
|
|
this.ws.close()
|
|
this.ws = null
|
|
}
|
|
}
|
|
|
|
initWs() {
|
|
if (this.ws) return
|
|
|
|
this.setState({ wsStatus: 'connecting' })
|
|
|
|
let host
|
|
if (process.env.NODE_ENV === 'development') {
|
|
host = 'localhost:9081'
|
|
} else {
|
|
host = new URL(document.URL).host
|
|
}
|
|
this.ws = new WebSocket(`ws://${host}/echo`)
|
|
this.ws.binaryType = 'arraybuffer'
|
|
|
|
this.ws.onopen = () => {
|
|
this.wsReconnCount = -1
|
|
this.setState({ wsStatus: 'open' })
|
|
}
|
|
|
|
this.ws.onerror = evt => {
|
|
console.error('ERROR:', evt)
|
|
this.ws?.close()
|
|
}
|
|
|
|
this.ws.onclose = () => {
|
|
this.setState({ wsStatus: 'close' })
|
|
if (this.wsUnmountClose) return
|
|
|
|
this.wsReconnCount++
|
|
this.ws = null
|
|
const waitSeconds = wsReconnIntervals[this.wsReconnCount] || wsReconnIntervals[wsReconnIntervals.length - 1]
|
|
console.info(`will reconnect after ${waitSeconds} seconds`)
|
|
setTimeout(() => {
|
|
this.initWs()
|
|
}, waitSeconds * 1000)
|
|
}
|
|
|
|
this.ws.onmessage = evt => {
|
|
const msg = parseMessage(evt.data)
|
|
if (!msg) {
|
|
console.error('parse error:', evt.data)
|
|
return
|
|
}
|
|
// console.log('msg:', msg)
|
|
|
|
if (msg.type === MessageType.REQUEST) {
|
|
const flow = new Flow(msg)
|
|
this.flowMgr.add(flow)
|
|
|
|
let shouldScroll = false
|
|
if (this.tableBottomRef?.current && isInViewPort(this.tableBottomRef.current)) {
|
|
shouldScroll = true
|
|
}
|
|
this.setState({ flows: this.flowMgr.showList() }, () => {
|
|
if (shouldScroll) {
|
|
this.tableBottomRef?.current?.scrollIntoView({ behavior: 'auto' })
|
|
}
|
|
})
|
|
}
|
|
else if (msg.type === MessageType.REQUEST_BODY) {
|
|
const flow = this.flowMgr.get(msg.id)
|
|
if (!flow) return
|
|
flow.addRequestBody(msg)
|
|
this.setState({ flows: this.state.flows })
|
|
}
|
|
else if (msg.type === MessageType.RESPONSE) {
|
|
const flow = this.flowMgr.get(msg.id)
|
|
if (!flow) return
|
|
flow.addResponse(msg)
|
|
this.setState({ flows: this.state.flows })
|
|
}
|
|
else if (msg.type === MessageType.RESPONSE_BODY) {
|
|
const flow = this.flowMgr.get(msg.id)
|
|
if (!flow || !flow.response) return
|
|
flow.addResponseBody(msg)
|
|
this.setState({ flows: this.state.flows })
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { flows } = this.state
|
|
return (
|
|
<div className="main-table-wrap">
|
|
<div className="top-control">
|
|
<div><Button size="sm" onClick={() => {
|
|
this.flowMgr.clear()
|
|
this.setState({ flows: this.flowMgr.showList(), flow: null })
|
|
}}>Clear</Button></div>
|
|
<div>
|
|
<Form.Control
|
|
size="sm" placeholder="Filter"
|
|
onChange={(e) => {
|
|
const value = e.target.value
|
|
this.flowMgr.changeFilterLazy(value, () => {
|
|
this.setState({ flows: this.flowMgr.showList() })
|
|
})
|
|
}}
|
|
>
|
|
</Form.Control>
|
|
</div>
|
|
|
|
<BreakPoint onSave={rules => {
|
|
const msg = buildMessageMeta(SendMessageType.CHANGE_BREAK_POINT_RULES, rules)
|
|
if (this.ws) this.ws.send(msg)
|
|
}} />
|
|
|
|
<span>status: {this.state.wsStatus}</span>
|
|
</div>
|
|
|
|
<div className="table-wrap-div">
|
|
<Table striped bordered size="sm" style={{ tableLayout: 'fixed' }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: '50px' }}>No</th>
|
|
<th style={{ width: '80px' }}>Method</th>
|
|
<th style={{ width: '200px' }}>Host</th>
|
|
<th style={{ width: 'auto' }}>Path</th>
|
|
<th style={{ width: '150px' }}>Type</th>
|
|
<th style={{ width: '80px' }}>Status</th>
|
|
<th style={{ width: '90px' }}>Size</th>
|
|
<th style={{ width: '90px' }}>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{
|
|
flows.map(f => {
|
|
const fp = f.preview()
|
|
|
|
return (
|
|
<FlowPreview
|
|
key={fp.id}
|
|
flow={fp}
|
|
isSelected={(this.state.flow && this.state.flow.id === fp.id) ? true : false}
|
|
onShowDetail={() => {
|
|
this.setState({ flow: f })
|
|
}}
|
|
/>
|
|
)
|
|
})
|
|
}
|
|
</tbody>
|
|
</Table>
|
|
<div ref={this.tableBottomRef} id="hidden-bottom" style={{ height: '0px', visibility: 'hidden', marginBottom: '1px' }}></div>
|
|
</div>
|
|
|
|
<ViewFlow
|
|
flow={this.state.flow}
|
|
onClose={() => { this.setState({ flow: null }) }}
|
|
onReRenderFlows={() => { this.setState({ flows: this.state.flows }) }}
|
|
onMessage={msg => { if (this.ws) this.ws.send(msg) }}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default App
|