aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-05-09 12:34:08 +0000
committerEmiliano Ciavatta2020-05-09 12:34:08 +0000
commit6006d47c1f3397862bb13c782d66689067135bdb (patch)
treea86e5a275063485cc93a5d8b7077ec119900037d
parentdee7d7dfcbec7ef4475896935873f04d4df0d40f (diff)
Add infinite scroll to connections, implement connection actions
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/src/colors.scss2
-rw-r--r--frontend/src/components/Connection.js104
-rw-r--r--frontend/src/components/Connection.scss26
-rw-r--r--frontend/src/components/ConnectionContent.js16
-rw-r--r--frontend/src/index.scss4
-rw-r--r--frontend/src/utils.js9
-rw-r--r--frontend/src/views/App.js10
-rw-r--r--frontend/src/views/Connections.js94
-rw-r--r--frontend/src/views/Footer.scss2
-rw-r--r--frontend/src/views/Header.js6
-rw-r--r--frontend/src/views/Header.scss2
-rw-r--r--frontend/src/views/MainPane.js10
-rw-r--r--frontend/src/views/MainPane.scss3
-rw-r--r--frontend/src/views/Services.js16
-rw-r--r--frontend/yarn.lock2
16 files changed, 242 insertions, 65 deletions
diff --git a/frontend/package.json b/frontend/package.json
index a91f51d..56b11c7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,7 @@
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
"react-dom": "^16.13.1",
+ "react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"typed.js": "^2.0.11"
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>
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 81dc71a..8628e68 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -8989,7 +8989,7 @@ react-router-dom@^5.1.2:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
-react-router@5.1.2:
+react-router@5.1.2, react-router@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==