From 2b2b8e66e7244592672c283fe7bb5d9a1fd9da99 Mon Sep 17 00:00:00 2001
From: Emiliano Ciavatta
Date: Thu, 8 Oct 2020 11:16:38 +0200
Subject: Minor changes
---
application_router.go | 6 +-
connections_controller.go | 4 +-
frontend/src/components/Connection.js | 33 ++---
frontend/src/components/Connection.scss | 4 +
frontend/src/components/Notifications.js | 14 +-
frontend/src/components/Notifications.scss | 1 +
frontend/src/views/App.js | 4 +-
frontend/src/views/Connections.js | 44 ++++--
frontend/src/views/Footer.js | 176 -----------------------
frontend/src/views/Footer.scss | 22 ---
frontend/src/views/Header.js | 24 ++--
frontend/src/views/Timeline.js | 215 +++++++++++++++++++++++++++++
frontend/src/views/Timeline.scss | 22 +++
13 files changed, 313 insertions(+), 256 deletions(-)
delete mode 100644 frontend/src/views/Footer.js
delete mode 100644 frontend/src/views/Footer.scss
create mode 100644 frontend/src/views/Timeline.js
create mode 100644 frontend/src/views/Timeline.scss
diff --git a/application_router.go b/application_router.go
index 6431e22..30ec7c6 100644
--- a/application_router.go
+++ b/application_router.go
@@ -269,9 +269,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext,
}
if result {
- c.Status(http.StatusAccepted)
- notificationController.Notify("connections.action", UpdateNotification,
- gin.H{"connection_id": c.Param("id"), "action": c.Param("action")})
+ response := gin.H{"connection_id": c.Param("id"), "action": c.Param("action")}
+ success(c, response)
+ notificationController.Notify("connections.action", UpdateNotification, response)
} else {
notFound(c, gin.H{"connection": id})
}
diff --git a/connections_controller.go b/connections_controller.go
index 2894193..e872c9f 100644
--- a/connections_controller.go
+++ b/connections_controller.go
@@ -68,11 +68,11 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
from, _ := RowIDFromHex(filter.From)
if !from.IsZero() {
- query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": from}}})
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lte": from}}})
}
to, _ := RowIDFromHex(filter.To)
if !to.IsZero() {
- query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": to}}})
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gte": to}}})
} else {
query = query.Sort("_id", false)
}
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
index 46a0cab..b7e2531 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/Connection.js
@@ -4,6 +4,7 @@ import {Form, OverlayTrigger, Popover} from "react-bootstrap";
import backend from "../backend";
import {dateTimeToTime, durationBetween, formatSize} from "../utils";
import ButtonField from "./fields/ButtonField";
+import LinkPopover from "./objects/LinkPopover";
const classNames = require('classnames');
@@ -54,9 +55,9 @@ class Connection extends Component {
serviceName = service.name;
serviceColor = service.color;
}
- let startedAt = new Date(conn.started_at);
- let closedAt = new Date(conn.closed_at);
- let processedAt = new Date(conn.processed_at);
+ let startedAt = new Date(conn["started_at"]);
+ let closedAt = new Date(conn["closed_at"]);
+ let processedAt = new Date(conn["processed_at"]);
let timeInfo =
Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}
@@ -88,28 +89,20 @@ class Connection extends Component {
this.props.addServicePortFilter(conn.port_dst)}/>
+ onClick={() => this.props.addServicePortFilter(conn["port_dst"])}/>
|
-
{conn.ip_src} |
-
{conn.port_src} |
-
{conn.ip_dst} |
-
{conn.port_dst} |
-
{dateTimeToTime(conn.started_at)} |
+
{conn["ip_src"]} |
+
{conn["port_src"]} |
+
{conn["ip_dst"]} |
+
{conn["port_dst"]} |
-
- {durationBetween(startedAt, closedAt)}
-
+
|
-
{formatSize(conn.client_bytes)} |
-
{formatSize(conn.server_bytes)} |
+
{durationBetween(startedAt, closedAt)} |
+
{formatSize(conn["client_bytes"])} |
+
{formatSize(conn["server_bytes"])} |
- {/*Hide this connection from the list)}>*/}
- {/* this.handleAction("hide")}>%*/}
- {/**/}
Mark this connection)}>
{
const notifications = this.state.notifications;
notification.open = true;
this.setState({notifications});
}, 100);
- setTimeout(() => {
+ const hideHandle = setTimeout(() => {
const notifications = _.without(this.state.notifications, notification);
const closedNotifications = this.state.closedNotifications.concat([notification]);
notification.closed = true;
this.setState({notifications, closedNotifications});
}, 5000);
- setTimeout(() => {
+ const removeHandle = setTimeout(() => {
const closedNotifications = _.without(this.state.closedNotifications, notification);
this.setState({closedNotifications});
}, 6000);
+
+ notification.onClick = () => {
+ clearTimeout(hideHandle);
+ clearTimeout(removeHandle);
+ const notifications = _.without(this.state.notifications, notification);
+ this.setState({notifications});
+ };
});
}
@@ -45,7 +51,7 @@ class Notifications extends Component {
{
this.state.closedNotifications.concat(this.state.notifications).map(n =>
+ {"notification-open": n.open})} onClick={n.onClick}>
{n.event}
{JSON.stringify(n.message)}
diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss
index bec7734..324d0bb 100644
--- a/frontend/src/components/Notifications.scss
+++ b/frontend/src/components/Notifications.scss
@@ -18,6 +18,7 @@
color: $color-green-light;
border-left: 5px solid $color-green-dark;
background-color: $color-green;
+ cursor: pointer;
.notification-title {
font-size: 0.9em;
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index c14b7f5..4bb9f57 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import './App.scss';
import Header from "./Header";
import MainPane from "../components/panels/MainPane";
-import Footer from "./Footer";
+import Timeline from "./Timeline";
import {BrowserRouter as Router} from "react-router-dom";
import Filters from "./Filters";
import ConfigurationPane from "../components/panels/ConfigurationPane";
@@ -52,7 +52,7 @@ class App extends Component {
{modal}
- {this.state.configured && }
+ {this.state.configured && }
}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index bd631a2..e835dcb 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -17,7 +17,6 @@ class Connections extends Component {
connections: [],
firstConnection: null,
lastConnection: null,
- queryString: null
};
constructor(props) {
@@ -29,14 +28,15 @@ class Connections extends Component {
this.queryLimit = 50;
this.connectionsListRef = React.createRef();
this.lastScrollPosition = 0;
+ this.doQueryStringRedirect = false;
+ this.doSelectedConnectionRedirect = false;
}
componentDidMount() {
this.loadConnections({limit: this.queryLimit})
.then(() => this.setState({loaded: true}));
- if (this.props.initialConnection != null) {
+ if (this.props.initialConnection) {
this.setState({selected: this.props.initialConnection.id});
- // TODO: scroll to initial connection
}
dispatcher.register("timeline_updates", payload => {
@@ -62,19 +62,24 @@ class Connections extends Component {
}
connectionSelected = (c) => {
+ this.doSelectedConnectionRedirect = true;
this.setState({selected: c.id});
this.props.onSelected(c);
};
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
- this.setState({queryString: this.props.location.search});
this.loadConnections({limit: this.queryLimit})
.then(() => log.info("Connections reloaded after query string update"));
}
}
handleScroll = (e) => {
+ if (this.disableScrollHandler) {
+ this.lastScrollPosition = e.currentTarget.scrollTop;
+ return;
+ }
+
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,})
@@ -103,7 +108,8 @@ class Connections extends Component {
addServicePortFilter = (port) => {
const urlParams = new URLSearchParams(this.props.location.search);
urlParams.set("service_port", port);
- this.setState({queryString: "?" + urlParams});
+ this.doQueryStringRedirect = true;
+ this.setState({queryString: urlParams});
};
addMatchedRulesFilter = (matchedRule) => {
@@ -112,7 +118,8 @@ class Connections extends Component {
if (!oldMatchedRules.includes(matchedRule)) {
urlParams.append("matched_rules", matchedRule);
- this.setState({queryString: "?" + urlParams});
+ this.doQueryStringRedirect = true;
+ this.setState({queryString: urlParams});
}
};
@@ -139,7 +146,7 @@ class Connections extends Component {
if (params !== undefined && params.from !== undefined && params.to === undefined) {
if (res.length > 0) {
- connections = this.state.connections.concat(res);
+ connections = this.state.connections.concat(res.slice(1));
lastConnection = connections[connections.length - 1];
if (connections.length > this.maxConnections) {
connections = connections.slice(connections.length - this.maxConnections,
@@ -149,7 +156,7 @@ class Connections extends Component {
}
} else if (params !== undefined && params.to !== undefined && params.from === undefined) {
if (res.length > 0) {
- connections = res.concat(this.state.connections);
+ connections = res.slice(0, res.length - 1).concat(this.state.connections);
firstConnection = connections[0];
if (connections.length > this.maxConnections) {
connections = connections.slice(0, this.maxConnections);
@@ -157,7 +164,6 @@ class Connections extends Component {
}
}
} else {
- this.connectionsListRef.current.scrollTop = 0;
if (res.length > 0) {
connections = res;
firstConnection = connections[0];
@@ -194,9 +200,12 @@ class Connections extends Component {
render() {
let redirect;
- let queryString = this.state.queryString !== null ? this.state.queryString : "";
- if (this.state.selected) {
- redirect = ;
+ if (this.doSelectedConnectionRedirect) {
+ redirect = ;
+ this.doSelectedConnectionRedirect = false;
+ } else if (this.doQueryStringRedirect) {
+ redirect = ;
+ this.doQueryStringRedirect = false;
}
let loading = null;
@@ -209,10 +218,15 @@ class Connections extends Component {
return (
{this.state.showMoreRecentButton &&
-
+ {
+ this.disableScrollHandler = true;
+ this.connectionsListRef.current.scrollTop = 0;
this.loadConnections({limit: this.queryLimit})
- .then(() => log.info("Most recent connections loaded"))
- }/>
+ .then(() => {
+ this.disableScrollHandler = false;
+ log.info("Most recent connections loaded");
+ });
+ }}/>
}
diff --git a/frontend/src/views/Footer.js b/frontend/src/views/Footer.js
deleted file mode 100644
index dcf9cf8..0000000
--- a/frontend/src/views/Footer.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import React, {Component} from 'react';
-import './Footer.scss';
-import {
- ChartContainer,
- ChartRow,
- Charts,
- LineChart,
- MultiBrush,
- Resizable,
- styler,
- YAxis
-} from "react-timeseries-charts";
-import {TimeRange, TimeSeries} from "pondjs";
-import backend from "../backend";
-import ChoiceField from "../components/fields/ChoiceField";
-import {withRouter} from "react-router-dom";
-import log from "../log";
-import dispatcher from "../dispatcher";
-
-
-class Footer extends Component {
-
- state = {
- metric: "connections_per_service"
- };
-
- constructor() {
- super();
-
- this.disableTimeSeriesChanges = false;
- this.selectionTimeout = null;
- }
-
- filteredPort = () => {
- const urlParams = new URLSearchParams(this.props.location.search);
- return urlParams.get("service_port");
- };
-
- componentDidMount() {
- const filteredPort = this.filteredPort();
- this.setState({filteredPort});
- this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount"));
-
- dispatcher.register("connection_updates", payload => {
- this.setState({
- selection: new TimeRange(payload.from, payload.to),
- });
- });
- }
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- const filteredPort = this.filteredPort();
- if (this.state.filteredPort !== filteredPort) {
- this.setState({filteredPort});
- this.loadStatistics(this.state.metric, filteredPort).then(() =>
- log.debug("Statistics reloaded after filtered port changes"));
- }
- }
-
- loadStatistics = async (metric, filteredPort) => {
- const urlParams = new URLSearchParams();
- urlParams.set("metric", metric);
-
- let services = (await backend.get("/api/services")).json;
- if (filteredPort && services[filteredPort]) {
- const service = services[filteredPort];
- services = {};
- services[filteredPort] = service;
- }
-
- const ports = Object.keys(services);
- ports.forEach(s => urlParams.append("ports", s));
-
- const metrics = (await backend.get("/api/statistics?" + urlParams)).json;
- const series = new TimeSeries({
- name: "statistics",
- columns: ["time"].concat(ports),
- points: metrics.map(m => [new Date(m["range_start"])].concat(ports.map(p => m[metric][p] || 0)))
- });
- this.setState({
- metric,
- series,
- services,
- timeRange: series.range(),
- });
- log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`);
- };
-
- createStyler = () => {
- return styler(Object.keys(this.state.services).map(port => {
- return {key: port, color: this.state.services[port].color, width: 2};
- }));
- };
-
- handleTimeRangeChange = (timeRange) => {
- if (!this.disableTimeSeriesChanges) {
- this.setState({timeRange});
- }
- };
-
- handleSelectionChange = (timeRange) => {
- this.disableTimeSeriesChanges = true;
-
- this.setState({selection: timeRange});
- if (this.selectionTimeout) {
- clearTimeout(this.selectionTimeout);
- }
- this.selectionTimeout = setTimeout(() => {
- dispatcher.dispatch("timeline_updates", {
- from: timeRange.begin(),
- to: timeRange.end()
- });
- this.selectionTimeout = null;
- this.disableTimeSeriesChanges = false;
- }, 1000);
- };
-
- aggregateSeries = (func) => {
- const values = this.state.series.columns().map(c => this.state.series[func](c));
- return Math[func](...values);
- };
-
- render() {
- return (
-
- );
- }
-}
-
-export default withRouter(Footer);
diff --git a/frontend/src/views/Footer.scss b/frontend/src/views/Footer.scss
deleted file mode 100644
index 14360d4..0000000
--- a/frontend/src/views/Footer.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@import "../colors.scss";
-
-.footer {
- padding: 15px;
-
- .time-line {
- position: relative;
- background-color: $color-primary-0;
-
- .metric-selection {
- font-size: 0.8em;
- position: absolute;
- top: 5px;
- right: 10px;
- }
- }
-
- svg text {
- font-family: "Fira Code", monospace !important;
- fill: $color-primary-4 !important;
- }
-}
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index 944f1d5..f5eff17 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import Typed from 'typed.js';
import './Header.scss';
import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
-import {Link} from "react-router-dom";
+import {Link, withRouter} from "react-router-dom";
import ButtonField from "../components/fields/ButtonField";
class Header extends Component {
@@ -46,7 +46,7 @@ class Header extends Component {
render() {
let quickFilters = filtersNames.filter(name => this.state[`${name}_active`])
- .map(name => {filtersDefinitions[name]})
+ .map(name => {filtersDefinitions[name]})
.slice(0, 5);
return (
@@ -68,18 +68,18 @@ class Header extends Component {
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
@@ -89,4 +89,4 @@ class Header extends Component {
}
}
-export default Header;
+export default withRouter(Header);
diff --git a/frontend/src/views/Timeline.js b/frontend/src/views/Timeline.js
new file mode 100644
index 0000000..3adbf88
--- /dev/null
+++ b/frontend/src/views/Timeline.js
@@ -0,0 +1,215 @@
+import React, {Component} from 'react';
+import './Timeline.scss';
+import {
+ ChartContainer,
+ ChartRow,
+ Charts,
+ LineChart,
+ MultiBrush,
+ Resizable,
+ styler,
+ YAxis
+} from "react-timeseries-charts";
+import {TimeRange, TimeSeries} from "pondjs";
+import backend from "../backend";
+import ChoiceField from "../components/fields/ChoiceField";
+import {withRouter} from "react-router-dom";
+import log from "../log";
+import dispatcher from "../dispatcher";
+
+const minutes = 60 * 1000;
+
+class Timeline extends Component {
+
+ state = {
+ metric: "connections_per_service"
+ };
+
+ constructor() {
+ super();
+
+ this.disableTimeSeriesChanges = false;
+ this.selectionTimeout = null;
+ }
+
+ filteredPort = () => {
+ const urlParams = new URLSearchParams(this.props.location.search);
+ return urlParams.get("service_port");
+ };
+
+ componentDidMount() {
+ const filteredPort = this.filteredPort();
+ this.setState({filteredPort});
+ this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount"));
+
+ dispatcher.register("connection_updates", payload => {
+ this.setState({
+ selection: new TimeRange(payload.from, payload.to),
+ });
+ });
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "services.edit") {
+ this.loadServices().then(() => log.debug("Services reloaded after notification update"));
+ }
+ });
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ const filteredPort = this.filteredPort();
+ if (this.state.filteredPort !== filteredPort) {
+ this.setState({filteredPort});
+ this.loadStatistics(this.state.metric, filteredPort).then(() =>
+ log.debug("Statistics reloaded after filtered port changes"));
+ }
+ }
+
+ loadStatistics = async (metric, filteredPort) => {
+ const urlParams = new URLSearchParams();
+ urlParams.set("metric", metric);
+
+ let services = await this.loadServices();
+ if (filteredPort && services[filteredPort]) {
+ const service = services[filteredPort];
+ services = {};
+ services[filteredPort] = service;
+ }
+
+ const ports = Object.keys(services);
+ ports.forEach(s => urlParams.append("ports", s));
+
+ const metrics = (await backend.get("/api/statistics?" + urlParams)).json;
+ const zeroFilledMetrics = [];
+ const toTime = m => new Date(m["range_start"]).getTime();
+
+ if (metrics.length > 0) {
+ let i = 0;
+ for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) {
+ if (interval === toTime(metrics[i])) {
+ const m = metrics[i++];
+ m["range_start"] = new Date(m["range_start"]);
+ zeroFilledMetrics.push(m);
+ } else {
+ const m = {};
+ m["range_start"] = new Date(interval);
+ m[metric] = {};
+ ports.forEach(p => m[metric][p] = 0);
+ zeroFilledMetrics.push(m);
+ }
+ }
+ }
+
+ const series = new TimeSeries({
+ name: "statistics",
+ columns: ["time"].concat(ports),
+ points: zeroFilledMetrics.map(m => [m["range_start"]].concat(ports.map(p => m[metric][p] || 0)))
+ });
+ const start = series.range().begin();
+ const end = series.range().end();
+ start.setTime(start.getTime() - minutes);
+ end.setTime(end.getTime() + minutes);
+
+ this.setState({
+ metric,
+ series,
+ timeRange: new TimeRange(start, end),
+ start,
+ end
+ });
+ log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`);
+ };
+
+ loadServices = async () => {
+ const services = (await backend.get("/api/services")).json;
+ this.setState({services});
+ return services;
+ };
+
+ createStyler = () => {
+ return styler(Object.keys(this.state.services).map(port => {
+ return {key: port, color: this.state.services[port].color, width: 2};
+ }));
+ };
+
+ handleTimeRangeChange = (timeRange) => {
+ if (!this.disableTimeSeriesChanges) {
+ this.setState({timeRange});
+ }
+ };
+
+ handleSelectionChange = (timeRange) => {
+ this.disableTimeSeriesChanges = true;
+
+ this.setState({selection: timeRange});
+ if (this.selectionTimeout) {
+ clearTimeout(this.selectionTimeout);
+ }
+ this.selectionTimeout = setTimeout(() => {
+ dispatcher.dispatch("timeline_updates", {
+ from: timeRange.begin(),
+ to: timeRange.end()
+ });
+ this.selectionTimeout = null;
+ this.disableTimeSeriesChanges = false;
+ }, 1000);
+ };
+
+ aggregateSeries = (func) => {
+ const values = this.state.series.columns().map(c => this.state.series[func](c));
+ return Math[func](...values);
+ };
+
+ render() {
+ if (!this.state.series) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+}
+
+export default withRouter(Timeline);
diff --git a/frontend/src/views/Timeline.scss b/frontend/src/views/Timeline.scss
new file mode 100644
index 0000000..14360d4
--- /dev/null
+++ b/frontend/src/views/Timeline.scss
@@ -0,0 +1,22 @@
+@import "../colors.scss";
+
+.footer {
+ padding: 15px;
+
+ .time-line {
+ position: relative;
+ background-color: $color-primary-0;
+
+ .metric-selection {
+ font-size: 0.8em;
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ }
+ }
+
+ svg text {
+ font-family: "Fira Code", monospace !important;
+ fill: $color-primary-4 !important;
+ }
+}
--
cgit v1.2.3-70-g09d2
|