diff options
Diffstat (limited to 'frontend/src/views')
-rw-r--r-- | frontend/src/views/App.js | 81 | ||||
-rw-r--r-- | frontend/src/views/App.scss | 16 | ||||
-rw-r--r-- | frontend/src/views/Connections.js | 293 | ||||
-rw-r--r-- | frontend/src/views/Connections.scss | 38 | ||||
-rw-r--r-- | frontend/src/views/Filters.js | 116 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 109 | ||||
-rw-r--r-- | frontend/src/views/Header.scss | 34 | ||||
-rw-r--r-- | frontend/src/views/Timeline.js | 232 | ||||
-rw-r--r-- | frontend/src/views/Timeline.scss | 22 |
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; - } -} |