diff options
Diffstat (limited to 'frontend/src/components')
-rw-r--r-- | frontend/src/components/App.js | 62 | ||||
-rw-r--r-- | frontend/src/components/Header.js | 111 | ||||
-rw-r--r-- | frontend/src/components/Header.scss | 34 | ||||
-rw-r--r-- | frontend/src/components/Timeline.js | 232 | ||||
-rw-r--r-- | frontend/src/components/Timeline.scss | 22 | ||||
-rw-r--r-- | frontend/src/components/dialogs/Filters.js | 116 | ||||
-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.js | 76 | ||||
-rw-r--r-- | frontend/src/components/pages/MainPage.scss | 24 | ||||
-rw-r--r-- | frontend/src/components/pages/ServiceUnavailablePage.js | 34 | ||||
-rw-r--r-- | frontend/src/components/pages/common.scss | 16 | ||||
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.js | 304 | ||||
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.scss | 38 | ||||
-rw-r--r-- | frontend/src/components/panels/MainPane.js | 47 | ||||
-rw-r--r-- | frontend/src/components/panels/MainPane.scss | 17 | ||||
-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%; |