diff options
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/src/components/Connection.js | 7 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.js | 163 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.scss | 105 | ||||
-rw-r--r-- | frontend/src/components/MessageAction.js | 52 | ||||
-rw-r--r-- | frontend/src/components/MessageAction.scss | 8 | ||||
-rw-r--r-- | frontend/src/utils.js | 5 |
6 files changed, 299 insertions, 41 deletions
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index e41f542..93c6438 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -57,6 +57,11 @@ class Connection extends Component { let closedAt = new Date(conn.closed_at); let processedAt = new Date(conn.processed_at); let duration = ((closedAt - startedAt) / 1000).toFixed(3); + if (duration > 1000 || duration < -1000) { + duration = "∞"; + } else { + duration += "s"; + } let timeInfo = <div> <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/> <span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/> @@ -106,7 +111,7 @@ class Connection extends Component { <td className="clickable" onClick={this.props.onSelected}> <OverlayTrigger trigger={["focus", "hover"]} placement="right" overlay={popoverFor("duration", timeInfo)}> - <span className="test-tooltip">{duration}s</span> + <span className="test-tooltip">{duration}</span> </OverlayTrigger> </td> <td className="clickable" onClick={this.props.onSelected}>{conn.client_bytes}</td> diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 905a56d..20ec92b 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -1,7 +1,10 @@ import React, {Component} from 'react'; import './ConnectionContent.scss'; -import {Dropdown} from 'react-bootstrap'; +import {Button, Dropdown, Row} from 'react-bootstrap'; import axios from 'axios'; +import MessageAction from "./MessageAction"; + +const classNames = require('classnames'); class ConnectionContent extends Component { @@ -10,7 +13,9 @@ class ConnectionContent extends Component { this.state = { loading: false, connectionContent: null, - format: "default" + format: "default", + tryParse: true, + messageActionDialog: null }; this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]; @@ -37,44 +42,148 @@ class ConnectionContent extends Component { } } + tryParseConnectionMessage(connectionMessage) { + if (connectionMessage.metadata == null) { + return connectionMessage.content; + } + if (connectionMessage["is_metadata_continuation"]) { + return <span style={{"fontSize": "12px"}}>**already parsed in previous messages**</span>; + } + + let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => + <p><strong>{key}</strong>: {value}</p> + ); + + let m = connectionMessage.metadata; + switch (m.type) { + case "http-request": + let url = <i><u><a href={"http://" + m.host + m.url} target="_blank" + rel="noopener noreferrer">{m.host}{m.url}</a></u></i>; + return <span className="type-http-request"> + <p style={{"marginBottom": "7px"}}><strong>{m.method}</strong> {url} {m.protocol}</p> + {unrollMap(m.headers)} + <div style={{"margin": "20px 0"}}>{m.body}</div> + {unrollMap(m.trailers)} + </span>; + case "http-response": + return <span className="type-http-response"> + <p style={{"marginBottom": "7px"}}>{m.protocol} <strong>{m.status}</strong></p> + {unrollMap(m.headers)} + <div style={{"margin": "20px 0"}}>{m.body}</div> + {unrollMap(m.trailers)} + </span>; + default: + return connectionMessage.content; + } + } + + connectionsActions(connectionMessage) { + if (connectionMessage.metadata == null || connectionMessage.metadata["reproducers"] === undefined) { + return null; + } + + return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => + <Button size="sm" key={actionName + "_button"} onClick={() => { + this.setState({ + messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue} + onHide={() => this.setState({messageActionDialog: null})}/> + }); + }}>{actionName}</Button> + ); + } + render() { let content = this.state.connectionContent; - if (content === null) { - return <div>nope</div>; + if (content == null) { + return <div>select a connection to view</div>; } let payload = content.map((c, i) => - <span key={`content-${i}`} className={c.from_client ? "from-client" : "from-server"}> - {c.content} - </span> + <div key={`content-${i}`} + className={classNames("connection-message", c.from_client ? "from-client" : "from-server")}> + <div className="connection-message-header container-fluid"> + <div className="row"> + <div className="connection-message-info col"> + <span><strong>offset</strong>: {c.index}</span> | <span><strong>timestamp</strong>: {c.timestamp} + </span> | <span><strong>retransmitted</strong>: {c["is_retransmitted"] ? "yes" : "no"}</span> + </div> + <div className="connection-message-actions col-auto">{this.connectionsActions(c)}</div> + </div> + </div> + <div className="connection-message-label">{c.from_client ? "client" : "server"}</div> + <div + className={classNames("message-content", this.state.decoded ? "message-parsed" : "message-original")}> + {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content} + </div> + </div> ); return ( <div className="connection-content"> - <div className="connection-content-options"> - <Dropdown onSelect={this.setFormat} > - <Dropdown.Toggle size="sm" id="dropdown-basic"> - format - </Dropdown.Toggle> - - <Dropdown.Menu> - <Dropdown.Item eventKey="default" active={this.state.format === "default"}>plain</Dropdown.Item> - <Dropdown.Item eventKey="hex" active={this.state.format === "hex"}>hex</Dropdown.Item> - <Dropdown.Item eventKey="hexdump" active={this.state.format === "hexdump"}>hexdump</Dropdown.Item> - <Dropdown.Item eventKey="base32" active={this.state.format === "base32"}>base32</Dropdown.Item> - <Dropdown.Item eventKey="base64" active={this.state.format === "base64"}>base64</Dropdown.Item> - <Dropdown.Item eventKey="ascii" active={this.state.format === "ascii"}>ascii</Dropdown.Item> - <Dropdown.Item eventKey="binary" active={this.state.format === "binary"}>binary</Dropdown.Item> - <Dropdown.Item eventKey="decimal" active={this.state.format === "decimal"}>decimal</Dropdown.Item> - <Dropdown.Item eventKey="octal" active={this.state.format === "octal"}>octal</Dropdown.Item> - </Dropdown.Menu> - </Dropdown> - - + <div className="connection-content-header container-fluid"> + <Row> + <div className="header-info col"> + <span><strong>flow</strong>: {this.props.connection.ip_src}:{this.props.connection.port_src} -> {this.props.connection.ip_dst}:{this.props.connection.port_dst}</span> + <span> | <strong>timestamp</strong>: {this.props.connection.started_at}</span> + </div> + <div className="header-actions col-auto"> + <Dropdown onSelect={this.setFormat}> + <Dropdown.Toggle size="sm" id="connection-content-format"> + format + </Dropdown.Toggle> + + <Dropdown.Menu> + <Dropdown.Item eventKey="default" + active={this.state.format === "default"}>plain</Dropdown.Item> + <Dropdown.Item eventKey="hex" + active={this.state.format === "hex"}>hex</Dropdown.Item> + <Dropdown.Item eventKey="hexdump" + active={this.state.format === "hexdump"}>hexdump</Dropdown.Item> + <Dropdown.Item eventKey="base32" + active={this.state.format === "base32"}>base32</Dropdown.Item> + <Dropdown.Item eventKey="base64" + active={this.state.format === "base64"}>base64</Dropdown.Item> + <Dropdown.Item eventKey="ascii" + active={this.state.format === "ascii"}>ascii</Dropdown.Item> + <Dropdown.Item eventKey="binary" + active={this.state.format === "binary"}>binary</Dropdown.Item> + <Dropdown.Item eventKey="decimal" + active={this.state.format === "decimal"}>decimal</Dropdown.Item> + <Dropdown.Item eventKey="octal" + active={this.state.format === "octal"}>octal</Dropdown.Item> + </Dropdown.Menu> + </Dropdown> + + <Dropdown> + <Dropdown.Toggle size="sm" id="connection-content-view"> + view_as + </Dropdown.Toggle> + + <Dropdown.Menu> + <Dropdown.Item eventKey="default" active={true}>default</Dropdown.Item> + </Dropdown.Menu> + + </Dropdown> + + <Dropdown> + <Dropdown.Toggle size="sm" id="connection-content-download"> + download_as + </Dropdown.Toggle> + + <Dropdown.Menu> + <Dropdown.Item eventKey="nl_separated">nl_separated</Dropdown.Item> + <Dropdown.Item eventKey="only_client">only_client</Dropdown.Item> + <Dropdown.Item eventKey="only_server">only_server</Dropdown.Item> + </Dropdown.Menu> + + </Dropdown> + </div> + </Row> </div> <pre>{payload}</pre> + {this.state.messageActionDialog} </div> ); } diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss index a1b4afd..8ee31ec 100644 --- a/frontend/src/components/ConnectionContent.scss +++ b/frontend/src/components/ConnectionContent.scss @@ -1,29 +1,108 @@ @import '../colors.scss'; .connection-content { - background-color: $color-primary-3; + background-color: $color-primary-0; + padding: 10px 10px 0; height: 100%; - overflow: auto; pre { - background-color: $color-primary-0; - padding: 10px 20px; word-break: break-word; - max-width: 100%; white-space: pre-wrap; - height: 100%; - } + overflow-x: hidden; + height: calc(100% - 31px); + padding: 0 10px; - .from-client { - color: #d4e0fc; + p { + margin: 0; + padding: 0; + } } - .from-server { - color: $color-secondary-4; + .connection-message { + border: 4px solid $color-primary-3; + border-top: 0; + margin: 10px 0; + position: relative; + + .connection-message-header { + background-color: $color-primary-3; + height: 25px; + + .connection-message-info { + font-size: 11px; + margin-left: -10px; + margin-top: 6px; + } + + .connection-message-actions { + margin-right: -18px; + display: none; + + button { + margin: 0 3px; + font-size: 11px; + padding: 5px; + } + } + } + + .message-content { + padding: 10px; + } - &:hover { + &:hover .connection-message-actions { + display: block; + } + + .connection-message-label { + position: absolute; background-color: $color-primary-3; - border-top: 1px solid $color-primary-1; + top: 0; + padding: 10px 0; + font-size: 12px; + + writing-mode: vertical-rl; + text-orientation: mixed; + } + + &.from-client { + color: $color-primary-4; + margin-right: 100px; + + .connection-message-label { + right: -22px; + } + } + + &.from-server { + color: $color-primary-4; + margin-left: 100px; + + .connection-message-label { + left: -22px; + transform: rotate(-180deg); + } + } + + } + + .connection-content-header { + background-color: $color-primary-2; + padding: 0; + height: 31px; + + .header-info { + padding-top: 5px; + padding-left: 20px; + font-size: 12px; + } + + .header-actions { + .dropdown { + display: inline-block; + } } } + + } diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js new file mode 100644 index 0000000..2c85d84 --- /dev/null +++ b/frontend/src/components/MessageAction.js @@ -0,0 +1,52 @@ +import React, {Component} from 'react'; +import './MessageAction.scss'; +import {Button, FormControl, InputGroup, Modal} from "react-bootstrap"; + +class MessageAction extends Component { + + constructor(props) { + super(props); + this.state = { + copyButtonText: "copy" + }; + this.actionValue = React.createRef(); + this.copyActionValue = this.copyActionValue.bind(this); + } + + copyActionValue() { + this.actionValue.current.select(); + document.execCommand('copy'); + this.setState({copyButtonText: "copied!"}); + setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + } + + render() { + return ( + <Modal + {...this.props} + show="true" + size="lg" + aria-labelledby="message-action-dialog" + centered + > + <Modal.Header> + <Modal.Title id="message-action-dialog"> + {this.props.actionName} + </Modal.Title> + </Modal.Header> + <Modal.Body> + <InputGroup> + <FormControl as="textarea" className="message-action-value" readOnly={true} + style={{"height": "300px"}} value={this.props.actionValue} ref={this.actionValue} /> + </InputGroup> + </Modal.Body> + <Modal.Footer className="dialog-footer"> + <Button variant="green" onClick={this.copyActionValue}>{this.state.copyButtonText}</Button> + <Button variant="red" onClick={this.props.onHide}>close</Button> + </Modal.Footer> + </Modal> + ); + } +} + +export default MessageAction; diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/MessageAction.scss new file mode 100644 index 0000000..f3a8772 --- /dev/null +++ b/frontend/src/components/MessageAction.scss @@ -0,0 +1,8 @@ +@import '../colors.scss'; + +.message-action-value { + font-size: 13px; + padding: 15px; + background-color: $color-primary-2; + color: $color-primary-4; +}
\ No newline at end of file diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 26c10d3..7381f69 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -59,3 +59,8 @@ export function timestampToTime(timestamp) { let seconds = "0" + d.getSeconds(); return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); } + +export function timestampToDateTime(timestamp) { + let d = new Date(timestamp); + return d.toLocaleDateString() + " " + d.toLocaleTimeString(); +} |