diff options
author | Emiliano Ciavatta | 2020-05-09 12:34:08 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-05-09 12:34:08 +0000 |
commit | 6006d47c1f3397862bb13c782d66689067135bdb (patch) | |
tree | a86e5a275063485cc93a5d8b7077ec119900037d /frontend/src | |
parent | dee7d7dfcbec7ef4475896935873f04d4df0d40f (diff) |
Add infinite scroll to connections, implement connection actions
Diffstat (limited to 'frontend/src')
-rw-r--r-- | frontend/src/colors.scss | 2 | ||||
-rw-r--r-- | frontend/src/components/Connection.js | 104 | ||||
-rw-r--r-- | frontend/src/components/Connection.scss | 26 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.js | 16 | ||||
-rw-r--r-- | frontend/src/index.scss | 4 | ||||
-rw-r--r-- | frontend/src/utils.js | 9 | ||||
-rw-r--r-- | frontend/src/views/App.js | 10 | ||||
-rw-r--r-- | frontend/src/views/Connections.js | 94 | ||||
-rw-r--r-- | frontend/src/views/Footer.scss | 2 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 6 | ||||
-rw-r--r-- | frontend/src/views/Header.scss | 2 | ||||
-rw-r--r-- | frontend/src/views/MainPane.js | 10 | ||||
-rw-r--r-- | frontend/src/views/MainPane.scss | 3 | ||||
-rw-r--r-- | frontend/src/views/Services.js | 16 |
14 files changed, 240 insertions, 64 deletions
diff --git a/frontend/src/colors.scss b/frontend/src/colors.scss index 75f1828..244e869 100644 --- a/frontend/src/colors.scss +++ b/frontend/src/colors.scss @@ -1,4 +1,4 @@ -$color-primary-0: #17223B; // Main Primary color */ +$color-primary-0: #17223B; // Main Primary color */ $color-primary-1: #040B1B; $color-primary-2: #0F192E; $color-primary-3: #1D2841; diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index 13b394a..8121d51 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -1,8 +1,50 @@ import React, {Component} from 'react'; import './Connection.scss'; -import {Button, OverlayTrigger, Tooltip} from "react-bootstrap"; +import axios from 'axios' +import {Button, Form, OverlayTrigger, Popover} from "react-bootstrap"; class Connection extends Component { + + constructor(props) { + super(props); + this.state = { + update: false, + copiedMessage: false + }; + + this.copyTextarea = React.createRef(); + this.handleAction = this.handleAction.bind(this); + } + + handleAction(name) { + if (name === "hide") { + const enabled = !this.props.data.hidden; + axios.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`) + .then(res => { + if (res.status === 202) { + this.props.onEnabled(!enabled); + this.setState({update: true}); + } + }); + } + if (name === "mark") { + const marked = this.props.data.marked; + axios.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`) + .then(res => { + if (res.status === 202) { + this.props.onMarked(!marked); + this.setState({update: true}); + } + }); + } + if (name === "copy") { + this.copyTextarea.current.select(); + document.execCommand('copy'); + this.setState({copiedMessage: true}); + setTimeout(() => this.setState({copiedMessage: false}), 3000); + } + } + render() { let conn = this.props.data; let serviceName = "/dev/null"; @@ -13,16 +55,37 @@ class Connection extends Component { } let startedAt = new Date(conn.started_at); let closedAt = new Date(conn.closed_at); + let processedAt = new Date(conn.processed_at); let duration = ((closedAt - startedAt) / 1000).toFixed(3); - let timeInfo = `Started at ${startedAt}\nClosed at ${closedAt}\nProcessed at ${new Date(conn.processed_at)}`; + let timeInfo = <div> + <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/> + <span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/> + <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span> + </div>; let classes = "connection"; if (this.props.selected) { classes += " connection-selected"; } - if (conn.marked){ - classes += " connection-marked"; - } + + const popoverFor = function (name, content) { + return <Popover id={`popover-${name}-${conn.id}`} className="connection-popover"> + <Popover.Content> + {content} + </Popover.Content> + </Popover>; + }; + + const commentPopoverContent = <div> + <span>Click to <strong>{conn.comment.length > 0 ? "edit" : "add"}</strong> comment</span> + {conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>} + </div>; + + const copyPopoverContent = <div> + {this.state.copiedMessage ? <span><strong>Copied!</strong></span> : + <span>Click to <strong>copy</strong> the connection id</span>} + <Form.Control as="textarea" readOnly={true} rows={1} defaultValue={conn.id} ref={this.copyTextarea}/> + </div>; return ( <tr className={classes}> @@ -38,24 +101,39 @@ class Connection extends Component { <td className="clickable" onClick={() => this.props.onSelected()}>{conn.ip_dst}</td> <td className="clickable" onClick={() => this.props.onSelected()}>{conn.port_dst}</td> <td className="clickable" onClick={() => this.props.onSelected()}> - {/*<OverlayTrigger placement="top" overlay={<Tooltip id={`tooltip-${conn.id}`}>{timeInfo}</Tooltip>}>*/} + <OverlayTrigger trigger={["focus", "hover"]} placement="right" + overlay={popoverFor("duration", timeInfo)}> <span className="test-tooltip">{duration}s</span> - {/*</OverlayTrigger>*/} + </OverlayTrigger> </td> <td className="clickable" onClick={() => this.props.onSelected()}>{conn.client_bytes}</td> <td className="clickable" onClick={() => this.props.onSelected()}>{conn.server_bytes}</td> <td> - <span className="connection-icon connection-hide">%</span> - <span className="connection-icon connection-mark">!!</span> - <span className="connection-icon connection-comment">@</span> - <span className="connection-icon connection-link">#</span> + <OverlayTrigger trigger={["focus", "hover"]} placement="right" + overlay={popoverFor("hide", <span>Hide this connection from the list</span>)}> + <span className={"connection-icon" + (conn.hidden ? " icon-enabled" : "")} + onClick={() => this.handleAction("hide")}>%</span> + </OverlayTrigger> + <OverlayTrigger trigger={["focus", "hover"]} placement="right" + overlay={popoverFor("hide", <span>Mark this connection</span>)}> + <span className={"connection-icon" + (conn.marked ? " icon-enabled" : "")} + onClick={() => this.handleAction("mark")}>!!</span> + </OverlayTrigger> + <OverlayTrigger trigger={["focus", "hover"]} placement="right" + overlay={popoverFor("comment", commentPopoverContent)}> + <span className={"connection-icon" + (conn.comment ? " icon-enabled" : "")} + onClick={() => this.handleAction("comment")}>@</span> + </OverlayTrigger> + <OverlayTrigger trigger={["focus", "hover"]} placement="right" + overlay={popoverFor("copy", copyPopoverContent)}> + <span className="connection-icon" + onClick={() => this.handleAction("copy")}>#</span> + </OverlayTrigger> </td> - </tr> ); } } - export default Connection; diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/Connection.scss index d27ebc8..5ad195d 100644 --- a/frontend/src/components/Connection.scss +++ b/frontend/src/components/Connection.scss @@ -9,10 +9,6 @@ vertical-align: middle; } - &.connection-marked { - border-right: 5px solid $color-secondary-2; - } - .connection-service { .btn { width: 100%; @@ -31,6 +27,10 @@ cursor: pointer; } + .connection-icon.icon-enabled { + color: $color-secondary-2; + } + &:hover { background-color: $color-primary-2; } @@ -41,3 +41,21 @@ } +.connection-popover { + background-color: $color-primary-4; + border: none; + + .popover-body { + color: $color-primary-1; + + textarea { + background-color: $color-primary-3; + font-size: 12px; + width: 200px; + } + } + + .arrow::after { + border-right-color: $color-primary-4; + } +} diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index bd35b5c..db63cbe 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -1,18 +1,13 @@ import React, {Component} from 'react'; import './ConnectionContent.scss'; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import { - -} from '@fortawesome/free-solid-svg-icons' -import {useParams} from "react-router-dom"; -import { Dropdown } from 'react-bootstrap'; +import {Dropdown} from 'react-bootstrap'; class ConnectionContent extends Component { render() { - let content = this.props.connectionPayload + let content = this.props.connectionPayload; if (content === undefined) { - return <div>nope</div> + return <div>nope</div>; } let payload = content.map(c => @@ -20,10 +15,7 @@ class ConnectionContent extends Component { {c.content} </span> - ) - - - + ); return ( <div className="connection-content"> diff --git a/frontend/src/index.scss b/frontend/src/index.scss index 044d150..b7f0cf4 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap'); -@import-normalize; + +@import-normalize ; @import 'colors.scss'; @@ -131,6 +132,7 @@ input.form-control, textarea.form-control { background-color: $color-primary-2; border: none; color: $color-primary-4; + font-family: 'Fira Code', monospace; &:focus { background-color: $color-primary-1; diff --git a/frontend/src/utils.js b/frontend/src/utils.js index b72ada7..db9405c 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -3,3 +3,12 @@ export function createCurlCommand(subCommand, data) { return `curl --request PUT \\\n --url ${full}/api${subCommand} \\\n ` + `--header 'content-type: application/json' \\\n --data '${JSON.stringify(data)}'`; } + +export function objectToQueryString(obj) { + let str = []; + for (let p in obj) + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + return str.join("&"); +} diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index 2b444a2..e3119aa 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -3,7 +3,7 @@ import Header from "./Header"; import './App.scss'; import MainPane from "./MainPane"; import Footer from "./Footer"; -import {Route, BrowserRouter as Router, Switch} from "react-router-dom"; +import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; import Services from "./Services"; class App extends Component { @@ -16,9 +16,9 @@ class App extends Component { } render() { - let modal = "" + let modal = ""; if (this.state.servicesShow) { - modal = <Services onHide={() => this.setState({servicesShow: false})} /> + modal = <Services onHide={() => this.setState({servicesShow: false})}/>; } return ( @@ -26,8 +26,8 @@ class App extends Component { <Router> <Header onOpenServices={() => this.setState({servicesShow: true})}/> <Switch> - <Route path="/connections/:id" children={<MainPane/>} /> - <Route path="/" children={<MainPane/>} /> + <Route path="/connections/:id" children={<MainPane/>}/> + <Route path="/" children={<MainPane/>}/> </Switch> {modal} <Footer/> diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index fa7798e..400f4e0 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -4,29 +4,107 @@ import axios from 'axios' import Connection from "../components/Connection"; import Table from 'react-bootstrap/Table'; import {Redirect} from 'react-router'; +import {objectToQueryString} from "../utils"; class Connections extends Component { + constructor(props) { super(props); this.state = { + loading: false, connections: [], + firstConnection: null, + lastConnection: null, + showHidden: false }; - } + this.scrollTopThreashold = 0.00001; + this.scrollBottomThreashold = 0.99999; + this.maxConnections = 500; + this.queryLimit = 50; + this.handleScroll = this.handleScroll.bind(this); + } + componentDidMount() { - axios.get("/api/connections").then(res => this.setState({connections: res.data})) + this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}); + } + + handleScroll(e) { + let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight); + if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) { + this.loadConnections({ + from: this.state.lastConnection.id, limit: this.queryLimit, + hidden: this.state.showHidden + }); + } + if (!this.state.loading && relativeScroll < this.scrollTopThreashold) { + this.loadConnections({ + to: this.state.firstConnection.id, limit: this.queryLimit, + hidden: this.state.showHidden + }); + } + + } + + async loadConnections(params) { + let url = "/api/connections"; + if (params !== undefined) { + url += "?" + objectToQueryString(params); + } + this.setState({loading: true}); + let res = await axios.get(url); + + let connections = this.state.connections; + let firstConnection = this.state.firstConnection; + let lastConnection = this.state.lastConnection; + if (res.data.length > 0) { + if (params !== undefined && params.from !== undefined) { + connections = this.state.connections.concat(res.data); + lastConnection = connections[connections.length - 1]; + if (connections.length > this.maxConnections) { + connections = connections.slice(connections.length - this.maxConnections, + connections.length - 1); + firstConnection = connections[0]; + } + } else if (params !== undefined && params.to !== undefined) { + connections = res.data.concat(this.state.connections); + firstConnection = connections[0]; + if (connections.length > this.maxConnections) { + connections = connections.slice(0, this.maxConnections); + lastConnection = connections[this.maxConnections - 1]; + } + } else { + connections = res.data; + firstConnection = connections[0]; + lastConnection = connections[connections.length - 1]; + } + } + + this.setState({ + loading: false, + connections: connections, + firstConnection: firstConnection, + lastConnection: lastConnection + }); } + render() { - let redirect = "" + let redirect = ""; if (this.state.selected) { - redirect = <Redirect push to={"/connections/" + this.state.selected} />; + redirect = <Redirect push to={"/connections/" + this.state.selected}/>; } - return ( + let loading = null; + if (this.state.loading) { + loading = <tr> + <td colSpan={9}>Loading...</td> + </tr>; + } - <div className="connections"> + return ( + <div className="connections" onScroll={this.handleScroll}> <div className="connections-header-padding"/> <Table borderless size="sm"> <thead> @@ -46,9 +124,11 @@ class Connections extends Component { { this.state.connections.map(c => <Connection key={c.id} data={c} onSelected={() => this.setState({selected: c.id})} - selected={this.state.selected === c.id}/> + selected={this.state.selected === c.id} onMarked={marked => c.marked = marked} + onEnabled={enabled => c.hidden = !enabled}/> ) } + {loading} </tbody> </Table> diff --git a/frontend/src/views/Footer.scss b/frontend/src/views/Footer.scss index 9a8cacb..c89f971 100644 --- a/frontend/src/views/Footer.scss +++ b/frontend/src/views/Footer.scss @@ -3,7 +3,7 @@ .footer { padding: 15px 30px; - >.row { + > .row { background-color: $color-primary-0; } diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index ac272e6..f96b036 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -31,7 +31,9 @@ class Header extends Component { <div className="row"> <div className="col"> <h1 className="header-title type-wrap"> - <span style={{ whiteSpace: 'pre' }} ref={(el) => { this.el = el; }} /> + <span style={{whiteSpace: 'pre'}} ref={(el) => { + this.el = el; + }}/> </h1> </div> <div className="col"> @@ -45,7 +47,7 @@ class Header extends Component { </div> </div> </header> - ) + ); } } diff --git a/frontend/src/views/Header.scss b/frontend/src/views/Header.scss index 16c2dbd..0c524e6 100644 --- a/frontend/src/views/Header.scss +++ b/frontend/src/views/Header.scss @@ -3,7 +3,7 @@ .header { padding: 15px 30px; - >.row { + > .row { background-color: $color-primary-0; } diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js index 0fc083e..e84f94c 100644 --- a/frontend/src/views/MainPane.js +++ b/frontend/src/views/MainPane.js @@ -19,9 +19,7 @@ class MainPane extends Component { const id = this.props.match.params.id; this.setState({id: id}); - axios.get(`/api/streams/${id}`).then(res => this.setState({connectionContent: res.data})) - - + axios.get(`/api/streams/${id}`).then(res => this.setState({connectionContent: res.data})); } } @@ -30,12 +28,8 @@ class MainPane extends Component { const id = this.props.match.params.id; this.setState({id: id}); - axios.get(`/api/streams/${id}`).then(res => this.setState({connectionContent: res.data})) - - + axios.get(`/api/streams/${id}`).then(res => this.setState({connectionContent: res.data})); } - - } render() { diff --git a/frontend/src/views/MainPane.scss b/frontend/src/views/MainPane.scss index c1a2f7f..d0d25b1 100644 --- a/frontend/src/views/MainPane.scss +++ b/frontend/src/views/MainPane.scss @@ -5,5 +5,4 @@ position: relative; } - -}
\ No newline at end of file +} diff --git a/frontend/src/views/Services.js b/frontend/src/views/Services.js index 66d99c6..b95b01c 100644 --- a/frontend/src/views/Services.js +++ b/frontend/src/views/Services.js @@ -112,7 +112,7 @@ class Services extends Component { let rows = Object.values(this.state.services).map(s => <tr> <td><Button variant="btn-edit" size="sm" - onClick={() => this.editService(s)} style={{ "backgroundColor": s.color }}>edit</Button></td> + onClick={() => this.editService(s)} style={{"backgroundColor": s.color}}>edit</Button></td> <td>{s.port}</td> <td>{s.name}</td> </tr> @@ -149,7 +149,7 @@ class Services extends Component { </tr> </thead> <tbody> - {rows} + {rows} </tbody> </Table> </Col> @@ -157,7 +157,7 @@ class Services extends Component { <Form> <Form.Group controlId="servicePort"> <Form.Label>port:</Form.Label> - <Form.Control type="text" onChange={this.portChanged} value={this.state.port} /> + <Form.Control type="text" onChange={this.portChanged} value={this.state.port}/> </Form.Group> <Form.Group controlId="serviceName"> @@ -168,16 +168,17 @@ class Services extends Component { <Form.Group controlId="serviceColor"> <Form.Label>color:</Form.Label> <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(0,8)} + {colorButtons.slice(0, 8)} </ButtonGroup> <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(8,18)} + {colorButtons.slice(8, 18)} </ButtonGroup> </Form.Group> <Form.Group controlId="serviceNotes"> <Form.Label>notes:</Form.Label> - <Form.Control as="textarea" rows={3} onChange={this.notesChanged} value={this.state.notes} /> + <Form.Control as="textarea" rows={3} onChange={this.notesChanged} + value={this.state.notes}/> </Form.Group> </Form> @@ -189,7 +190,8 @@ class Services extends Component { <Row> <Col md={12}> <InputGroup> - <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} value={output}/> + <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} + value={output}/> </InputGroup> </Col> |