aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-05 21:46:18 +0000
committerEmiliano Ciavatta2020-10-05 21:46:18 +0000
commite905618113309eaba7227ff1328a20f6846e4afd (patch)
treef6dd471683ac8ed7e630ce84956508ead28eab83
parentf11e5d9e55c963109af8b8517c7790bf2eb7cac8 (diff)
Implement timeline
-rw-r--r--application_context.go2
-rw-r--r--application_router.go22
-rw-r--r--connection_handler.go26
-rw-r--r--connections_controller.go52
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/src/components/filters/FiltersDefinitions.js86
-rw-r--r--frontend/src/components/panels/MainPane.js34
-rw-r--r--frontend/src/globals.js5
-rw-r--r--frontend/src/log.js7
-rw-r--r--frontend/src/views/App.js18
-rw-r--r--frontend/src/views/Connections.js158
-rw-r--r--frontend/src/views/Connections.scss52
-rw-r--r--frontend/src/views/Footer.js173
-rw-r--r--frontend/src/views/Footer.scss17
-rw-r--r--frontend/yarn.lock312
-rw-r--r--statistics_controller.go71
-rw-r--r--storage.go31
17 files changed, 850 insertions, 219 deletions
diff --git a/application_context.go b/application_context.go
index e4be74d..6ea449d 100644
--- a/application_context.go
+++ b/application_context.go
@@ -20,6 +20,7 @@ type ApplicationContext struct {
ConnectionsController ConnectionsController
ServicesController *ServicesController
ConnectionStreamsController ConnectionStreamsController
+ StatisticsController StatisticsController
IsConfigured bool
}
@@ -93,5 +94,6 @@ func (sm *ApplicationContext) configure() {
sm.ServicesController = NewServicesController(sm.Storage)
sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.ServicesController)
sm.ConnectionStreamsController = NewConnectionStreamsController(sm.Storage)
+ sm.StatisticsController = NewStatisticsController(sm.Storage)
sm.IsConfigured = true
}
diff --git a/application_router.go b/application_router.go
index 501956b..8b5e32f 100644
--- a/application_router.go
+++ b/application_router.go
@@ -119,7 +119,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
flushAllValue, isPresent := c.GetPostForm("flush_all")
flushAll := isPresent && strings.ToLower(flushAllValue) == "true"
fileName := fmt.Sprintf("%v-%s", time.Now().UnixNano(), fileHeader.Filename)
- if err := c.SaveUploadedFile(fileHeader, ProcessingPcapsBasePath + fileName); err != nil {
+ if err := c.SaveUploadedFile(fileHeader, ProcessingPcapsBasePath+fileName); err != nil {
log.WithError(err).Panic("failed to save uploaded file")
}
@@ -147,7 +147,7 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
}
fileName := fmt.Sprintf("%v-%s", time.Now().UnixNano(), filepath.Base(request.File))
- if err := CopyFile(ProcessingPcapsBasePath + fileName, request.File); err != nil {
+ if err := CopyFile(ProcessingPcapsBasePath+fileName, request.File); err != nil {
log.WithError(err).Panic("failed to copy pcap file")
}
if sessionID, err := applicationContext.PcapImporter.ImportPcap(fileName, request.FlushAll); err != nil {
@@ -179,9 +179,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
sessionID := c.Param("id")
if _, isPresent := applicationContext.PcapImporter.GetSession(sessionID); isPresent {
if FileExists(PcapsBasePath + sessionID + ".pcap") {
- c.FileAttachment(PcapsBasePath + sessionID + ".pcap", sessionID[:16] + ".pcap")
+ c.FileAttachment(PcapsBasePath+sessionID+".pcap", sessionID[:16]+".pcap")
} else if FileExists(PcapsBasePath + sessionID + ".pcapng") {
- c.FileAttachment(PcapsBasePath + sessionID + ".pcapng", sessionID[:16] + ".pcapng")
+ c.FileAttachment(PcapsBasePath+sessionID+".pcapng", sessionID[:16]+".pcapng")
} else {
log.WithField("sessionID", sessionID).Panic("pcap file not exists")
}
@@ -289,9 +289,17 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
unprocessableEntity(c, err)
}
})
- }
+ api.GET("/statistics", func(c *gin.Context) {
+ var filter StatisticsFilter
+ if err := c.ShouldBindQuery(&filter); err != nil {
+ badRequest(c, err)
+ return
+ }
+ success(c, applicationContext.StatisticsController.GetStatistics(c, filter))
+ })
+ }
return router
}
@@ -352,3 +360,7 @@ func unprocessableEntity(c *gin.Context, err error) {
func notFound(c *gin.Context, obj interface{}) {
c.JSON(http.StatusNotFound, obj)
}
+
+func serverError(c *gin.Context, err error) {
+ c.JSON(http.StatusInternalServerError, UnorderedDocument{"result": "error", "error": err.Error()})
+}
diff --git a/connection_handler.go b/connection_handler.go
index ffe4fac..3d38531 100644
--- a/connection_handler.go
+++ b/connection_handler.go
@@ -2,6 +2,7 @@ package main
import (
"encoding/binary"
+ "fmt"
"github.com/flier/gohs/hyperscan"
"github.com/google/gopacket"
"github.com/google/gopacket/tcpassembly"
@@ -225,6 +226,31 @@ func (ch *connectionHandlerImpl) Complete(handler *StreamHandler) {
log.WithError(err).WithField("connection", connection).Error("failed to update all connections streams")
}
}
+
+ ch.UpdateStatistics(connection)
+}
+
+func (ch *connectionHandlerImpl) UpdateStatistics(connection Connection) {
+ rangeStart := connection.StartedAt.Unix() / 60 // group statistic records by minutes
+ duration := connection.ClosedAt.Sub(connection.StartedAt)
+ // if one of the two parts doesn't close connection, the duration is +infinity or -infinity
+ if duration.Hours() > 1 || duration.Hours() < -1 {
+ duration = 0
+ }
+ servicePort := connection.DestinationPort
+
+ var results interface{}
+ if _, err := ch.Storage().Update(Statistics).Upsert(&results).
+ Filter(OrderedDocument{{"_id", time.Unix(rangeStart*60, 0)}}).OneComplex(UnorderedDocument{
+ "$inc": UnorderedDocument{
+ fmt.Sprintf("connections_per_service.%d", servicePort): 1,
+ fmt.Sprintf("client_bytes_per_service.%d", servicePort): connection.ClientBytes,
+ fmt.Sprintf("server_bytes_per_service.%d", servicePort): connection.ServerBytes,
+ fmt.Sprintf("duration_per_service.%d", servicePort): duration.Milliseconds(),
+ },
+ }); err != nil {
+ log.WithError(err).WithField("connection", connection).Error("failed to update connection statistics")
+ }
}
func (ch *connectionHandlerImpl) Storage() Storage {
diff --git a/connections_controller.go b/connections_controller.go
index 2773506..2894193 100644
--- a/connections_controller.go
+++ b/connections_controller.go
@@ -31,40 +31,40 @@ type Connection struct {
}
type ConnectionsFilter struct {
- From string `form:"from" binding:"omitempty,hexadecimal,len=24"`
- To string `form:"to" binding:"omitempty,hexadecimal,len=24"`
- ServicePort uint16 `form:"service_port"`
- ClientAddress string `form:"client_address" binding:"omitempty,ip"`
- ClientPort uint16 `form:"client_port"`
- MinDuration uint `form:"min_duration"`
- MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"`
- MinBytes uint `form:"min_bytes"`
- MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"`
- StartedAfter int64 `form:"started_after" `
- StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"`
- ClosedAfter int64 `form:"closed_after" `
- ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"`
- Hidden bool `form:"hidden"`
- Marked bool `form:"marked"`
+ From string `form:"from" binding:"omitempty,hexadecimal,len=24"`
+ To string `form:"to" binding:"omitempty,hexadecimal,len=24"`
+ ServicePort uint16 `form:"service_port"`
+ ClientAddress string `form:"client_address" binding:"omitempty,ip"`
+ ClientPort uint16 `form:"client_port"`
+ MinDuration uint `form:"min_duration"`
+ MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"`
+ MinBytes uint `form:"min_bytes"`
+ MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"`
+ StartedAfter int64 `form:"started_after" `
+ StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"`
+ ClosedAfter int64 `form:"closed_after" `
+ ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"`
+ Hidden bool `form:"hidden"`
+ Marked bool `form:"marked"`
MatchedRules []string `form:"matched_rules" binding:"dive,hexadecimal,len=24"`
- Limit int64 `form:"limit"`
+ Limit int64 `form:"limit"`
}
type ConnectionsController struct {
- storage Storage
+ storage Storage
servicesController *ServicesController
}
func NewConnectionsController(storage Storage, servicesController *ServicesController) ConnectionsController {
return ConnectionsController{
- storage: storage,
+ storage: storage,
servicesController: servicesController,
}
}
func (cc ConnectionsController) GetConnections(c context.Context, filter ConnectionsFilter) []Connection {
var connections []Connection
- query := cc.storage.Find(Connections).Context(c).Sort("_id", false)
+ query := cc.storage.Find(Connections).Context(c)
from, _ := RowIDFromHex(filter.From)
if !from.IsZero() {
@@ -73,6 +73,8 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
to, _ := RowIDFromHex(filter.To)
if !to.IsZero() {
query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": to}}})
+ } else {
+ query = query.Sort("_id", false)
}
if filter.ServicePort > 0 {
query = query.Filter(OrderedDocument{{"port_dst", filter.ServicePort}})
@@ -146,6 +148,10 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
}
}
+ if !to.IsZero() {
+ connections = reverseConnections(connections)
+ }
+
return connections
}
@@ -178,3 +184,11 @@ func (cc ConnectionsController) setProperty(c context.Context, id RowID, propert
}
return updated
}
+
+func reverseConnections(connections []Connection) []Connection {
+ for i := 0; i < len(connections)/2; i++ {
+ j := len(connections) - i - 1
+ connections[i], connections[j] = connections[j], connections[i]
+ }
+ return connections
+}
diff --git a/frontend/package.json b/frontend/package.json
index f22fad5..5bc13f1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,8 +14,10 @@
"classnames": "^2.2.6",
"dompurify": "^2.1.1",
"eslint-config-react-app": "^5.2.1",
+ "flux": "^3.1.3",
"lodash": "^4.17.20",
"node-sass": "^4.14.0",
+ "pondjs": "^0.9.0",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
"react-dom": "^16.13.1",
@@ -25,6 +27,7 @@
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"react-tag-autocomplete": "^6.0.0-beta.6",
+ "react-timeseries-charts": "^0.16.1",
"typed.js": "^2.0.11"
},
"scripts": {
diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js
index 02ccb42..d4f2912 100644
--- a/frontend/src/components/filters/FiltersDefinitions.js
+++ b/frontend/src/components/filters/FiltersDefinitions.js
@@ -1,12 +1,4 @@
-import {
- cleanNumber,
- timestampToTime,
- timeToTimestamp,
- validate24HourTime,
- validateIpAddress,
- validateMin,
- validatePort
-} from "../../utils";
+import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils";
import StringConnectionsFilter from "./StringConnectionsFilter";
import React from "react";
import RulesConnectionsFilter from "./RulesConnectionsFilter";
@@ -22,69 +14,69 @@ export const filtersDefinitions = {
replaceFunc={cleanNumber}
validateFunc={validatePort}
key="service_port_filter"
- width={200} />,
- matched_rules: <RulesConnectionsFilter />,
+ width={200}/>,
+ matched_rules: <RulesConnectionsFilter/>,
client_address: <StringConnectionsFilter filterName="client_address"
defaultFilterValue="all_addresses"
validateFunc={validateIpAddress}
key="client_address_filter"
- width={320} />,
+ width={320}/>,
client_port: <StringConnectionsFilter filterName="client_port"
defaultFilterValue="all_ports"
replaceFunc={cleanNumber}
validateFunc={validatePort}
key="client_port_filter"
- width={200} />,
+ width={200}/>,
min_duration: <StringConnectionsFilter filterName="min_duration"
defaultFilterValue="0"
replaceFunc={cleanNumber}
validateFunc={validateMin(0)}
key="min_duration_filter"
- width={200} />,
+ width={200}/>,
max_duration: <StringConnectionsFilter filterName="max_duration"
defaultFilterValue="∞"
replaceFunc={cleanNumber}
key="max_duration_filter"
- width={200} />,
+ width={200}/>,
min_bytes: <StringConnectionsFilter filterName="min_bytes"
defaultFilterValue="0"
replaceFunc={cleanNumber}
validateFunc={validateMin(0)}
key="min_bytes_filter"
- width={200} />,
+ width={200}/>,
max_bytes: <StringConnectionsFilter filterName="max_bytes"
defaultFilterValue="∞"
replaceFunc={cleanNumber}
key="max_bytes_filter"
- width={200} />,
- started_after: <StringConnectionsFilter filterName="started_after"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="started_after_filter"
- width={200} />,
- started_before: <StringConnectionsFilter filterName="started_before"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="started_before_filter"
- width={200} />,
- closed_after: <StringConnectionsFilter filterName="closed_after"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="closed_after_filter"
- width={200} />,
- closed_before: <StringConnectionsFilter filterName="closed_before"
- defaultFilterValue="00:00:00"
- validateFunc={validate24HourTime}
- encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime}
- key="closed_before_filter"
- width={200} />,
- marked: <BooleanConnectionsFilter filterName={"marked"} />,
- hidden: <BooleanConnectionsFilter filterName={"hidden"} />
+ width={200}/>,
+ // started_after: <StringConnectionsFilter filterName="started_after"
+ // defaultFilterValue="00:00:00"
+ // validateFunc={validate24HourTime}
+ // encodeFunc={timeToTimestamp}
+ // decodeFunc={timestampToTime}
+ // key="started_after_filter"
+ // width={230} />,
+ // started_before: <StringConnectionsFilter filterName="started_before"
+ // defaultFilterValue="00:00:00"
+ // validateFunc={validate24HourTime}
+ // encodeFunc={timeToTimestamp}
+ // decodeFunc={timestampToTime}
+ // key="started_before_filter"
+ // width={230} />,
+ // closed_after: <StringConnectionsFilter filterName="closed_after"
+ // defaultFilterValue="00:00:00"
+ // validateFunc={validate24HourTime}
+ // encodeFunc={timeToTimestamp}
+ // decodeFunc={timestampToTime}
+ // key="closed_after_filter"
+ // width={230} />,
+ // closed_before: <StringConnectionsFilter filterName="closed_before"
+ // defaultFilterValue="00:00:00"
+ // validateFunc={validate24HourTime}
+ // encodeFunc={timeToTimestamp}
+ // decodeFunc={timestampToTime}
+ // key="closed_before_filter"
+ // width={230} />,
+ marked: <BooleanConnectionsFilter filterName={"marked"}/>,
+ // hidden: <BooleanConnectionsFilter filterName={"hidden"} />
};
diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js
index 3202d6d..bd25e0c 100644
--- a/frontend/src/components/panels/MainPane.js
+++ b/frontend/src/components/panels/MainPane.js
@@ -8,24 +8,23 @@ import PcapPane from "./PcapPane";
import backend from "../../backend";
import RulePane from "./RulePane";
import ServicePane from "./ServicePane";
+import log from "../../log";
class MainPane extends Component {
- constructor(props) {
- super(props);
- this.state = {
- selectedConnection: null,
- loading: false
- };
- }
+ state = {};
componentDidMount() {
const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
if (match != null) {
- this.setState({loading: true});
+ this.loading = true;
backend.get(`/api/connections/${match[1]}`)
- .then(res => this.setState({selectedConnection: res.json, loading: false}))
- .catch(error => console.log(error));
+ .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));
}
}
@@ -34,18 +33,19 @@ class MainPane extends Component {
<div className="main-pane">
<div className="pane connections-pane">
{
- !this.state.loading &&
+ !this.loading &&
<Connections onSelected={(c) => this.setState({selectedConnection: c})}
- initialConnection={this.state.selectedConnection} />
+ 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 />} />
+ <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>
</div>
diff --git a/frontend/src/globals.js b/frontend/src/globals.js
new file mode 100644
index 0000000..cd4dc64
--- /dev/null
+++ b/frontend/src/globals.js
@@ -0,0 +1,5 @@
+import {Dispatcher} from "flux";
+
+const dispatcher = new Dispatcher();
+
+export default dispatcher;
diff --git a/frontend/src/log.js b/frontend/src/log.js
new file mode 100644
index 0000000..0883962
--- /dev/null
+++ b/frontend/src/log.js
@@ -0,0 +1,7 @@
+const log = {
+ debug: (...obj) => console.info(...obj),
+ info: (...obj) => console.info(...obj),
+ error: (...obj) => console.error(obj)
+};
+
+export default log;
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index abd5209..00d9110 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -7,17 +7,17 @@ import {BrowserRouter as Router} from "react-router-dom";
import Filters from "./Filters";
import backend from "../backend";
import ConfigurationPane from "../components/panels/ConfigurationPane";
+import log from "../log";
class App extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- }
+ state = {};
componentDidMount() {
- backend.get("/api/services").then(_ => this.setState({configured: true}));
+ backend.get("/api/services").then(_ => {
+ log.debug("Caronte is already configured. Loading main..");
+ this.setState({configured: true});
+ });
setInterval(() => {
if (document.title.endsWith("❚")) {
@@ -38,11 +38,11 @@ class App extends Component {
<div className="main">
<Router>
<div className="main-header">
- <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} />
+ <Header onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
</div>
<div className="main-content">
- {this.state.configured ? <MainPane /> :
- <ConfigurationPane onConfigured={() => this.setState({configured: true})} />}
+ {this.state.configured ? <MainPane/> :
+ <ConfigurationPane onConfigured={() => this.setState({configured: true})}/>}
{modal}
</div>
<div className="main-footer">
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index 73979c4..fe655b3 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -6,26 +6,31 @@ import {Redirect} from 'react-router';
import {withRouter} from "react-router-dom";
import backend from "../backend";
import ConnectionMatchedRules from "../components/ConnectionMatchedRules";
+import dispatcher from "../globals";
+import log from "../log";
+import ButtonField from "../components/fields/ButtonField";
class Connections extends Component {
+ state = {
+ loading: false,
+ connections: [],
+ firstConnection: null,
+ lastConnection: null,
+ flagRule: null,
+ rules: null,
+ queryString: null
+ };
+
constructor(props) {
super(props);
- this.state = {
- loading: false,
- connections: [],
- firstConnection: null,
- lastConnection: null,
- prevParams: null,
- flagRule: null,
- rules: null,
- queryString: null
- };
this.scrollTopThreashold = 0.00001;
this.scrollBottomThreashold = 0.99999;
- this.maxConnections = 500;
+ this.maxConnections = 200;
this.queryLimit = 50;
+ this.connectionsListRef = React.createRef();
+ this.lastScrollPosition = 0;
}
componentDidMount() {
@@ -35,6 +40,17 @@ class Connections extends Component {
this.setState({selected: this.props.initialConnection.id});
// TODO: scroll to initial connection
}
+
+ dispatcher.register((payload) => {
+ if (payload.actionType === "timeline-update") {
+ 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}`));
+ }
+ });
}
connectionSelected = (c) => {
@@ -46,7 +62,7 @@ class Connections extends Component {
if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
this.setState({queryString: this.props.location.search});
this.loadConnections({limit: this.queryLimit})
- .then(() => console.log("Connections reloaded after query string update"));
+ .then(() => log.info("Connections reloaded after query string update"));
}
}
@@ -54,12 +70,26 @@ class Connections extends Component {
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(() => console.log("Following connections loaded"));
+ .then(() => log.info("Following connections loaded"));
}
if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
- .then(() => console.log("Previous connections loaded"));
+ .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) => {
@@ -85,14 +115,14 @@ class Connections extends Component {
urlParams.set(name, value);
}
- this.setState({loading: true, prevParams: params});
+ this.setState({loading: true});
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) {
+ if (params !== undefined && params.from !== undefined && params.to === undefined) {
if (res.length > 0) {
connections = this.state.connections.concat(res);
lastConnection = connections[connections.length - 1];
@@ -102,7 +132,7 @@ class Connections extends Component {
firstConnection = connections[0];
}
}
- } else if (params !== undefined && params.to !== undefined) {
+ } else if (params !== undefined && params.to !== undefined && params.from === undefined) {
if (res.length > 0) {
connections = res.concat(this.state.connections);
firstConnection = connections[0];
@@ -112,6 +142,7 @@ class Connections extends Component {
}
}
} else {
+ this.connectionsListRef.current.scrollTop = 0;
if (res.length > 0) {
connections = res;
firstConnection = connections[0];
@@ -124,7 +155,7 @@ class Connections extends Component {
}
let rules = this.state.rules;
- if (rules === null) {
+ if (rules == null) {
rules = (await backend.get("/api/rules")).json;
}
@@ -135,15 +166,21 @@ class Connections extends Component {
firstConnection: firstConnection,
lastConnection: lastConnection
});
+
+ if (firstConnection != null && lastConnection != null) {
+ dispatcher.dispatch({
+ actionType: "connections-update",
+ from: new Date(lastConnection["started_at"]),
+ to: new Date(firstConnection["started_at"])
+ });
+ }
}
render() {
let redirect;
let queryString = this.state.queryString !== null ? this.state.queryString : "";
if (this.state.selected) {
- let format = this.props.match.params.format;
- format = format !== undefined ? "/" + format : "";
- redirect = <Redirect push to={`/connections/${this.state.selected}${format}${queryString}`} />;
+ redirect = <Redirect push to={`/connections/${this.state.selected}${queryString}`}/>;
}
let loading = null;
@@ -154,48 +191,55 @@ class Connections extends Component {
}
return (
- <div className="connections" onScroll={this.handleScroll}>
- <div className="connections-header-padding"/>
- <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} />,
- c.matched_rules.length > 0 &&
+ <div className="connections-container">
+ {this.state.showMoreRecentButton && <div className="most-recent-button">
+ <ButtonField name="most_recent" variant="green" onClick={() =>
+ this.loadConnections({limit: this.queryLimit})
+ .then(() => 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}/>,
+ 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}
+ 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
index c7bd1df..9e2b6ba 100644
--- a/frontend/src/views/Connections.scss
+++ b/frontend/src/views/Connections.scss
@@ -1,37 +1,39 @@
@import "../colors.scss";
-.connections {
+.connections-container {
position: relative;
- overflow: auto;
height: 100%;
- padding: 0 10px;
+ padding: 10px;
background-color: $color-primary-3;
- .table {
- margin-top: 10px;
- }
+ .connections {
+ position: relative;
+ overflow-y: scroll;
+ height: 100%;
- .connections-header-padding {
- position: sticky;
- top: 0;
- right: 0;
- left: 0;
- height: 10px;
- margin-bottom: -10px;
- background-color: $color-primary-3;
- }
+ .table {
+ margin-bottom: 0;
+ }
+
+ th {
+ font-size: 13.5px;
+ position: sticky;
+ top: 0;
+ padding: 5px;
+ border: none;
+ background-color: $color-primary-2;
+ }
- th {
- font-size: 13.5px;
- position: sticky;
- top: 10px;
- padding: 5px;
- border-top: 3px solid $color-primary-3;
- border-bottom: 3px solid $color-primary-3;
- background-color: $color-primary-2;
+ &:hover::-webkit-scrollbar-thumb {
+ background: $color-secondary-2;
+ }
}
- &: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/Footer.js b/frontend/src/views/Footer.js
index 0a3c5a3..ad1e4f7 100644
--- a/frontend/src/views/Footer.js
+++ b/frontend/src/views/Footer.js
@@ -1,19 +1,180 @@
import React, {Component} from 'react';
import './Footer.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 dispatcher from "../globals";
+import {withRouter} from "react-router-dom";
+import log from "../log";
+
class Footer 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((payload) => {
+ if (payload.actionType === "connections-update") {
+ this.setState({
+ selection: new TimeRange(payload.from, payload.to),
+ });
+ }
+ });
+ }
+
+ 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 backend.get("/api/services")).json;
+ 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 series = new TimeSeries({
+ name: "statistics",
+ columns: ["time"].concat(ports),
+ points: metrics.map(m => [new Date(m["range_start"])].concat(ports.map(p => m[metric][p] || 0)))
+ });
+ this.setState({
+ metric,
+ series,
+ services,
+ timeRange: series.range(),
+ });
+ log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`);
+ };
+
+ 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({
+ actionType: "timeline-update",
+ 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() {
return (
- <footer className="footer container-fluid">
- <div className="row">
- <div className="col-12">
- <div className="footer-timeline">timeline - <a href="https://github.com/eciavatta/caronte/issues/12">to be implemented</a></div>
- </div>
+ <footer className="footer">
+ <div className="time-line">
+ {this.state.series &&
+ <>
+ <Resizable>
+ <ChartContainer timeRange={this.state.timeRange} enableDragZoom={false}
+ paddingTop={5} utc minDuration={60000}
+ maxTime={this.state.series.range().end()}
+ minTime={this.state.series.range().begin()}
+ paddingLeft={0} paddingRight={0} paddingBottom={0}
+ enablePanZoom={true}
+ 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 Footer;
+export default withRouter(Footer);
diff --git a/frontend/src/views/Footer.scss b/frontend/src/views/Footer.scss
index 6ec4a62..14360d4 100644
--- a/frontend/src/views/Footer.scss
+++ b/frontend/src/views/Footer.scss
@@ -1,13 +1,22 @@
@import "../colors.scss";
.footer {
- padding: 15px 30px;
+ padding: 15px;
- > .row {
+ .time-line {
+ position: relative;
background-color: $color-primary-0;
+
+ .metric-selection {
+ font-size: 0.8em;
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ }
}
- .footer-timeline {
- height: 100px;
+ svg text {
+ font-family: "Fira Code", monospace !important;
+ fill: $color-primary-4 !important;
}
}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 84d6058..fa150ab 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -909,6 +909,14 @@
"@babel/helper-create-regexp-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4"
+"@babel/polyfill@^7.7.0":
+ version "7.11.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.11.5.tgz#df550b2ec53abbc2ed599367ec59e64c7a707bb5"
+ integrity sha512-FunXnE0Sgpd61pKSj2OSOs1D44rKTD3pGOfGilZ6LGrrIH0QEtJlTjqOqdF8Bs98JmjfGhni2BBkTfv9KcKJ9g==
+ dependencies:
+ core-js "^2.6.5"
+ regenerator-runtime "^0.13.4"
+
"@babel/preset-env@7.9.0":
version "7.9.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
@@ -1725,9 +1733,9 @@
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35":
- version "16.9.49"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
- integrity sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==
+ version "16.9.50"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.50.tgz#cb5f2c22d42de33ca1f5efc6a0959feb784a3a2d"
+ integrity sha512-kPx5YsNnKDJejTk1P+lqThwxN2PczrocwsvqXnjvVvKpFescoY62ZiM3TV7dH1T8lFhlHZF+PE5xUyimUwqEGA==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -2268,6 +2276,11 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+array.prototype.fill@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.fill/-/array.prototype.fill-1.0.2.tgz#ab33207f21d57d1ab2f7f0d1cf122d3419c38ef5"
+ integrity sha1-qzMgfyHVfRqy9/DRzxItNBnDjvU=
+
array.prototype.flat@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
@@ -2535,7 +2548,7 @@ babel-preset-react-app@^9.1.2:
babel-plugin-macros "2.8.0"
babel-plugin-transform-react-remove-prop-types "0.4.24"
-babel-runtime@^6.26.0:
+babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -2981,9 +2994,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
- version "1.0.30001140"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001140.tgz#30dae27599f6ede2603a0962c82e468bca894232"
- integrity sha512-xFtvBtfGrpjTOxTpjP5F2LmN04/ZGfYV8EQzUIC/RmKpdrmzJrjqlJ4ho7sGuAMPko2/Jl08h7x9uObCfBFaAA==
+ version "1.0.30001142"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz#a8518fdb5fee03ad95ac9f32a9a1e5999469c250"
+ integrity sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3251,6 +3264,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
+colorbrewer@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/colorbrewer/-/colorbrewer-1.3.0.tgz#1d7e92a6277e42dc56377911bbd867bdbcb2ff7d"
+ integrity sha512-AzVPpWa+fuO/qY8LxPQjej6F49Lb2Cl+7U9YhPn6y4/SOY6u/EZiXUc7qHzRb6i6fWPStCUdEaU2731QyQKWjg==
+
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
@@ -3429,7 +3447,7 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
-core-js@^2.4.0:
+core-js@^2.4.0, core-js@^2.6.5:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
@@ -3641,9 +3659,9 @@ css-what@2.1:
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
css-what@^3.2.1:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
- integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
+ integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
css.escape@^1.5.1:
version "1.5.1"
@@ -3779,6 +3797,123 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+d3-array@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+ integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-axis@^1.0.8:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
+ integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
+
+d3-collection@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+ integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
+ integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
+
+d3-dispatch@1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
+ integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
+
+d3-ease@1, d3-ease@^1.0.3:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
+ integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
+
+d3-format@1, d3-format@^1.2.0:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
+ integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
+
+d3-interpolate@1, d3-interpolate@^1.1.5:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+ integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-scale-chromatic@^1.1.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
+ integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
+ dependencies:
+ d3-color "1"
+ d3-interpolate "1"
+
+d3-scale@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ integrity sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection-multi@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58"
+ integrity sha1-zWwlQT0EosuXRw54byzYd/PjT1g=
+ dependencies:
+ d3-selection "1"
+ d3-transition "1"
+
+d3-selection@1, d3-selection@^1.1.0:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
+ integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@^2.0.5:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
+ integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@^1.0.7:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+ integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
+d3-timer@1:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
+ integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
+
+d3-transition@1, d3-transition@^1.1.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
+ integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -4041,6 +4176,11 @@ dom-helpers@^5.0.1, dom-helpers@^5.1.0, dom-helpers@^5.1.2:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
+dom-resize@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/dom-resize/-/dom-resize-1.0.3.tgz#df9d71e808171fdb66ee88517b0d1c02cdb98876"
+ integrity sha512-lohasnGy9LABj1Sq7ZPUGIWSYf+4LFUwL0Aev+dAzxSzUiovc+lKnFyrc6M2TycMIoH7674kwKaucRIPZPgJXw==
+
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -4049,6 +4189,11 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -4243,23 +4388,23 @@ error-ex@^1.2.0, error-ex@^1.3.1:
is-arrayish "^0.2.1"
es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
- version "1.17.6"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
- integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
+ integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
- is-callable "^1.2.0"
- is-regex "^1.1.0"
- object-inspect "^1.7.0"
+ is-callable "^1.2.2"
+ is-regex "^1.1.1"
+ object-inspect "^1.8.0"
object-keys "^1.1.1"
- object.assign "^4.1.0"
+ object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
-es-abstract@^1.18.0-next.0:
+es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
@@ -4758,7 +4903,7 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -5238,6 +5383,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@^4.3.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -5437,6 +5590,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+ integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
+
hoist-non-react-statics@^3.1.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -5656,6 +5814,16 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+immutable-devtools@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/immutable-devtools/-/immutable-devtools-0.0.4.tgz#1e7e87f2c7a4f0533955bc4c2922d124bf9129dd"
+ integrity sha1-Hn6H8sek8FM5VbxMKSLRJL+RKd0=
+
+immutable@^3.6.4:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+ integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5809,7 +5977,7 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
+invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -5894,7 +6062,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.0, is-callable@^1.2.2:
+is-callable@^1.1.4, is-callable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
@@ -6081,7 +6249,7 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
-is-regex@^1.0.4, is-regex@^1.1.0, is-regex@^1.1.1:
+is-regex@^1.0.4, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
@@ -7232,6 +7400,11 @@ merge2@^1.2.3:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+ integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -7301,6 +7474,13 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
+ dependencies:
+ dom-walk "^0.1.0"
+
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
@@ -7413,6 +7593,16 @@ mixin-object@^2.0.1:
dependencies:
minimist "^1.2.5"
+moment-duration-format@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-1.3.0.tgz#541771b5f87a049cc65540475d3ad966737d6908"
+ integrity sha1-VBdxtfh6BJzGVUBHXTrZZnN9aQg=
+
+moment@^2.18.1, moment@^2.24.0:
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
+ integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7734,18 +7924,18 @@ object-hash@^2.0.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
-object-inspect@^1.7.0, object-inspect@^1.8.0:
+object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.0.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
- integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
+ integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
dependencies:
define-properties "^1.1.3"
- es-abstract "^1.17.5"
+ es-abstract "^1.18.0-next.1"
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
@@ -8301,6 +8491,17 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
+pondjs@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/pondjs/-/pondjs-0.9.0.tgz#5c931233f8ef56852914eb669506eaac62b0f13b"
+ integrity sha512-fXgEYrWhgC/N3CVuXarG+/q54jar35Mqf7QPdl7z+mX5RkMyyjLfj9fMgY3oHv2hzMo9zkNx2kK62DrZuG3LXg==
+ dependencies:
+ "@babel/polyfill" "^7.7.0"
+ immutable "^3.6.4"
+ immutable-devtools "0.0.4"
+ moment "^2.24.0"
+ underscore "^1.9.1"
+
portfinder@^1.0.25:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -9080,7 +9281,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -9335,6 +9536,18 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+react-hot-loader@4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.1.2.tgz#5e8025f5bc5605506586b46eb2c6cc4006fd54d7"
+ integrity sha512-7EFwgpJOx4AG4pwVifgr/ZNBPAxl2z424nGJPc/APB3F8YtCA3WdYuGlcerRK2C9vYAoqiiLw745IB3wjnzrRQ==
+ dependencies:
+ fast-levenshtein "^2.0.6"
+ global "^4.3.0"
+ hoist-non-react-statics "^2.5.0"
+ prop-types "^15.6.1"
+ react-lifecycles-compat "^3.0.2"
+ shallowequal "^1.0.2"
+
react-input-mask@^3.0.0-alpha.2:
version "3.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-3.0.0-alpha.2.tgz#113102942a557edc7a192e66020b8ce1ba699a5c"
@@ -9359,7 +9572,7 @@ react-json-view@^1.19.1:
react-lifecycles-compat "^3.0.4"
react-textarea-autosize "^6.1.0"
-react-lifecycles-compat@^3.0.4:
+react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -9479,6 +9692,35 @@ react-textarea-autosize@^6.1.0:
dependencies:
prop-types "^15.6.0"
+react-timeseries-charts@^0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/react-timeseries-charts/-/react-timeseries-charts-0.16.1.tgz#46675a41a7806155a0b5a1342e3b811172845a5c"
+ integrity sha512-WK2Pege5/FGrE5ifGX1XTVQOZkobRUV5YMwRQi4+TpYAALL5cBLp+XCuAX8x8agq4AmqChpClPIrANc8pRL5RA==
+ dependencies:
+ array.prototype.fill "^1.0.1"
+ babel-runtime "^6.23.0"
+ colorbrewer "^1.0.0"
+ d3-axis "^1.0.8"
+ d3-ease "^1.0.3"
+ d3-format "^1.2.0"
+ d3-interpolate "^1.1.5"
+ d3-scale "^1.0.6"
+ d3-scale-chromatic "^1.1.1"
+ d3-selection "^1.1.0"
+ d3-selection-multi "^1.0.1"
+ d3-shape "^1.2.0"
+ d3-time "^1.0.7"
+ d3-time-format "^2.0.5"
+ d3-transition "^1.1.0"
+ dom-resize "^1.0.3"
+ invariant "^2.1.1"
+ merge "^1.2.0"
+ moment "^2.18.1"
+ moment-duration-format "^1.3.0"
+ prop-types "^15.5.10"
+ react-hot-loader "4.1.2"
+ underscore "^1.8.3"
+
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@@ -10205,6 +10447,11 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
+shallowequal@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -11108,6 +11355,11 @@ uncontrollable@^7.0.0:
invariant "^2.2.4"
react-lifecycles-compat "^3.0.4"
+underscore@^1.8.3, underscore@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e"
+ integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==
+
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
diff --git a/statistics_controller.go b/statistics_controller.go
new file mode 100644
index 0000000..65c7d58
--- /dev/null
+++ b/statistics_controller.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "time"
+)
+
+type StatisticRecord struct {
+ RangeStart time.Time `json:"range_start" bson:"_id"`
+ ConnectionsPerService map[uint16]int `json:"connections_per_service" bson:"connections_per_service"`
+ ClientBytesPerService map[uint16]int `json:"client_bytes_per_service" bson:"client_bytes_per_service"`
+ ServerBytesPerService map[uint16]int `json:"server_bytes_per_service" bson:"server_bytes_per_service"`
+ DurationPerService map[uint16]int64 `json:"duration_per_service" bson:"duration_per_service"`
+}
+
+type StatisticsFilter struct {
+ RangeFrom time.Time `form:"range_from"`
+ RangeTo time.Time `form:"range_to"`
+ Ports []uint16 `form:"ports"`
+ Metric string `form:"metric"`
+}
+
+type StatisticsController struct {
+ storage Storage
+ metrics []string
+}
+
+func NewStatisticsController(storage Storage) StatisticsController {
+ return StatisticsController{
+ storage: storage,
+ metrics: []string{"connections_per_service", "client_bytes_per_service",
+ "server_bytes_per_service", "duration_per_service"},
+ }
+}
+
+func (sc *StatisticsController) GetStatistics(context context.Context, filter StatisticsFilter) []StatisticRecord {
+ var statisticRecords []StatisticRecord
+ query := sc.storage.Find(Statistics).Context(context)
+ if !filter.RangeFrom.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": filter.RangeFrom}}})
+ }
+ if !filter.RangeTo.IsZero() {
+ query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$gt": filter.RangeTo}}})
+ }
+ for _, port := range filter.Ports {
+ for _, metric := range sc.metrics {
+ if filter.Metric == "" || filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{fmt.Sprintf("%s.%d", metric, port), 1}})
+ }
+ }
+ }
+ if filter.Metric != "" && len(filter.Ports) == 0 {
+ for _, metric := range sc.metrics {
+ if filter.Metric == metric {
+ query = query.Projection(OrderedDocument{{metric, 1}})
+ }
+ }
+ }
+
+ if err := query.All(&statisticRecords); err != nil {
+ log.WithError(err).WithField("filter", filter).Error("failed to retrieve statistics")
+ return []StatisticRecord{}
+ }
+ if statisticRecords == nil {
+ return []StatisticRecord{}
+ }
+
+ return statisticRecords
+}
diff --git a/storage.go b/storage.go
index aced06b..0888ce0 100644
--- a/storage.go
+++ b/storage.go
@@ -17,6 +17,7 @@ const ImportingSessions = "importing_sessions"
const Rules = "rules"
const Settings = "settings"
const Services = "services"
+const Statistics = "statistics"
var ZeroRowID [12]byte
@@ -57,6 +58,7 @@ func NewMongoStorage(uri string, port int, database string) (*MongoStorage, erro
Rules: db.Collection(Rules),
Settings: db.Collection(Settings),
Services: db.Collection(Services),
+ Statistics: db.Collection(Statistics),
}
if _, err := collections[Services].Indexes().CreateOne(ctx, mongo.IndexModel{
@@ -150,6 +152,7 @@ type UpdateOperation interface {
Filter(filter OrderedDocument) UpdateOperation
Upsert(upsertResults *interface{}) UpdateOperation
One(update interface{}) (bool, error)
+ OneComplex(update interface{}) (bool, error)
Many(update interface{}) (int64, error)
}
@@ -200,6 +203,22 @@ func (fo MongoUpdateOperation) One(update interface{}) (bool, error) {
return result.ModifiedCount == 1, nil
}
+func (fo MongoUpdateOperation) OneComplex(update interface{}) (bool, error) {
+ if fo.err != nil {
+ return false, fo.err
+ }
+
+ result, err := fo.collection.UpdateOne(fo.ctx, fo.filter, update, fo.opt)
+ if err != nil {
+ return false, err
+ }
+
+ if fo.upsertResult != nil {
+ *(fo.upsertResult) = result.UpsertedID
+ }
+ return result.ModifiedCount == 1, nil
+}
+
func (fo MongoUpdateOperation) Many(update interface{}) (int64, error) {
if fo.err != nil {
return 0, fo.err
@@ -238,6 +257,7 @@ func (storage *MongoStorage) Update(collectionName string) UpdateOperation {
type FindOperation interface {
Context(ctx context.Context) FindOperation
Filter(filter OrderedDocument) FindOperation
+ Projection(filter OrderedDocument) FindOperation
Sort(field string, ascending bool) FindOperation
Limit(n int64) FindOperation
First(result interface{}) error
@@ -247,6 +267,7 @@ type FindOperation interface {
type MongoFindOperation struct {
collection *mongo.Collection
filter OrderedDocument
+ projection OrderedDocument
ctx context.Context
optFind *options.FindOptions
optFindOne *options.FindOneOptions
@@ -266,6 +287,15 @@ func (fo MongoFindOperation) Filter(filter OrderedDocument) FindOperation {
return fo
}
+func (fo MongoFindOperation) Projection(projection OrderedDocument) FindOperation {
+ for _, elem := range projection {
+ fo.projection = append(fo.projection, primitive.E{Key: elem.Key, Value: elem.Value})
+ }
+ fo.optFindOne.SetProjection(fo.projection)
+ fo.optFind.SetProjection(fo.projection)
+ return fo
+}
+
func (fo MongoFindOperation) Limit(n int64) FindOperation {
fo.optFind.SetLimit(n)
return fo
@@ -321,6 +351,7 @@ func (storage *MongoStorage) Find(collectionName string) FindOperation {
op := MongoFindOperation{
collection: collection,
filter: OrderedDocument{},
+ projection: OrderedDocument{},
optFind: options.Find(),
optFindOne: options.FindOne(),
sorts: OrderedDocument{},