aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-15 06:53:09 +0000
committerEmiliano Ciavatta2020-10-15 06:53:09 +0000
commit08456e7f2e1c1af6fc8fdbf580c0178a25b93f8b (patch)
tree5c98b7c4b33848dfe92c22b9902f228a90fdacbf /frontend
parent09cb0a1518feb2221ccd8c10dced859c010e9991 (diff)
General improvements
Diffstat (limited to 'frontend')
-rw-r--r--frontend/public/logo128.png (renamed from frontend/public/logo192.png)bin6498 -> 6498 bytes
-rw-r--r--frontend/public/manifest.json8
-rw-r--r--frontend/src/components/Header.js2
-rw-r--r--frontend/src/components/Timeline.js122
-rw-r--r--frontend/src/components/Timeline.scss1
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.js64
-rw-r--r--frontend/src/components/filters/ExitSearchFilter.js9
-rw-r--r--frontend/src/components/filters/FiltersDispatcher.js57
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js81
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.js77
-rw-r--r--frontend/src/components/objects/Connection.js4
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.js21
-rw-r--r--frontend/src/components/pages/MainPage.js3
-rw-r--r--frontend/src/components/panels/ConnectionsPane.js122
-rw-r--r--frontend/src/components/panels/SearchPane.scss1
-rw-r--r--frontend/src/components/panels/StreamsPane.js2
-rw-r--r--frontend/src/dispatcher.js6
17 files changed, 285 insertions, 295 deletions
diff --git a/frontend/public/logo192.png b/frontend/public/logo128.png
index 1969e1d..1969e1d 100644
--- a/frontend/public/logo192.png
+++ b/frontend/public/logo128.png
Binary files differ
diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json
index 0409a59..32674ce 100644
--- a/frontend/public/manifest.json
+++ b/frontend/public/manifest.json
@@ -1,6 +1,6 @@
{
- "short_name": "Caronte",
- "name": "Caronte",
+ "short_name": "caronte",
+ "name": "caronte",
"icons": [
{
"src": "favicon.ico",
@@ -8,9 +8,9 @@
"type": "image/x-icon"
},
{
- "src": "logo192.png",
+ "src": "logo128.png",
"type": "image/png",
- "sizes": "192x192"
+ "sizes": "128x128"
},
{
"src": "logo512.png",
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
index b72b532..b4a2177 100644
--- a/frontend/src/components/Header.js
+++ b/frontend/src/components/Header.js
@@ -89,7 +89,7 @@ class Header extends Component {
<div className="col">
<div className="header-buttons">
- {/*<ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/>*/}
+ <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/>
<Link to={"/searches" + this.props.location.search}>
<ButtonField variant="pink" name="searches" bordered/>
</Link>
diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js
index 6b8806f..bc42a01 100644
--- a/frontend/src/components/Timeline.js
+++ b/frontend/src/components/Timeline.js
@@ -35,8 +35,12 @@ import log from "../log";
import dispatcher from "../dispatcher";
const minutes = 60 * 1000;
+const _ = require('lodash');
const classNames = require('classnames');
+const leftSelectionPaddingMultiplier = 24;
+const rightSelectionPaddingMultiplier = 8;
+
class Timeline extends Component {
state = {
@@ -50,25 +54,30 @@ class Timeline extends Component {
this.selectionTimeout = null;
}
- filteredPort = () => {
+ additionalFilters = () => {
const urlParams = new URLSearchParams(this.props.location.search);
- return urlParams.get("service_port");
+ if (this.state.metric === "matched_rules") {
+ return urlParams.getAll("matched_rules") || [];
+ } else {
+ 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"));
+ const additionalFilters = this.additionalFilters();
+ this.setState({filters: additionalFilters});
+ this.loadStatistics(this.state.metric, additionalFilters).then(() => log.debug("Statistics loaded after mount"));
dispatcher.register("connection_updates", payload => {
this.setState({
selection: new TimeRange(payload.from, payload.to),
});
+ this.adjustSelection();
});
dispatcher.register("notifications", payload => {
if (payload.event === "services.edit") {
- this.loadServices().then(() => log.debug("Services reloaded after notification update"));
+ this.loadServices().then(() => this.adjustSelection());
}
});
@@ -79,27 +88,48 @@ class Timeline extends Component {
}
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"));
+ const additionalFilters = this.additionalFilters();
+ const updateStatistics = () => {
+ this.setState({filters: additionalFilters});
+ this.loadStatistics(this.state.metric, additionalFilters).then(() =>
+ log.debug("Statistics reloaded after filters changes"));
+ };
+
+ if (this.state.metric === "matched_rules") {
+ if (!Array.isArray(this.state.filters) ||
+ !_.isEqual(_.sortBy(additionalFilters), _.sortBy(this.state.filters))) {
+ updateStatistics();
+ }
+ } else {
+ if (this.state.filters !== additionalFilters) {
+ updateStatistics();
+ }
}
}
- loadStatistics = async (metric, filteredPort) => {
+ loadStatistics = async (metric, filters) => {
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;
- }
+ let columns = [];
+ if (metric === "matched_rules") {
+ let rules = await this.loadRules();
+ filters.forEach(id => {
+ urlParams.append("matched_rules", id);
+ });
+ columns = rules.map(r => r.id);
+ } else {
+ let services = await this.loadServices();
+ const filteredPort = filters;
+ if (filteredPort && services[filters]) {
+ const service = services[filteredPort];
+ services = {};
+ services[filteredPort] = service;
+ }
- const ports = Object.keys(services);
- ports.forEach(s => urlParams.append("ports", s));
+ columns = Object.keys(services);
+ columns.forEach(port => urlParams.append("ports", port));
+ }
const metrics = (await backend.get("/api/statistics?" + urlParams)).json;
if (metrics.length === 0) {
@@ -109,8 +139,8 @@ class Timeline extends Component {
const zeroFilledMetrics = [];
const toTime = m => new Date(m["range_start"]).getTime();
let i = 0;
- for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) {
- if (interval === toTime(metrics[i])) {
+ for (let interval = toTime(metrics[0]) - minutes; interval <= toTime(metrics[metrics.length - 1]) + minutes; interval += minutes) {
+ if (i < metrics.length && interval === toTime(metrics[i])) {
const m = metrics[i++];
m["range_start"] = new Date(m["range_start"]);
zeroFilledMetrics.push(m);
@@ -118,30 +148,31 @@ class Timeline extends Component {
const m = {};
m["range_start"] = new Date(interval);
m[metric] = {};
- ports.forEach(p => m[metric][p] = 0);
+ columns.forEach(c => m[metric][c] = 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)))
+ columns: ["time"].concat(columns),
+ points: zeroFilledMetrics.map(m => [m["range_start"]].concat(columns.map(c =>
+ ((metric in m) && (m[metric] != null)) ? (m[metric][c] || 0) : 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),
+ columns,
start,
end
});
- log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`);
+ log.debug(`Loaded statistics for metric "${metric}"`);
};
loadServices = async () => {
@@ -150,10 +181,22 @@ class Timeline extends Component {
return services;
};
+ loadRules = async () => {
+ const rules = (await backend.get("/api/rules")).json;
+ this.setState({rules});
+ return rules;
+ };
+
createStyler = () => {
- return styler(Object.keys(this.state.services).map(port => {
- return {key: port, color: this.state.services[port].color, width: 2};
- }));
+ if (this.state.metric === "matched_rules") {
+ return styler(this.state.rules.map(rule => {
+ return {key: rule.id, color: rule.color, width: 2};
+ }));
+ } else {
+ return styler(Object.keys(this.state.services).map(port => {
+ return {key: port, color: this.state.services[port].color, width: 2};
+ }));
+ }
};
handleTimeRangeChange = (timeRange) => {
@@ -179,6 +222,15 @@ class Timeline extends Component {
}, 1000);
};
+ adjustSelection = () => {
+ const seriesRange = this.state.series.range();
+ const selection = this.state.selection;
+ const delta = selection.end() - selection.begin();
+ const start = Math.max(selection.begin().getTime() - delta * leftSelectionPaddingMultiplier, seriesRange.begin().getTime());
+ const end = Math.min(selection.end().getTime() + delta * rightSelectionPaddingMultiplier, seriesRange.end().getTime());
+ this.setState({timeRange: new TimeRange(start, end)});
+ };
+
aggregateSeries = (func) => {
const values = this.state.series.columns().map(c => this.state.series[func](c));
return Math[func](...values);
@@ -207,7 +259,7 @@ class Timeline extends Component {
max={this.aggregateSeries("max")} width="35" type="linear" transition={300}/>
<Charts>
<LineChart axis="axis1" series={this.state.series}
- columns={Object.keys(this.state.services)}
+ columns={this.state.columns}
style={this.createStyler()} interpolation="curveBasis"/>
<MultiBrush
@@ -224,10 +276,10 @@ class Timeline extends Component {
<div className="metric-selection">
<ChoiceField inline small
keys={["connections_per_service", "client_bytes_per_service",
- "server_bytes_per_service", "duration_per_service"]}
+ "server_bytes_per_service", "duration_per_service", "matched_rules"]}
values={["connections_per_service", "client_bytes_per_service",
- "server_bytes_per_service", "duration_per_service"]}
- onChange={(metric) => this.loadStatistics(metric, this.state.filteredPort)
+ "server_bytes_per_service", "duration_per_service", "matched_rules"]}
+ onChange={(metric) => this.loadStatistics(metric, this.state.filters)
.then(() => log.debug("Statistics loaded after metric changes"))}
value={this.state.metric}/>
</div>
diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss
index db8d9c8..262da1e 100644
--- a/frontend/src/components/Timeline.scss
+++ b/frontend/src/components/Timeline.scss
@@ -12,6 +12,7 @@
position: absolute;
top: 5px;
right: 10px;
+ width: 180px;
}
&.pulse-timeline {
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js
index a9a420e..c611a0d 100644
--- a/frontend/src/components/filters/BooleanConnectionsFilter.js
+++ b/frontend/src/components/filters/BooleanConnectionsFilter.js
@@ -17,65 +17,49 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
import CheckField from "../fields/CheckField";
+import dispatcher from "../../dispatcher";
class BooleanConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- filterActive: "false"
- };
-
- this.filterChanged = this.filterChanged.bind(this);
- this.needRedirect = false;
- }
+ state = {
+ filterActive: "false"
+ };
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()});
+
+ this.connectionsFiltersCallback = payload => {
+ const name = this.props.filterName;
+ if (name in payload && this.state.filterActive !== payload[name]) {
+ this.setState({filterActive: payload[name]});
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let externalActive = this.toBoolean(urlParams.get(this.props.filterName));
- let filterActive = this.toBoolean(this.state.filterActive);
- // if the filterActive state is changed by another component (and not by filterChanged func) and
- // the query string is not equals at the filterActive state, update the state of the component
- if (this.toBoolean(prevState.filterActive) === filterActive && filterActive !== externalActive) {
- this.setState({filterActive: externalActive.toString()});
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- toBoolean(value) {
+ toBoolean = (value) => {
return value !== null && value.toLowerCase() === "true";
- }
+ };
- filterChanged() {
- this.needRedirect = true;
- this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()});
- }
+ filterChanged = () => {
+ const newValue = (!this.toBoolean(this.state.filterActive)).toString();
+ const urlParams = {};
+ urlParams[this.props.filterName] = newValue === "true" ? "true" : null;
+ dispatcher.dispatch("connections_filters", urlParams);
+ this.setState({filterActive: newValue});
+ };
render() {
- let redirect = null;
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- if (this.toBoolean(this.state.filterActive)) {
- urlParams.set(this.props.filterName, "true");
- } else {
- urlParams.delete(this.props.filterName);
- }
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
-
- this.needRedirect = false;
- }
-
return (
<div className="filter" style={{"width": `${this.props.width}px`}}>
<CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName}
- onChange={this.filterChanged} />
- {redirect}
+ onChange={this.filterChanged}/>
</div>
);
}
diff --git a/frontend/src/components/filters/ExitSearchFilter.js b/frontend/src/components/filters/ExitSearchFilter.js
index cfee298..68ca686 100644
--- a/frontend/src/components/filters/ExitSearchFilter.js
+++ b/frontend/src/components/filters/ExitSearchFilter.js
@@ -28,11 +28,16 @@ class ExitSearchFilter extends Component {
let params = new URLSearchParams(this.props.location.search);
this.setState({performedSearch: params.get("performed_search")});
- dispatcher.register("connections_filters", payload => {
+ this.connectionsFiltersCallback = payload => {
if (this.state.performedSearch !== payload["performed_search"]) {
this.setState({performedSearch: payload["performed_search"]});
}
- });
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
render() {
diff --git a/frontend/src/components/filters/FiltersDispatcher.js b/frontend/src/components/filters/FiltersDispatcher.js
deleted file mode 100644
index 3769055..0000000
--- a/frontend/src/components/filters/FiltersDispatcher.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
-import dispatcher from "../../dispatcher";
-
-class FiltersDispatcher extends Component {
-
- state = {};
-
- componentDidMount() {
- let params = new URLSearchParams(this.props.location.search);
- this.setState({params});
-
- dispatcher.register("connections_filters", payload => {
- const params = this.state.params;
-
- Object.entries(payload).forEach(([key, value]) => {
- if (value == null) {
- params.delete(key);
- } else {
- params.set(key, value);
- }
- });
-
- this.needRedirect = true;
- this.setState({params});
- });
- }
-
- render() {
- if (this.needRedirect) {
- this.needRedirect = false;
- return <Redirect push to={`${this.props.location.pathname}?${this.state.params}`}/>;
- }
-
- return null;
- }
-}
-
-export default withRouter(FiltersDispatcher);
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index fc0ad4d..4c993dc 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -17,87 +17,74 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
import './RulesConnectionsFilter.scss';
import ReactTags from 'react-tag-autocomplete';
import backend from "../../backend";
+import dispatcher from "../../dispatcher";
const classNames = require('classnames');
+const _ = require('lodash');
class RulesConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- mounted: false,
- rules: [],
- activeRules: []
- };
-
- this.needRedirect = false;
- }
+ state = {
+ rules: [],
+ activeRules: []
+ };
componentDidMount() {
- let params = new URLSearchParams(this.props.location.search);
+ const params = new URLSearchParams(this.props.location.search);
let activeRules = params.getAll("matched_rules") || [];
backend.get("/api/rules").then(res => {
let rules = res.json.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
activeRules = rules.filter(rule => activeRules.some(id => rule.id === id));
- this.setState({rules, activeRules, mounted: true});
+ this.setState({rules, activeRules});
});
+
+ this.connectionsFiltersCallback = payload => {
+ if ("matched_rules" in payload && !_.isEqual(payload["matched_rules"].sort(), this.state.activeRules.sort())) {
+ const newRules = this.state.rules.filter(r => payload["matched_rules"].includes(r.id));
+ this.setState({
+ activeRules: newRules.map(r => {
+ return {id: r.id, name: r.name};
+ })
+ });
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let externalRules = urlParams.getAll("matched_rules") || [];
- let activeRules = this.state.activeRules.map(r => r.id);
- let compareRules = (first, second) => first.sort().join(",") === second.sort().join(",");
- if (this.state.mounted &&
- compareRules(prevState.activeRules.map(r => r.id), activeRules) &&
- !compareRules(externalRules, activeRules)) {
- this.setState({activeRules: externalRules.map(id => this.state.rules.find(r => r.id === id))});
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- onDelete(i) {
- const activeRules = this.state.activeRules.slice(0);
+ onDelete = (i) => {
+ const activeRules = _.clone(this.state.activeRules);
activeRules.splice(i, 1);
- this.needRedirect = true;
- this.setState({ activeRules });
- }
+ this.setState({activeRules});
+ dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)});
+ };
- onAddition(rule) {
+ onAddition = (rule) => {
if (!this.state.activeRules.includes(rule)) {
const activeRules = [].concat(this.state.activeRules, rule);
- this.needRedirect = true;
this.setState({activeRules});
+ dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)});
}
- }
+ };
render() {
- let redirect = null;
-
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- urlParams.delete("matched_rules");
- this.state.activeRules.forEach(rule => urlParams.append("matched_rules", rule.id));
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
-
- this.needRedirect = false;
- }
-
return (
- <div className={classNames("filter", "d-inline-block", {"filter-active" : this.state.filterActive === "true"})}>
+ <div
+ className={classNames("filter", "d-inline-block", {"filter-active": this.state.filterActive === "true"})}>
<div className="filter-rules">
<ReactTags tags={this.state.activeRules} suggestions={this.state.rules}
- onDelete={this.onDelete.bind(this)} onAddition={this.onAddition.bind(this)}
+ onDelete={this.onDelete} onAddition={this.onAddition}
minQueryLength={0} placeholderText="rule_name"
suggestionsFilter={(suggestion, query) =>
- suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} />
+ suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)}/>
</div>
-
- {redirect}
</div>
);
}
diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js
index a3b45dc..c833220 100644
--- a/frontend/src/components/filters/StringConnectionsFilter.js
+++ b/frontend/src/components/filters/StringConnectionsFilter.js
@@ -17,37 +17,36 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
import InputField from "../fields/InputField";
+import dispatcher from "../../dispatcher";
class StringConnectionsFilter extends Component {
- constructor(props) {
- super(props);
- this.state = {
- fieldValue: "",
- filterValue: null,
- timeoutHandle: null,
- invalidValue: false
- };
- this.needRedirect = false;
- this.filterChanged = this.filterChanged.bind(this);
- }
+ state = {
+ fieldValue: "",
+ filterValue: null,
+ timeoutHandle: null,
+ invalidValue: false
+ };
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
this.updateStateFromFilterValue(params.get(this.props.filterName));
+
+ this.connectionsFiltersCallback = payload => {
+ const name = this.props.filterName;
+ if (name in payload && this.state.filterValue !== payload[name]) {
+ this.updateStateFromFilterValue(payload[name]);
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- let urlParams = new URLSearchParams(this.props.location.search);
- let filterValue = urlParams.get(this.props.filterName);
- if (prevState.filterValue === this.state.filterValue && this.state.filterValue !== filterValue) {
- this.updateStateFromFilterValue(filterValue);
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- updateStateFromFilterValue(filterValue) {
+ updateStateFromFilterValue = (filterValue) => {
if (filterValue !== null) {
let fieldValue = filterValue;
if (typeof this.props.decodeFunc === "function") {
@@ -70,15 +69,21 @@ class StringConnectionsFilter extends Component {
} else {
this.setState({fieldValue: "", filterValue: null});
}
- }
+ };
- isValueValid(value) {
+ isValueValid = (value) => {
return typeof this.props.validateFunc !== "function" ||
(typeof this.props.validateFunc === "function" && this.props.validateFunc(value));
- }
+ };
- filterChanged(fieldValue) {
- if (this.state.timeoutHandle !== null) {
+ changeFilterValue = (value) => {
+ const urlParams = {};
+ urlParams[this.props.filterName] = value;
+ dispatcher.dispatch("connections_filters", urlParams);
+ };
+
+ filterChanged = (fieldValue) => {
+ if (this.state.timeoutHandle) {
clearTimeout(this.state.timeoutHandle);
}
@@ -87,11 +92,12 @@ class StringConnectionsFilter extends Component {
}
if (fieldValue === "") {
- this.needRedirect = true;
this.setState({fieldValue: "", filterValue: null, invalidValue: false});
- return;
+ return this.changeFilterValue(null);
}
+
+
if (this.isValueValid(fieldValue)) {
let filterValue = fieldValue;
if (filterValue !== "" && typeof this.props.encodeFunc === "function") {
@@ -101,40 +107,27 @@ class StringConnectionsFilter extends Component {
this.setState({
fieldValue: fieldValue,
timeoutHandle: setTimeout(() => {
- this.needRedirect = true;
this.setState({filterValue: filterValue});
+ this.changeFilterValue(filterValue);
}, 500),
invalidValue: false
});
} else {
- this.needRedirect = true;
this.setState({
fieldValue: fieldValue,
invalidValue: true
});
}
- }
+ };
render() {
- let redirect = null;
- if (this.needRedirect) {
- let urlParams = new URLSearchParams(this.props.location.search);
- if (this.state.filterValue !== null) {
- urlParams.set(this.props.filterName, this.state.filterValue);
- } else {
- urlParams.delete(this.props.filterName);
- }
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
- this.needRedirect = false;
- }
let active = this.state.filterValue !== null;
return (
<div className="filter" style={{"width": `${this.props.width}px`}}>
<InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName}
placeholder={this.props.defaultFilterValue} onChange={this.filterChanged}
- value={this.state.fieldValue} inline={true} small={true} />
- {redirect}
+ value={this.state.fieldValue} inline={true} small={true}/>
</div>
);
}
diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js
index e0e942a..96f2235 100644
--- a/frontend/src/components/objects/Connection.js
+++ b/frontend/src/components/objects/Connection.js
@@ -23,6 +23,7 @@ import {dateTimeToTime, durationBetween, formatSize} from "../../utils";
import ButtonField from "../fields/ButtonField";
import LinkPopover from "./LinkPopover";
import TextField from "../fields/TextField";
+import dispatcher from "../../dispatcher";
const classNames = require('classnames');
@@ -99,7 +100,8 @@ class Connection extends Component {
<td>
<span className="connection-service">
<ButtonField small fullSpan color={serviceColor} name={serviceName}
- onClick={() => this.props.addServicePortFilter(conn["port_dst"])}/>
+ onClick={() => dispatcher.dispatch("connections_filters",
+ {"service_port": conn["port_dst"].toString()})}/>
</span>
</td>
<td className="clickable" onClick={this.props.onSelected}>{conn["ip_src"]}</td>
diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js
index 73d5c5d..92bde49 100644
--- a/frontend/src/components/objects/ConnectionMatchedRules.js
+++ b/frontend/src/components/objects/ConnectionMatchedRules.js
@@ -18,20 +18,25 @@
import React, {Component} from 'react';
import './ConnectionMatchedRules.scss';
import ButtonField from "../fields/ButtonField";
+import dispatcher from "../../dispatcher";
+import {withRouter} from "react-router-dom";
class ConnectionMatchedRules extends Component {
- constructor(props) {
- super(props);
- this.state = {
- };
- }
+ onMatchedRulesSelected = (id) => {
+ const params = new URLSearchParams(this.props.location.search);
+ const rules = params.getAll("matched_rules");
+ if (!rules.includes(id)) {
+ rules.push(id);
+ dispatcher.dispatch("connections_filters",{"matched_rules": rules});
+ }
+ };
render() {
const matchedRules = this.props.matchedRules.map(mr => {
const rule = this.props.rules.find(r => r.id === mr);
- return <ButtonField key={mr} onClick={() => this.props.addMatchedRulesFilter(rule.id)} name={rule.name}
- color={rule.color} small />;
+ return <ButtonField key={mr} onClick={() => this.onMatchedRulesSelected(rule.id)} name={rule.name}
+ color={rule.color} small/>;
});
return (
@@ -43,4 +48,4 @@ class ConnectionMatchedRules extends Component {
}
}
-export default ConnectionMatchedRules;
+export default withRouter(ConnectionMatchedRules);
diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js
index da57e1a..0b06f55 100644
--- a/frontend/src/components/pages/MainPage.js
+++ b/frontend/src/components/pages/MainPage.js
@@ -29,7 +29,6 @@ import Header from "../Header";
import Filters from "../dialogs/Filters";
import MainPane from "../panels/MainPane";
import SearchPane from "../panels/SearchPane";
-import FiltersDispatcher from "../filters/FiltersDispatcher";
class MainPage extends Component {
@@ -70,8 +69,6 @@ class MainPage extends Component {
<div className="page-footer">
<Timeline/>
</div>
-
- <FiltersDispatcher />
</Router>
</div>
);
diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js
index 1f79ab8..33dd7c1 100644
--- a/frontend/src/components/panels/ConnectionsPane.js
+++ b/frontend/src/components/panels/ConnectionsPane.js
@@ -19,13 +19,13 @@ import React, {Component} from 'react';
import './ConnectionsPane.scss';
import Connection from "../objects/Connection";
import Table from 'react-bootstrap/Table';
-import {Redirect} from 'react-router';
import {withRouter} from "react-router-dom";
import backend from "../../backend";
import ConnectionMatchedRules from "../objects/ConnectionMatchedRules";
import log from "../../log";
import ButtonField from "../fields/ButtonField";
import dispatcher from "../../dispatcher";
+import {Redirect} from "react-router";
const classNames = require('classnames');
@@ -50,60 +50,91 @@ class ConnectionsPane extends Component {
}
componentDidMount() {
- const initialParams = {limit: this.queryLimit};
+ let urlParams = new URLSearchParams(this.props.location.search);
+ this.setState({urlParams});
+
+ const additionalParams = {limit: this.queryLimit};
const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
if (match != null) {
const id = match[1];
- initialParams.from = id;
+ additionalParams.from = id;
backend.get(`/api/connections/${id}`)
- .then(res => this.connectionSelected(res.json, false))
+ .then(res => this.connectionSelected(res.json))
.catch(error => log.error("Error loading initial connection", error));
}
- this.loadConnections(initialParams, true).then(() => log.debug("Connections loaded"));
+ this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded"));
+
+ this.connectionsFiltersCallback = payload => {
+ const params = this.state.urlParams;
+ const initialParams = params.toString();
+
+ Object.entries(payload).forEach(([key, value]) => {
+ if (value == null) {
+ params.delete(key);
+ } else if (Array.isArray(value)) {
+ params.delete(key);
+ value.forEach(v => params.append(key, v));
+ } else {
+ params.set(key, value);
+ }
+ });
+
+ if (initialParams === params.toString()) {
+ return;
+ }
- dispatcher.register("timeline_updates", payload => {
+ log.debug("Update following url params:", payload);
+ this.queryStringRedirect = true;
+ this.setState({urlParams});
+
+ this.loadConnections({limit: this.queryLimit}, urlParams)
+ .then(() => log.info("ConnectionsPane reloaded after query string update"));
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
+
+ this.timelineUpdatesCallback = payload => {
this.connectionsListRef.current.scrollTop = 0;
this.loadConnections({
started_after: Math.round(payload.from.getTime() / 1000),
started_before: Math.round(payload.to.getTime() / 1000),
limit: this.maxConnections
}).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`));
- });
+ };
+ dispatcher.register("timeline_updates", this.timelineUpdatesCallback);
- dispatcher.register("notifications", payload => {
+ this.notificationsCallback = payload => {
if (payload.event === "rules.new" || payload.event === "rules.edit") {
this.loadRules().then(() => log.debug("Loaded connection rules after notification update"));
}
- });
-
- dispatcher.register("notifications", payload => {
if (payload.event === "services.edit") {
this.loadServices().then(() => log.debug("Services reloaded after notification update"));
}
- });
+ };
+ dispatcher.register("notifications", this.notificationsCallback);
- dispatcher.register("pulse_connections_view", payload => {
+ this.pulseConnectionsViewCallback = payload => {
this.setState({pulseConnectionsView: true});
setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration);
- });
+ };
+ dispatcher.register("pulse_connections_view", this.pulseConnectionsViewCallback);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.timelineUpdatesCallback);
+ dispatcher.unregister(this.notificationsCallback);
+ dispatcher.unregister(this.pulseConnectionsViewCallback);
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- connectionSelected = (c, doRedirect = true) => {
- this.doSelectedConnectionRedirect = doRedirect;
+ connectionSelected = (c) => {
+ this.connectionSelectedRedirect = true;
this.setState({selected: c.id});
this.props.onSelected(c);
log.debug(`Connection ${c.id} selected`);
};
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (prevProps.location.search !== this.props.location.search) {
- this.loadConnections({limit: this.queryLimit})
- .then(() => log.info("ConnectionsPane reloaded after query string update"));
- }
- }
-
handleScroll = (e) => {
if (this.disableScrollHandler) {
this.lastScrollPosition = e.currentTarget.scrollTop;
@@ -135,27 +166,12 @@ class ConnectionsPane extends Component {
this.lastScrollPosition = e.currentTarget.scrollTop;
};
- addServicePortFilter = (port) => {
- const urlParams = new URLSearchParams(this.props.location.search);
- urlParams.set("service_port", port);
- this.doQueryStringRedirect = true;
- this.setState({queryString: urlParams});
- };
-
- addMatchedRulesFilter = (matchedRule) => {
- const urlParams = new URLSearchParams(this.props.location.search);
- const oldMatchedRules = urlParams.getAll("matched_rules") || [];
-
- if (!oldMatchedRules.includes(matchedRule)) {
- urlParams.append("matched_rules", matchedRule);
- this.doQueryStringRedirect = true;
- this.setState({queryString: urlParams});
+ async loadConnections(additionalParams, initialParams = null, isInitial = false) {
+ if (!initialParams) {
+ initialParams = this.state.urlParams;
}
- };
-
- async loadConnections(params, isInitial = false) {
- const urlParams = new URLSearchParams(this.props.location.search);
- for (const [name, value] of Object.entries(params)) {
+ const urlParams = new URLSearchParams(initialParams.toString());
+ for (const [name, value] of Object.entries(additionalParams)) {
urlParams.set(name, value);
}
@@ -173,7 +189,7 @@ class ConnectionsPane extends Component {
let firstConnection = this.state.firstConnection;
let lastConnection = this.state.lastConnection;
- if (params !== undefined && params.from !== undefined && params.to === undefined) {
+ if (additionalParams !== undefined && additionalParams.from !== undefined && additionalParams.to === undefined) {
if (res.length > 0) {
if (!isInitial) {
res = res.slice(1);
@@ -189,7 +205,7 @@ class ConnectionsPane extends Component {
firstConnection = connections[0];
}
}
- } else if (params !== undefined && params.to !== undefined && params.from === undefined) {
+ } else if (additionalParams !== undefined && additionalParams.to !== undefined && additionalParams.from === undefined) {
if (res.length > 0) {
connections = res.slice(0, res.length - 1).concat(this.state.connections);
firstConnection = connections[0];
@@ -235,12 +251,12 @@ class ConnectionsPane extends Component {
render() {
let redirect;
- if (this.doSelectedConnectionRedirect) {
- redirect = <Redirect push to={`/connections/${this.state.selected}${this.props.location.search}`}/>;
- this.doSelectedConnectionRedirect = false;
- } else if (this.doQueryStringRedirect) {
- redirect = <Redirect push to={`${this.props.location.pathname}?${this.state.queryString}`}/>;
- this.doQueryStringRedirect = false;
+ if (this.connectionSelectedRedirect) {
+ redirect = <Redirect push to={`/connections/${this.state.selected}?${this.state.urlParams}`}/>;
+ this.connectionSelectedRedirect = false;
+ } else if (this.queryStringRedirect) {
+ redirect = <Redirect push to={`${this.props.location.pathname}?${this.state.urlParams}`}/>;
+ this.queryStringRedirect = false;
}
let loading = null;
@@ -288,12 +304,10 @@ class ConnectionsPane extends Component {
selected={this.state.selected === c.id}
onMarked={marked => c.marked = marked}
onEnabled={enabled => c.hidden = !enabled}
- addServicePortFilter={this.addServicePortFilter}
services={this.state.services}/>,
c.matched_rules.length > 0 &&
<ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
- rules={this.state.rules}
- addMatchedRulesFilter={this.addMatchedRulesFilter}/>
+ rules={this.state.rules}/>
];
})
}
diff --git a/frontend/src/components/panels/SearchPane.scss b/frontend/src/components/panels/SearchPane.scss
index 15fc7da..63e11fb 100644
--- a/frontend/src/components/panels/SearchPane.scss
+++ b/frontend/src/components/panels/SearchPane.scss
@@ -4,6 +4,7 @@
.searches-list {
overflow: hidden;
+ flex: 2 1;
.section-content {
height: 100%;
diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js
index bd1964e..1aa5c53 100644
--- a/frontend/src/components/panels/StreamsPane.js
+++ b/frontend/src/components/panels/StreamsPane.js
@@ -107,7 +107,7 @@ class StreamsPane extends Component {
const json = JSON.parse(m.body);
body = <ReactJson src={json} theme="grayscale" collapsed={false} displayDataTypes={false}/>;
} catch (e) {
- console.log(e);
+ log.error(e);
}
}
diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js
index 943f7ec..fa08d48 100644
--- a/frontend/src/dispatcher.js
+++ b/frontend/src/dispatcher.js
@@ -15,6 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+const _ = require('lodash');
+
class Dispatcher {
constructor() {
@@ -44,6 +46,10 @@ class Dispatcher {
}
};
+ unregister = (callback) => {
+ this.listeners = _.without(callback);
+ };
+
}
const dispatcher = new Dispatcher();