aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/views')
-rw-r--r--frontend/src/views/App.js81
-rw-r--r--frontend/src/views/App.scss16
-rw-r--r--frontend/src/views/Connections.js293
-rw-r--r--frontend/src/views/Connections.scss38
-rw-r--r--frontend/src/views/Filters.js116
-rw-r--r--frontend/src/views/Header.js109
-rw-r--r--frontend/src/views/Header.scss34
-rw-r--r--frontend/src/views/Timeline.js232
-rw-r--r--frontend/src/views/Timeline.scss22
9 files changed, 0 insertions, 941 deletions
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
deleted file mode 100644
index 8105117..0000000
--- a/frontend/src/views/App.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import './App.scss';
-import Header from "./Header";
-import MainPane from "../components/panels/MainPane";
-import Timeline from "./Timeline";
-import {BrowserRouter as Router} from "react-router-dom";
-import Filters from "./Filters";
-import ConfigurationPane from "../components/panels/ConfigurationPane";
-import Notifications from "../components/Notifications";
-import dispatcher from "../dispatcher";
-
-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() {
- let modal;
- if (this.state.filterWindowOpen && this.state.configured) {
- modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
- }
-
- return (
- <div className="main">
- <Notifications/>
- {this.state.connected &&
- <Router>
- <div className="main-header">
- <Header onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
- </div>
- <div className="main-content">
- {this.state.configured ? <MainPane/> :
- <ConfigurationPane onConfigured={() => this.setState({configured: true})}/>}
- {modal}
- </div>
- <div className="main-footer">
- {this.state.configured && <Timeline/>}
- </div>
- </Router>
- }
- </div>
- );
- }
-}
-
-export default App;
diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss
deleted file mode 100644
index 87661c3..0000000
--- a/frontend/src/views/App.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-
-.main {
- display: flex;
- flex-direction: column;
- height: 100vh;
-
- .main-content {
- overflow: hidden;
- flex: 1 1;
- }
-
- .main-header,
- .main-footer {
- flex: 0 0;
- }
-}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
deleted file mode 100644
index b2edd3f..0000000
--- a/frontend/src/views/Connections.js
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import './Connections.scss';
-import Connection from "../components/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 "../components/ConnectionMatchedRules";
-import log from "../log";
-import ButtonField from "../components/fields/ButtonField";
-import dispatcher from "../dispatcher";
-
-class Connections 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;
- this.doQueryStringRedirect = false;
- this.doSelectedConnectionRedirect = false;
- }
-
- componentDidMount() {
- this.loadConnections({limit: this.queryLimit})
- .then(() => this.setState({loaded: true}));
- if (this.props.initialConnection) {
- this.setState({selected: this.props.initialConnection.id});
- }
-
- 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) => {
- this.doSelectedConnectionRedirect = true;
- this.setState({selected: c.id});
- this.props.onSelected(c);
- };
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
- this.loadConnections({limit: this.queryLimit})
- .then(() => log.info("Connections reloaded after query string update"));
- }
- }
-
- handleScroll = (e) => {
- if (this.disableScrollHandler) {
- this.lastScrollPosition = e.currentTarget.scrollTop;
- return;
- }
-
- let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
- if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
- this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
- .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) {
- let url = "/api/connections";
- 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(`${url}?${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) {
- connections = this.state.connections.concat(res.slice(1));
- lastConnection = connections[connections.length - 1];
- if (connections.length > this.maxConnections) {
- connections = connections.slice(connections.length - this.maxConnections,
- 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={9}>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(Connections);
diff --git a/frontend/src/views/Connections.scss b/frontend/src/views/Connections.scss
deleted file mode 100644
index de06096..0000000
--- a/frontend/src/views/Connections.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-@import "../colors.scss";
-
-.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/views/Filters.js b/frontend/src/views/Filters.js
deleted file mode 100644
index 3dd8280..0000000
--- a/frontend/src/views/Filters.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import {Col, Container, Modal, Row, Table} from "react-bootstrap";
-import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
-import ButtonField from "../components/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/views/Header.js b/frontend/src/views/Header.js
deleted file mode 100644
index 2cfe9fb..0000000
--- a/frontend/src/views/Header.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import Typed from 'typed.js';
-import './Header.scss';
-import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
-import {Link, withRouter} from "react-router-dom";
-import ButtonField from "../components/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">
- <span style={{whiteSpace: 'pre'}} ref={(el) => {
- this.el = el;
- }}/>
- </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/views/Header.scss b/frontend/src/views/Header.scss
deleted file mode 100644
index 0711159..0000000
--- a/frontend/src/views/Header.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-@import "../colors.scss";
-
-.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/views/Timeline.js b/frontend/src/views/Timeline.js
deleted file mode 100644
index ebe3eb9..0000000
--- a/frontend/src/views/Timeline.js
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-import React, {Component} from 'react';
-import './Timeline.scss';
-import {
- ChartContainer,
- ChartRow,
- Charts,
- LineChart,
- MultiBrush,
- Resizable,
- styler,
- YAxis
-} from "react-timeseries-charts";
-import {TimeRange, TimeSeries} from "pondjs";
-import backend from "../backend";
-import ChoiceField from "../components/fields/ChoiceField";
-import {withRouter} from "react-router-dom";
-import log from "../log";
-import dispatcher from "../dispatcher";
-
-const minutes = 60 * 1000;
-
-class Timeline extends Component {
-
- state = {
- metric: "connections_per_service"
- };
-
- constructor() {
- super();
-
- this.disableTimeSeriesChanges = false;
- this.selectionTimeout = null;
- }
-
- filteredPort = () => {
- const urlParams = new URLSearchParams(this.props.location.search);
- return urlParams.get("service_port");
- };
-
- componentDidMount() {
- const filteredPort = this.filteredPort();
- this.setState({filteredPort});
- this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount"));
-
- dispatcher.register("connection_updates", payload => {
- this.setState({
- selection: new TimeRange(payload.from, payload.to),
- });
- });
-
- dispatcher.register("notifications", payload => {
- if (payload.event === "services.edit") {
- this.loadServices().then(() => log.debug("Services reloaded after notification update"));
- }
- });
- }
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- const filteredPort = this.filteredPort();
- if (this.state.filteredPort !== filteredPort) {
- this.setState({filteredPort});
- this.loadStatistics(this.state.metric, filteredPort).then(() =>
- log.debug("Statistics reloaded after filtered port changes"));
- }
- }
-
- loadStatistics = async (metric, filteredPort) => {
- const urlParams = new URLSearchParams();
- urlParams.set("metric", metric);
-
- let services = await this.loadServices();
- if (filteredPort && services[filteredPort]) {
- const service = services[filteredPort];
- services = {};
- services[filteredPort] = service;
- }
-
- const ports = Object.keys(services);
- ports.forEach(s => urlParams.append("ports", s));
-
- const metrics = (await backend.get("/api/statistics?" + urlParams)).json;
- const zeroFilledMetrics = [];
- const toTime = m => new Date(m["range_start"]).getTime();
-
- if (metrics.length > 0) {
- let i = 0;
- for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) {
- if (interval === toTime(metrics[i])) {
- const m = metrics[i++];
- m["range_start"] = new Date(m["range_start"]);
- zeroFilledMetrics.push(m);
- } else {
- const m = {};
- m["range_start"] = new Date(interval);
- m[metric] = {};
- ports.forEach(p => m[metric][p] = 0);
- zeroFilledMetrics.push(m);
- }
- }
- }
-
- const series = new TimeSeries({
- name: "statistics",
- columns: ["time"].concat(ports),
- points: zeroFilledMetrics.map(m => [m["range_start"]].concat(ports.map(p => m[metric][p] || 0)))
- });
- const start = series.range().begin();
- const end = series.range().end();
- start.setTime(start.getTime() - minutes);
- end.setTime(end.getTime() + minutes);
-
- this.setState({
- metric,
- series,
- timeRange: new TimeRange(start, end),
- start,
- end
- });
- log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`);
- };
-
- loadServices = async () => {
- const services = (await backend.get("/api/services")).json;
- this.setState({services});
- return services;
- };
-
- createStyler = () => {
- return styler(Object.keys(this.state.services).map(port => {
- return {key: port, color: this.state.services[port].color, width: 2};
- }));
- };
-
- handleTimeRangeChange = (timeRange) => {
- if (!this.disableTimeSeriesChanges) {
- this.setState({timeRange});
- }
- };
-
- handleSelectionChange = (timeRange) => {
- this.disableTimeSeriesChanges = true;
-
- this.setState({selection: timeRange});
- if (this.selectionTimeout) {
- clearTimeout(this.selectionTimeout);
- }
- this.selectionTimeout = setTimeout(() => {
- dispatcher.dispatch("timeline_updates", {
- from: timeRange.begin(),
- to: timeRange.end()
- });
- this.selectionTimeout = null;
- this.disableTimeSeriesChanges = false;
- }, 1000);
- };
-
- aggregateSeries = (func) => {
- const values = this.state.series.columns().map(c => this.state.series[func](c));
- return Math[func](...values);
- };
-
- render() {
- if (!this.state.series) {
- return null;
- }
-
- return (
- <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/views/Timeline.scss b/frontend/src/views/Timeline.scss
deleted file mode 100644
index 14360d4..0000000
--- a/frontend/src/views/Timeline.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@import "../colors.scss";
-
-.footer {
- padding: 15px;
-
- .time-line {
- position: relative;
- background-color: $color-primary-0;
-
- .metric-selection {
- font-size: 0.8em;
- position: absolute;
- top: 5px;
- right: 10px;
- }
- }
-
- svg text {
- font-family: "Fira Code", monospace !important;
- fill: $color-primary-4 !important;
- }
-}