aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-08 20:17:04 +0000
committerEmiliano Ciavatta2020-10-08 20:17:04 +0000
commitd203f3c7e3bcaa20895c0f32f348cd1513ae9876 (patch)
treebc5beea659f6d1717a0e31b0ee10cde6699da2ad /frontend/src/components
parente1198433a63eec2c900ac8986dbf0ae7db16b777 (diff)
Frontend folder structure refactor
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/App.js62
-rw-r--r--frontend/src/components/Header.js111
-rw-r--r--frontend/src/components/Header.scss34
-rw-r--r--frontend/src/components/Timeline.js232
-rw-r--r--frontend/src/components/Timeline.scss22
-rw-r--r--frontend/src/components/dialogs/Filters.js116
-rw-r--r--frontend/src/components/objects/Connection.js (renamed from frontend/src/components/Connection.js)8
-rw-r--r--frontend/src/components/objects/Connection.scss (renamed from frontend/src/components/Connection.scss)2
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.js (renamed from frontend/src/components/ConnectionMatchedRules.js)2
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.scss (renamed from frontend/src/components/ConnectionMatchedRules.scss)2
-rw-r--r--frontend/src/components/objects/MessageAction.js (renamed from frontend/src/components/MessageAction.js)4
-rw-r--r--frontend/src/components/objects/MessageAction.scss (renamed from frontend/src/components/MessageAction.scss)2
-rw-r--r--frontend/src/components/pages/ConfigurationPage.js (renamed from frontend/src/components/panels/ConfigurationPane.js)10
-rw-r--r--frontend/src/components/pages/ConfigurationPage.scss (renamed from frontend/src/components/panels/ConfigurationPane.scss)2
-rw-r--r--frontend/src/components/pages/MainPage.js76
-rw-r--r--frontend/src/components/pages/MainPage.scss24
-rw-r--r--frontend/src/components/pages/ServiceUnavailablePage.js34
-rw-r--r--frontend/src/components/pages/common.scss16
-rw-r--r--frontend/src/components/panels/ConnectionsPane.js304
-rw-r--r--frontend/src/components/panels/ConnectionsPane.scss38
-rw-r--r--frontend/src/components/panels/MainPane.js47
-rw-r--r--frontend/src/components/panels/MainPane.scss17
-rw-r--r--frontend/src/components/panels/PcapsPane.js (renamed from frontend/src/components/panels/PcapPane.js)6
-rw-r--r--frontend/src/components/panels/PcapsPane.scss (renamed from frontend/src/components/panels/PcapPane.scss)0
-rw-r--r--frontend/src/components/panels/RulesPane.js (renamed from frontend/src/components/panels/RulePane.js)6
-rw-r--r--frontend/src/components/panels/RulesPane.scss (renamed from frontend/src/components/panels/RulePane.scss)0
-rw-r--r--frontend/src/components/panels/ServicesPane.js (renamed from frontend/src/components/panels/ServicePane.js)6
-rw-r--r--frontend/src/components/panels/ServicesPane.scss (renamed from frontend/src/components/panels/ServicePane.scss)0
-rw-r--r--frontend/src/components/panels/StreamsPane.js (renamed from frontend/src/components/ConnectionContent.js)77
-rw-r--r--frontend/src/components/panels/StreamsPane.scss (renamed from frontend/src/components/ConnectionContent.scss)2
30 files changed, 1139 insertions, 123 deletions
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js
new file mode 100644
index 0000000..bf959c5
--- /dev/null
+++ b/frontend/src/components/App.js
@@ -0,0 +1,62 @@
+/*
+ * 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 ConfigurationPage from "./pages/ConfigurationPage";
+import Notifications from "./Notifications";
+import dispatcher from "../dispatcher";
+import MainPage from "./pages/MainPage";
+import ServiceUnavailablePage from "./pages/ServiceUnavailablePage";
+
+class App extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "connected") {
+ this.setState({
+ connected: true,
+ configured: payload.message["is_configured"]
+ });
+ }
+ });
+
+ setInterval(() => {
+ if (document.title.endsWith("❚")) {
+ document.title = document.title.slice(0, -1);
+ } else {
+ document.title += "❚";
+ }
+ }, 500);
+ }
+
+ render() {
+ return (
+ <>
+ <Notifications/>
+ {this.state.connected ?
+ (this.state.configured ? <MainPage/> :
+ <ConfigurationPage onConfigured={() => this.setState({configured: true})}/>) :
+ <ServiceUnavailablePage/>
+ }
+ </>
+ );
+ }
+}
+
+export default App;
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
new file mode 100644
index 0000000..4d29364
--- /dev/null
+++ b/frontend/src/components/Header.js
@@ -0,0 +1,111 @@
+/*
+ * 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 Typed from 'typed.js';
+import './Header.scss';
+import {filtersDefinitions, filtersNames} from "./filters/FiltersDefinitions";
+import {Link, withRouter} from "react-router-dom";
+import ButtonField from "./fields/ButtonField";
+
+class Header extends Component {
+
+ constructor(props) {
+ super(props);
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = false);
+ this.state = newState;
+ this.fetchStateFromLocalStorage = this.fetchStateFromLocalStorage.bind(this);
+ }
+
+ componentDidMount() {
+ const options = {
+ strings: ["caronte$ "],
+ typeSpeed: 50,
+ cursorChar: "❚"
+ };
+ this.typed = new Typed(this.el, options);
+
+ this.fetchStateFromLocalStorage();
+
+ if (typeof window !== "undefined") {
+ window.addEventListener("quick-filters", this.fetchStateFromLocalStorage);
+ }
+ }
+
+ componentWillUnmount() {
+ this.typed.destroy();
+
+ if (typeof window !== "undefined") {
+ window.removeEventListener("quick-filters", this.fetchStateFromLocalStorage);
+ }
+ }
+
+ fetchStateFromLocalStorage() {
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
+ this.setState(newState);
+ }
+
+ render() {
+ let quickFilters = filtersNames.filter(name => this.state[`${name}_active`])
+ .map(name => <React.Fragment key={name}>{filtersDefinitions[name]}</React.Fragment>)
+ .slice(0, 5);
+
+ return (
+ <header className="header container-fluid">
+ <div className="row">
+ <div className="col-auto">
+ <h1 className="header-title type-wrap">
+ <Link to="/">
+ <span style={{whiteSpace: 'pre'}} ref={(el) => {
+ this.el = el;
+ }}/>
+ </Link>
+ </h1>
+ </div>
+
+ <div className="col-auto">
+ <div className="filters-bar">
+ {quickFilters}
+ </div>
+ </div>
+
+ <div className="col">
+ <div className="header-buttons">
+ <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/>
+ <Link to={"/pcaps" + this.props.location.search}>
+ <ButtonField variant="purple" name="pcaps" bordered/>
+ </Link>
+ <Link to={"/rules" + this.props.location.search}>
+ <ButtonField variant="deep-purple" name="rules" bordered/>
+ </Link>
+ <Link to={"/services" + this.props.location.search}>
+ <ButtonField variant="indigo" name="services" bordered/>
+ </Link>
+ <Link to={"/config" + this.props.location.search}>
+ <ButtonField variant="blue" name="config" bordered/>
+ </Link>
+ </div>
+ </div>
+ </div>
+ </header>
+ );
+ }
+}
+
+export default withRouter(Header);
diff --git a/frontend/src/components/Header.scss b/frontend/src/components/Header.scss
new file mode 100644
index 0000000..e2e8e1c
--- /dev/null
+++ b/frontend/src/components/Header.scss
@@ -0,0 +1,34 @@
+@import "../colors";
+
+.header {
+ height: 80px;
+ padding: 15px 30px;
+
+ > .row {
+ background-color: $color-primary-0;
+ }
+
+ .header-title {
+ width: 200px;
+ margin: 5px 0 5px -5px;
+ }
+
+ .header-buttons {
+ display: flex;
+ justify-content: flex-end;
+ margin: 7px 0;
+
+ .button-field {
+ margin-left: 7px;
+ }
+ }
+
+ .filters-bar {
+ padding: 3px 0;
+
+ .filter {
+ display: inline-block;
+ margin-right: 10px;
+ }
+ }
+}
diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js
new file mode 100644
index 0000000..7be42e0
--- /dev/null
+++ b/frontend/src/components/Timeline.js
@@ -0,0 +1,232 @@
+/*
+ * 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 './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 "./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 (
+ <footer className="footer">
+ <div className="time-line">
+ <Resizable>
+ <ChartContainer timeRange={this.state.timeRange} enableDragZoom={false}
+ paddingTop={5} minDuration={60000}
+ maxTime={this.state.end}
+ minTime={this.state.start}
+ paddingLeft={0} paddingRight={0} paddingBottom={0}
+ enablePanZoom={true} utc={false}
+ onTimeRangeChanged={this.handleTimeRangeChange}>
+
+ <ChartRow height="125">
+ <YAxis id="axis1" hideAxisLine
+ min={this.aggregateSeries("min")}
+ max={this.aggregateSeries("max")} width="35" type="linear" transition={300}/>
+ <Charts>
+ <LineChart axis="axis1" series={this.state.series}
+ columns={Object.keys(this.state.services)}
+ style={this.createStyler()} interpolation="curveBasis"/>
+
+ <MultiBrush
+ timeRanges={[this.state.selection]}
+ allowSelectionClear={false}
+ allowFreeDrawing={false}
+ onTimeRangeChanged={this.handleSelectionChange}
+ />
+ </Charts>
+ </ChartRow>
+ </ChartContainer>
+ </Resizable>
+
+ <div className="metric-selection">
+ <ChoiceField inline small
+ keys={["connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service"]}
+ values={["connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service"]}
+ onChange={(metric) => this.loadStatistics(metric, this.state.filteredPort)
+ .then(() => log.debug("Statistics loaded after metric changes"))}
+ value={this.state.metric}/>
+ </div>
+ </div>
+ </footer>
+ );
+ }
+}
+
+export default withRouter(Timeline);
diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss
new file mode 100644
index 0000000..eeb9d50
--- /dev/null
+++ b/frontend/src/components/Timeline.scss
@@ -0,0 +1,22 @@
+@import "../colors";
+
+.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/components/dialogs/Filters.js b/frontend/src/components/dialogs/Filters.js
new file mode 100644
index 0000000..35c11df
--- /dev/null
+++ b/frontend/src/components/dialogs/Filters.js
@@ -0,0 +1,116 @@
+/*
+ * 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 {Col, Container, Modal, Row, Table} from "react-bootstrap";
+import {filtersDefinitions, filtersNames} from "../filters/FiltersDefinitions";
+import ButtonField from "../fields/ButtonField";
+
+class Filters extends Component {
+
+ constructor(props) {
+ super(props);
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = false);
+ this.state = newState;
+ }
+
+ componentDidMount() {
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
+ this.setState(newState);
+ }
+
+ checkboxChangesHandler(filterName, event) {
+ this.setState({[`${filterName}_active`]: event.target.checked});
+ localStorage.setItem(`filters.${filterName}`, event.target.checked);
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(new Event("quick-filters"));
+ }
+ }
+
+ generateRows(filtersNames) {
+ return filtersNames.map(name =>
+ <tr key={name}>
+ <td><input type="checkbox"
+ checked={this.state[`${name}_active`]}
+ onChange={event => this.checkboxChangesHandler(name, event)}/></td>
+ <td>{filtersDefinitions[name]}</td>
+ </tr>
+ );
+ }
+
+ render() {
+ return (
+ <Modal
+ {...this.props}
+ show="true"
+ size="lg"
+ aria-labelledby="filters-dialog"
+ centered
+ >
+ <Modal.Header>
+ <Modal.Title id="filters-dialog">
+ ~/filters
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Container>
+ <Row>
+ <Col md={6}>
+ <Table borderless size="sm" className="filters-table">
+ <thead>
+ <tr>
+ <th>show</th>
+ <th>filter</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.generateRows(["service_port", "client_address", "min_duration",
+ "min_bytes", "started_after", "closed_after", "marked"])}
+ </tbody>
+ </Table>
+ </Col>
+ <Col md={6}>
+ <Table borderless size="sm" className="filters-table">
+ <thead>
+ <tr>
+ <th>show</th>
+ <th>filter</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.generateRows(["matched_rules", "client_port", "max_duration",
+ "max_bytes", "started_before", "closed_before", "hidden"])}
+ </tbody>
+ </Table>
+ </Col>
+
+ </Row>
+
+
+ </Container>
+ </Modal.Body>
+ <Modal.Footer className="dialog-footer">
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+export default Filters;
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/objects/Connection.js
index c7b0010..5e2beba 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/objects/Connection.js
@@ -18,10 +18,10 @@
import React, {Component} from 'react';
import './Connection.scss';
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";
+import backend from "../../backend";
+import {dateTimeToTime, durationBetween, formatSize} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import LinkPopover from "./LinkPopover";
const classNames = require('classnames');
diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/objects/Connection.scss
index cc1ea96..3b9f479 100644
--- a/frontend/src/components/Connection.scss
+++ b/frontend/src/components/objects/Connection.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.connection {
border-top: 3px solid $color-primary-3;
diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js
index 35643c5..73d5c5d 100644
--- a/frontend/src/components/ConnectionMatchedRules.js
+++ b/frontend/src/components/objects/ConnectionMatchedRules.js
@@ -17,7 +17,7 @@
import React, {Component} from 'react';
import './ConnectionMatchedRules.scss';
-import ButtonField from "./fields/ButtonField";
+import ButtonField from "../fields/ButtonField";
class ConnectionMatchedRules extends Component {
diff --git a/frontend/src/components/ConnectionMatchedRules.scss b/frontend/src/components/objects/ConnectionMatchedRules.scss
index 65d9ac8..f46a914 100644
--- a/frontend/src/components/ConnectionMatchedRules.scss
+++ b/frontend/src/components/objects/ConnectionMatchedRules.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.connection-matches {
background-color: $color-primary-0;
diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/objects/MessageAction.js
index b94cbb9..9f199b7 100644
--- a/frontend/src/components/MessageAction.js
+++ b/frontend/src/components/objects/MessageAction.js
@@ -18,8 +18,8 @@
import React, {Component} from 'react';
import './MessageAction.scss';
import {Modal} from "react-bootstrap";
-import TextField from "./fields/TextField";
-import ButtonField from "./fields/ButtonField";
+import TextField from "../fields/TextField";
+import ButtonField from "../fields/ButtonField";
class MessageAction extends Component {
diff --git a/frontend/src/components/MessageAction.scss b/frontend/src/components/objects/MessageAction.scss
index faa23d3..996007b 100644
--- a/frontend/src/components/MessageAction.scss
+++ b/frontend/src/components/objects/MessageAction.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.message-action-value {
font-size: 13px;
diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/pages/ConfigurationPage.js
index 9ae2cfb..6ab8ae3 100644
--- a/frontend/src/components/panels/ConfigurationPane.js
+++ b/frontend/src/components/pages/ConfigurationPage.js
@@ -16,8 +16,8 @@
*/
import React, {Component} from 'react';
-import './common.scss';
-import './ConfigurationPane.scss';
+import '../panels/common.scss';
+import './ConfigurationPage.scss';
import LinkPopover from "../objects/LinkPopover";
import {Col, Container, Row} from "react-bootstrap";
import InputField from "../fields/InputField";
@@ -29,7 +29,7 @@ import Table from "react-bootstrap/Table";
import validation from "../../validation";
import backend from "../../backend";
-class ConfigurationPane extends Component {
+class ConfigurationPage extends Component {
constructor(props) {
super(props);
@@ -114,7 +114,7 @@ class ConfigurationPane extends Component {
</tr>);
return (
- <div className="configuration-pane">
+ <div className="configuration-page">
<div className="pane">
<div className="pane-container">
<div className="pane-section">
@@ -176,4 +176,4 @@ class ConfigurationPane extends Component {
}
}
-export default ConfigurationPane;
+export default ConfigurationPage;
diff --git a/frontend/src/components/panels/ConfigurationPane.scss b/frontend/src/components/pages/ConfigurationPage.scss
index ef48b34..4509865 100644
--- a/frontend/src/components/panels/ConfigurationPane.scss
+++ b/frontend/src/components/pages/ConfigurationPage.scss
@@ -1,6 +1,6 @@
@import "../../colors";
-.configuration-pane {
+.configuration-page {
display: flex;
align-items: center;
justify-content: center;
diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js
new file mode 100644
index 0000000..7376091
--- /dev/null
+++ b/frontend/src/components/pages/MainPage.js
@@ -0,0 +1,76 @@
+/*
+ * 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 './MainPage.scss';
+import './common.scss';
+import Connections from "../panels/ConnectionsPane";
+import StreamsPane from "../panels/StreamsPane";
+import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
+import Timeline from "../Timeline";
+import PcapsPane from "../panels/PcapsPane";
+import RulesPane from "../panels/RulesPane";
+import ServicesPane from "../panels/ServicesPane";
+import Header from "../Header";
+import Filters from "../dialogs/Filters";
+import MainPane from "../panels/MainPane";
+
+class MainPage extends Component {
+
+ state = {};
+
+ render() {
+ let modal;
+ if (this.state.filterWindowOpen) {
+ modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
+ }
+
+ return (
+ <div className="page main-page">
+ <Router>
+ <div className="page-header">
+ <Header onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
+ </div>
+
+ <div className="page-content">
+ <div className="pane connections-pane">
+ <Connections onSelected={(c) => this.setState({selectedConnection: c})}/>
+ </div>
+ <div className="pane details-pane">
+ <Switch>
+ <Route path="/pcaps" children={<PcapsPane/>}/>
+ <Route path="/rules" children={<RulesPane/>}/>
+ <Route path="/services" children={<ServicesPane/>}/>
+ <Route exact path="/connections/:id"
+ children={<StreamsPane connection={this.state.selectedConnection}/>}/>
+ <Route children={<MainPane/>}/>
+ </Switch>
+ </div>
+
+ {modal}
+ </div>
+
+ <div className="page-footer">
+ <Timeline/>
+ </div>
+ </Router>
+ </div>
+ );
+ }
+}
+
+export default MainPage;
diff --git a/frontend/src/components/pages/MainPage.scss b/frontend/src/components/pages/MainPage.scss
new file mode 100644
index 0000000..3b1a689
--- /dev/null
+++ b/frontend/src/components/pages/MainPage.scss
@@ -0,0 +1,24 @@
+@import "../../colors";
+
+.main-page {
+ .page-content {
+ display: flex;
+ flex: 1;
+ padding: 0 15px;
+ background-color: $color-primary-2;
+
+ .connections-pane {
+ flex: 1 0;
+ margin-right: 7.5px;
+ }
+
+ .details-pane {
+ flex: 1 1;
+ margin-left: 7.5px;
+ }
+ }
+
+ .page-footer {
+ flex: 0;
+ }
+}
diff --git a/frontend/src/components/pages/ServiceUnavailablePage.js b/frontend/src/components/pages/ServiceUnavailablePage.js
new file mode 100644
index 0000000..f27d84d
--- /dev/null
+++ b/frontend/src/components/pages/ServiceUnavailablePage.js
@@ -0,0 +1,34 @@
+/*
+ * 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 './MainPage.scss';
+
+class ServiceUnavailablePage extends Component {
+
+ state = {};
+
+ render() {
+ return (
+ <div className="main-page">
+
+ </div>
+ );
+ }
+}
+
+export default ServiceUnavailablePage;
diff --git a/frontend/src/components/pages/common.scss b/frontend/src/components/pages/common.scss
new file mode 100644
index 0000000..fcf5c20
--- /dev/null
+++ b/frontend/src/components/pages/common.scss
@@ -0,0 +1,16 @@
+.page {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ .page-header,
+ .page-footer {
+ flex: 0;
+ }
+
+ .page-content {
+ overflow: hidden;
+ flex: 1;
+ }
+}
diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js
new file mode 100644
index 0000000..038ef8f
--- /dev/null
+++ b/frontend/src/components/panels/ConnectionsPane.js
@@ -0,0 +1,304 @@
+/*
+ * 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 './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";
+
+class ConnectionsPane extends Component {
+
+ state = {
+ loading: false,
+ connections: [],
+ firstConnection: null,
+ lastConnection: null,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.scrollTopThreashold = 0.00001;
+ this.scrollBottomThreashold = 0.99999;
+ this.maxConnections = 200;
+ this.queryLimit = 50;
+ this.connectionsListRef = React.createRef();
+ this.lastScrollPosition = 0;
+ }
+
+ componentDidMount() {
+ const initialParams = {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;
+ backend.get(`/api/connections/${id}`)
+ .then(res => this.connectionSelected(res.json, false))
+ .catch(error => log.error("Error loading initial connection", error));
+ }
+
+ this.loadConnections(initialParams, true).then(() => log.debug("Connections loaded"));
+
+ dispatcher.register("timeline_updates", 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("notifications", 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"));
+ }
+ });
+ }
+
+ connectionSelected = (c, doRedirect = true) => {
+ this.doSelectedConnectionRedirect = doRedirect;
+ 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;
+ 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,})
+ .then(() => log.info("Following connections loaded"));
+ }
+ if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
+ this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
+ .then(() => log.info("Previous connections loaded"));
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ } else {
+ if (this.lastScrollPosition > e.currentTarget.scrollTop) {
+ if (!this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: true});
+ }
+ } else {
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ }
+ }
+ 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(params, isInitial = false) {
+ const urlParams = new URLSearchParams(this.props.location.search);
+ for (const [name, value] of Object.entries(params)) {
+ urlParams.set(name, value);
+ }
+
+ this.setState({loading: true});
+ if (!this.state.rules) {
+ await this.loadRules();
+ }
+ if (!this.state.services) {
+ await this.loadServices();
+ }
+
+ let res = (await backend.get(`/api/connections?${urlParams}`)).json;
+
+ let connections = this.state.connections;
+ let firstConnection = this.state.firstConnection;
+ let lastConnection = this.state.lastConnection;
+
+ if (params !== undefined && params.from !== undefined && params.to === undefined) {
+ if (res.length > 0) {
+ if (!isInitial) {
+ res = res.slice(1);
+ }
+ connections = this.state.connections.concat(res);
+ lastConnection = connections[connections.length - 1];
+ if (isInitial) {
+ firstConnection = connections[0];
+ }
+ 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 && params.from === undefined) {
+ if (res.length > 0) {
+ 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);
+ lastConnection = connections[this.maxConnections - 1];
+ }
+ }
+ } else {
+ if (res.length > 0) {
+ connections = res;
+ firstConnection = connections[0];
+ lastConnection = connections[connections.length - 1];
+ } else {
+ connections = [];
+ firstConnection = null;
+ lastConnection = null;
+ }
+ }
+
+ this.setState({
+ loading: false,
+ connections: connections,
+ firstConnection: firstConnection,
+ lastConnection: lastConnection
+ });
+
+ if (firstConnection != null && lastConnection != null) {
+ dispatcher.dispatch("connection_updates", {
+ from: new Date(lastConnection["started_at"]),
+ to: new Date(firstConnection["started_at"])
+ });
+ }
+ }
+
+ loadRules = async () => {
+ return backend.get("/api/rules").then(res => this.setState({rules: res.json}));
+ };
+
+ loadServices = async () => {
+ return backend.get("/api/services").then(res => this.setState({services: res.json}));
+ };
+
+ 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;
+ }
+
+ let loading = null;
+ if (this.state.loading) {
+ loading = <tr>
+ <td colSpan={10}>Loading...</td>
+ </tr>;
+ }
+
+ return (
+ <div className="connections-container">
+ {this.state.showMoreRecentButton && <div className="most-recent-button">
+ <ButtonField name="most_recent" variant="green" onClick={() => {
+ this.disableScrollHandler = true;
+ this.connectionsListRef.current.scrollTop = 0;
+ this.loadConnections({limit: this.queryLimit})
+ .then(() => {
+ this.disableScrollHandler = false;
+ log.info("Most recent connections loaded");
+ });
+ }}/>
+ </div>}
+
+ <div className="connections" onScroll={this.handleScroll} ref={this.connectionsListRef}>
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>service</th>
+ <th>srcip</th>
+ <th>srcport</th>
+ <th>dstip</th>
+ <th>dstport</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>up</th>
+ <th>down</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ this.state.connections.flatMap(c => {
+ return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
+ 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}/>
+ ];
+ })
+ }
+ {loading}
+ </tbody>
+ </Table>
+
+ {redirect}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(ConnectionsPane);
diff --git a/frontend/src/components/panels/ConnectionsPane.scss b/frontend/src/components/panels/ConnectionsPane.scss
new file mode 100644
index 0000000..06f5827
--- /dev/null
+++ b/frontend/src/components/panels/ConnectionsPane.scss
@@ -0,0 +1,38 @@
+@import "../../colors";
+
+.connections-container {
+ position: relative;
+ height: 100%;
+ background-color: $color-primary-3;
+
+ .connections {
+ position: relative;
+ overflow-y: scroll;
+ height: 100%;
+
+ .table {
+ margin-bottom: 0;
+ }
+
+ th {
+ font-size: 13.5px;
+ position: sticky;
+ top: 0;
+ padding: 5px;
+ border: none;
+ background-color: $color-primary-3;
+ }
+
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
+ }
+
+ .most-recent-button {
+ position: absolute;
+ z-index: 20;
+ top: 45px;
+ left: calc(50% - 50px);
+ background-color: red;
+ }
+}
diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js
index d34d58a..74c859c 100644
--- a/frontend/src/components/panels/MainPane.js
+++ b/frontend/src/components/panels/MainPane.js
@@ -17,57 +17,22 @@
import React, {Component} from 'react';
import './common.scss';
-import './MainPane.scss';
-import Connections from "../../views/Connections";
-import ConnectionContent from "../ConnectionContent";
-import {Route, Switch, withRouter} from "react-router-dom";
-import PcapPane from "./PcapPane";
-import backend from "../../backend";
-import RulePane from "./RulePane";
-import ServicePane from "./ServicePane";
-import log from "../../log";
+import './ServicesPane.scss';
class MainPane extends Component {
state = {};
- componentDidMount() {
- const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
- if (match != null) {
- this.loading = true;
- backend.get(`/api/connections/${match[1]}`)
- .then(res => {
- this.loading = false;
- this.setState({selectedConnection: res.json});
- log.debug(`Initial connection ${match[1]} loaded`);
- })
- .catch(error => log.error("Error loading initial connection", error));
- }
- }
-
render() {
return (
- <div className="main-pane">
- <div className="pane connections-pane">
- {
- !this.loading &&
- <Connections onSelected={(c) => this.setState({selectedConnection: c})}
- initialConnection={this.state.selectedConnection}/>
- }
- </div>
- <div className="pane details-pane">
- <Switch>
- <Route path="/pcaps" children={<PcapPane/>}/>
- <Route path="/rules" children={<RulePane/>}/>
- <Route path="/services" children={<ServicePane/>}/>
- <Route exact path="/connections/:id"
- children={<ConnectionContent connection={this.state.selectedConnection}/>}/>
- <Route children={<ConnectionContent/>}/>
- </Switch>
+ <div className="pane-container main-pane">
+ <div className="pane-section">
+ MainPane
</div>
</div>
);
}
+
}
-export default withRouter(MainPane);
+export default MainPane;
diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss
index 2973c00..c8460f2 100644
--- a/frontend/src/components/panels/MainPane.scss
+++ b/frontend/src/components/panels/MainPane.scss
@@ -1,22 +1,5 @@
@import "../../colors";
.main-pane {
- display: flex;
- height: 100%;
- padding: 0 15px;
- background-color: $color-primary-2;
- .pane {
- flex: 1;
- }
-
- .connections-pane {
- flex: 1 0;
- margin-right: 7.5px;
- }
-
- .details-pane {
- flex: 1 1;
- margin-left: 7.5px;
- }
}
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapsPane.js
index d5c2225..8722230 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapsPane.js
@@ -16,7 +16,7 @@
*/
import React, {Component} from 'react';
-import './PcapPane.scss';
+import './PcapsPane.scss';
import './common.scss';
import Table from "react-bootstrap/Table";
import backend from "../../backend";
@@ -28,7 +28,7 @@ import ButtonField from "../fields/ButtonField";
import LinkPopover from "../objects/LinkPopover";
import dispatcher from "../../dispatcher";
-class PcapPane extends Component {
+class PcapsPane extends Component {
state = {
sessions: [],
@@ -270,4 +270,4 @@ class PcapPane extends Component {
}
}
-export default PcapPane;
+export default PcapsPane;
diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapsPane.scss
index 4dbc2b2..4dbc2b2 100644
--- a/frontend/src/components/panels/PcapPane.scss
+++ b/frontend/src/components/panels/PcapsPane.scss
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulesPane.js
index 9913962..a66cde7 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulesPane.js
@@ -17,7 +17,7 @@
import React, {Component} from 'react';
import './common.scss';
-import './RulePane.scss';
+import './RulesPane.scss';
import Table from "react-bootstrap/Table";
import {Col, Container, Row} from "react-bootstrap";
import InputField from "../fields/InputField";
@@ -36,7 +36,7 @@ import dispatcher from "../../dispatcher";
const classNames = require('classnames');
const _ = require('lodash');
-class RulePane extends Component {
+class RulesPane extends Component {
emptyRule = {
"name": "",
@@ -435,4 +435,4 @@ class RulePane extends Component {
}
-export default RulePane;
+export default RulesPane;
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulesPane.scss
index 992445a..992445a 100644
--- a/frontend/src/components/panels/RulePane.scss
+++ b/frontend/src/components/panels/RulesPane.scss
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicesPane.js
index fc7004b..bc82356 100644
--- a/frontend/src/components/panels/ServicePane.js
+++ b/frontend/src/components/panels/ServicesPane.js
@@ -17,7 +17,7 @@
import React, {Component} from 'react';
import './common.scss';
-import './ServicePane.scss';
+import './ServicesPane.scss';
import Table from "react-bootstrap/Table";
import {Col, Container, Row} from "react-bootstrap";
import InputField from "../fields/InputField";
@@ -34,7 +34,7 @@ import dispatcher from "../../dispatcher";
const classNames = require('classnames');
const _ = require('lodash');
-class ServicePane extends Component {
+class ServicesPane extends Component {
emptyService = {
"port": 0,
@@ -209,4 +209,4 @@ class ServicePane extends Component {
}
-export default ServicePane;
+export default ServicesPane;
diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicesPane.scss
index daf7e79..daf7e79 100644
--- a/frontend/src/components/panels/ServicePane.scss
+++ b/frontend/src/components/panels/ServicesPane.scss
diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/panels/StreamsPane.js
index b468277..c8bd121 100644
--- a/frontend/src/components/ConnectionContent.js
+++ b/frontend/src/components/panels/StreamsPane.js
@@ -16,47 +16,47 @@
*/
import React, {Component} from 'react';
-import './ConnectionContent.scss';
+import './StreamsPane.scss';
import {Row} from 'react-bootstrap';
-import MessageAction from "./MessageAction";
-import backend from "../backend";
-import ButtonField from "./fields/ButtonField";
-import ChoiceField from "./fields/ChoiceField";
+import MessageAction from "../objects/MessageAction";
+import backend from "../../backend";
+import ButtonField from "../fields/ButtonField";
+import ChoiceField from "../fields/ChoiceField";
import DOMPurify from 'dompurify';
import ReactJson from 'react-json-view'
-import {downloadBlob, getHeaderValue} from "../utils";
-import log from "../log";
+import {downloadBlob, getHeaderValue} from "../../utils";
+import log from "../../log";
const classNames = require('classnames');
-class ConnectionContent extends Component {
+class StreamsPane extends Component {
+
+ state = {
+ messages: [],
+ format: "default",
+ tryParse: true
+ };
constructor(props) {
super(props);
- this.state = {
- loading: false,
- connectionContent: null,
- format: "default",
- tryParse: true,
- messageActionDialog: null
- };
this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
}
componentDidMount() {
- if (this.props.connection != null) {
- this.loadStream();
+ if (this.props.connection && this.state.currentId !== this.props.connection.id) {
+ this.setState({currentId: this.props.connection.id});
+ this.loadStream(this.props.connection.id);
}
document.title = "caronte:~/$";
}
componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.props.connection != null && (
+ if (this.props.connection && (
this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
this.closeRenderWindow();
- this.loadStream();
+ this.loadStream(this.props.connection.id);
}
}
@@ -64,15 +64,10 @@ class ConnectionContent extends Component {
this.closeRenderWindow();
}
- loadStream = () => {
- this.setState({loading: true});
- // TODO: limit workaround.
- backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
- this.setState({
- connectionContent: res.json,
- loading: false
- });
- });
+ loadStream = (connectionId) => {
+ this.setState({messages: []});
+ backend.get(`/api/streams/${connectionId}?format=${this.state.format}`)
+ .then(res => this.setState({messages: res.json}));
};
setFormat = (format) => {
@@ -128,7 +123,7 @@ class ConnectionContent extends Component {
};
connectionsActions = (connectionMessage) => {
- if (connectionMessage.metadata == null) { //} || !connectionMessage.metadata["reproducers"]) {
+ if (!connectionMessage.metadata) {
return null;
}
@@ -169,9 +164,11 @@ class ConnectionContent extends Component {
};
downloadStreamRaw = (value) => {
- backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`)
- .then(res => downloadBlob(res.blob, `${this.props.connection.id}-${value}-${this.state.format}.txt`))
- .catch(_ => log.error("Failed to download stream messages"));
+ if (this.state.currentId) {
+ backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`)
+ .then(res => downloadBlob(res.blob, `${this.state.currentId}-${value}-${this.state.format}.txt`))
+ .catch(_ => log.error("Failed to download stream messages"));
+ }
};
closeRenderWindow = () => {
@@ -181,12 +178,14 @@ class ConnectionContent extends Component {
};
render() {
- const conn = this.props.connection;
- const content = this.state.connectionContent;
-
- if (content == null) {
- return <div>select a connection to view</div>;
- }
+ const conn = this.props.connection || {
+ "ip_src": "0.0.0.0",
+ "ip_dst": "0.0.0.0",
+ "port_src": "0",
+ "port_dst": "0",
+ "started_at": new Date().toISOString(),
+ };
+ const content = this.state.messages || [];
let payload = content.map((c, i) =>
<div key={`content-${i}`}
@@ -240,4 +239,4 @@ class ConnectionContent extends Component {
}
-export default ConnectionContent;
+export default StreamsPane;
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/panels/StreamsPane.scss
index f4edec9..d5510cf 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/panels/StreamsPane.scss
@@ -1,4 +1,4 @@
-@import "../colors.scss";
+@import "../../colors";
.connection-content {
height: 100%;