diff options
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/src/components/ServicePortFilter.js | 91 | ||||
-rw-r--r-- | frontend/src/components/filters/BooleanConnectionsFilter.js | 72 | ||||
-rw-r--r-- | frontend/src/components/filters/BooleanConnectionsFilter.scss | 26 | ||||
-rw-r--r-- | frontend/src/components/filters/FiltersDefinitions.js | 69 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.js | 90 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.scss | 136 | ||||
-rw-r--r-- | frontend/src/components/filters/StringConnectionsFilter.js | 146 | ||||
-rw-r--r-- | frontend/src/components/filters/StringConnectionsFilter.scss (renamed from frontend/src/components/ServicePortFilter.scss) | 17 | ||||
-rw-r--r-- | frontend/src/utils.js | 61 | ||||
-rw-r--r-- | frontend/src/views/App.js | 16 | ||||
-rw-r--r-- | frontend/src/views/Connections.js | 19 | ||||
-rw-r--r-- | frontend/src/views/Filters.js | 98 | ||||
-rw-r--r-- | frontend/src/views/Filters.scss | 0 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 37 |
14 files changed, 750 insertions, 128 deletions
diff --git a/frontend/src/components/ServicePortFilter.js b/frontend/src/components/ServicePortFilter.js deleted file mode 100644 index 72f2643..0000000 --- a/frontend/src/components/ServicePortFilter.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, {Component} from 'react'; -import './ServicePortFilter.scss'; -import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; - -class ServicePortFilter extends Component { - - constructor(props) { - super(props); - this.state = { - servicePort: "", - servicePortUrl: null, - timeoutHandle: null - }; - - this.servicePortChanged = this.servicePortChanged.bind(this); - } - - componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - let servicePort = params.get("service_port"); - if (servicePort !== null) { - this.setState({ - servicePort: servicePort, - servicePortUrl: servicePort - }); - } - } - - servicePortChanged(event) { - let value = event.target.value.replace(/[^\d]/gi, ''); - if (value.startsWith("0")) { - return; - } - if (value !== "") { - let port = parseInt(value); - if (port > 65565) { - return; - } - } - - if (this.state.timeoutHandle !== null) { - clearTimeout(this.state.timeoutHandle); - } - this.setState({ - servicePort: value, - timeoutHandle: setTimeout(() => - this.setState({servicePortUrl: value === "" ? null : value}), 300) - }); - } - - render() { - let redirect = null; - let urlParams = new URLSearchParams(this.props.location.search); - if (urlParams.get("service_port") !== this.state.servicePortUrl) { - if (this.state.servicePortUrl !== null) { - urlParams.set("service_port", this.state.servicePortUrl); - } else { - urlParams.delete("service_port"); - } - redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />; - } - let active = this.state.servicePort !== ""; - - return ( - <div className={"filter d-inline-block" + (active ? " filter-active" : "")} - style={{"width": "200px"}}> - <div className="input-group"> - <div className="filter-name-wrapper"> - <span className="filter-name" id="filter-service_port">service_port:</span> - </div> - <input placeholder="all ports" aria-label="service_port" aria-describedby="filter-service_port" - className="form-control filter-value" onChange={this.servicePortChanged} value={this.state.servicePort} /></div> - - { active && - <div className="filter-delete"> - <span className="filter-delete-icon" onClick={() => this.setState({ - servicePort: "", - servicePortUrl: null - })}>del</span> - </div> - } - - {redirect} - </div> - ); - } - -} - -export default withRouter(ServicePortFilter); diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js new file mode 100644 index 0000000..7dea7cf --- /dev/null +++ b/frontend/src/components/filters/BooleanConnectionsFilter.js @@ -0,0 +1,72 @@ +import React, {Component} from 'react'; +import {withRouter} from "react-router-dom"; +import {Redirect} from "react-router"; +import './BooleanConnectionsFilter.scss'; + +const classNames = require('classnames'); + +class BooleanConnectionsFilter extends Component { + + constructor(props) { + super(props); + this.state = { + filterActive: "false" + }; + + this.filterChanged = this.filterChanged.bind(this); + this.needRedirect = false; + } + + componentDidMount() { + let params = new URLSearchParams(this.props.location.search); + this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()}); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + let urlParams = new URLSearchParams(this.props.location.search); + let externalActive = this.toBoolean(urlParams.get(this.props.filterName)); + let filterActive = this.toBoolean(this.state.filterActive); + // if the filterActive state is changed by another component (and not by filterChanged func) and + // the query string is not equals at the filterActive state, update the state of the component + if (this.toBoolean(prevState.filterActive) === filterActive && filterActive !== externalActive) { + this.setState({filterActive: externalActive.toString()}); + } + } + + toBoolean(value) { + return value !== null && value.toLowerCase() === "true"; + } + + filterChanged() { + this.needRedirect = true; + this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()}); + } + + render() { + let redirect = null; + if (this.needRedirect) { + let urlParams = new URLSearchParams(this.props.location.search); + if (this.toBoolean(this.state.filterActive)) { + urlParams.set(this.props.filterName, "true"); + } else { + urlParams.delete(this.props.filterName); + } + redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />; + + this.needRedirect = false; + } + + return ( + <div className={classNames("filter", "d-inline-block", {"filter-active" : this.toBoolean(this.state.filterActive)})}> + <div className="filter-boolean" onClick={this.filterChanged}> + <span>{this.props.filterName}</span> + </div> + + {redirect} + </div> + ); + } + +} + +export default withRouter(BooleanConnectionsFilter); diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.scss b/frontend/src/components/filters/BooleanConnectionsFilter.scss new file mode 100644 index 0000000..f3d8697 --- /dev/null +++ b/frontend/src/components/filters/BooleanConnectionsFilter.scss @@ -0,0 +1,26 @@ +@import '../../colors'; + +.filter { + + .filter-boolean { + padding: 0 10px; + background-color: $color-primary-2; + border-radius: 5px; + cursor: pointer; + height: 34px; + + span { + display: block; + font-size: 13px; + padding: 6px 5px; + } + } + + &.filter-active { + .filter-boolean { + background-color: $color-primary-4; + color: $color-primary-3; + } + } + +} diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js new file mode 100644 index 0000000..a582d02 --- /dev/null +++ b/frontend/src/components/filters/FiltersDefinitions.js @@ -0,0 +1,69 @@ +import { + cleanNumber, + timestampToTime, + timeToTimestamp, + validate24HourTime, + validateIpAddress, + validateMin, + validatePort +} from "../../utils"; +import StringConnectionsFilter from "./StringConnectionsFilter"; +import React from "react"; +import RulesConnectionsFilter from "./RulesConnectionsFilter"; +import BooleanConnectionsFilter from "./BooleanConnectionsFilter"; + + +export const filtersNames = ["service_port", "matched_rules", "client_address", "client_port", + "min_duration", "max_duration", "min_bytes", "max_bytes", "started_after", + "started_before", "closed_after", "closed_before", "marked", "hidden"]; + +export const filtersDefinitions = { + service_port: <StringConnectionsFilter filterName="service_port" + defaultFilterValue="all_ports" + replaceFunc={cleanNumber} + validateFunc={validatePort}/>, + matched_rules: <RulesConnectionsFilter />, + client_address: <StringConnectionsFilter filterName="client_address" + defaultFilterValue="all_addresses" + validateFunc={validateIpAddress} />, + client_port: <StringConnectionsFilter filterName="client_port" + defaultFilterValue="all_ports" + replaceFunc={cleanNumber} + validateFunc={validatePort}/>, + min_duration: <StringConnectionsFilter filterName="min_duration" + defaultFilterValue="0" + replaceFunc={cleanNumber} + validateFunc={validateMin(0)}/>, + max_duration: <StringConnectionsFilter filterName="max_duration" + defaultFilterValue="∞" + replaceFunc={cleanNumber} />, + min_bytes: <StringConnectionsFilter filterName="min_bytes" + defaultFilterValue="0" + replaceFunc={cleanNumber} + validateFunc={validateMin(0)} />, + max_bytes: <StringConnectionsFilter filterName="max_bytes" + defaultFilterValue="∞" + replaceFunc={cleanNumber} />, + started_after: <StringConnectionsFilter filterName="started_after" + defaultFilterValue="00:00:00" + validateFunc={validate24HourTime} + encodeFunc={timeToTimestamp} + decodeFunc={timestampToTime} />, + started_before: <StringConnectionsFilter filterName="started_before" + defaultFilterValue="00:00:00" + validateFunc={validate24HourTime} + encodeFunc={timeToTimestamp} + decodeFunc={timestampToTime} />, + closed_after: <StringConnectionsFilter filterName="closed_after" + defaultFilterValue="00:00:00" + validateFunc={validate24HourTime} + encodeFunc={timeToTimestamp} + decodeFunc={timestampToTime} />, + closed_before: <StringConnectionsFilter filterName="closed_before" + defaultFilterValue="00:00:00" + validateFunc={validate24HourTime} + encodeFunc={timeToTimestamp} + decodeFunc={timestampToTime} />, + marked: <BooleanConnectionsFilter filterName={"marked"} />, + hidden: <BooleanConnectionsFilter filterName={"hidden"} /> +}; diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js new file mode 100644 index 0000000..358085f --- /dev/null +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -0,0 +1,90 @@ +import React, {Component} from 'react'; +import {withRouter} from "react-router-dom"; +import {Redirect} from "react-router"; +import './RulesConnectionsFilter.scss'; +import ReactTags from 'react-tag-autocomplete'; +import axios from 'axios'; + +const classNames = require('classnames'); + +class RulesConnectionsFilter extends Component { + + constructor(props) { + super(props); + this.state = { + mounted: false, + rules: [], + activeRules: [] + }; + + this.needRedirect = false; + } + + componentDidMount() { + let params = new URLSearchParams(this.props.location.search); + let activeRules = params.getAll("matched_rules") || []; + + axios.get("/api/rules").then(res => { + let rules = res.data.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []); + activeRules = rules.filter(rule => activeRules.some(id => rule.id === id)); + this.setState({rules, activeRules, mounted: true}); + }); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + let urlParams = new URLSearchParams(this.props.location.search); + let externalRules = urlParams.getAll("matched_rules") || []; + let activeRules = this.state.activeRules.map(r => r.id); + let compareRules = (first, second) => first.sort().join(",") === second.sort().join(","); + if (this.state.mounted && + compareRules(prevState.activeRules.map(r => r.id), activeRules) && + !compareRules(externalRules, activeRules)) { + this.setState({activeRules: externalRules.map(id => this.state.rules.find(r => r.id === id))}); + } + } + + onDelete(i) { + const activeRules = this.state.activeRules.slice(0); + activeRules.splice(i, 1); + this.needRedirect = true; + this.setState({ activeRules }); + } + + onAddition(rule) { + if (!this.state.activeRules.includes(rule)) { + const activeRules = [].concat(this.state.activeRules, rule); + this.needRedirect = true; + this.setState({activeRules}); + } + } + + render() { + let redirect = null; + + if (this.needRedirect) { + let urlParams = new URLSearchParams(this.props.location.search); + urlParams.delete("matched_rules"); + this.state.activeRules.forEach(rule => urlParams.append("matched_rules", rule.id)); + redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />; + + this.needRedirect = false; + } + + return ( + <div className={classNames("filter", "d-inline-block", {"filter-active" : this.state.filterActive === "true"})}> + <div className="filter-booleanq"> + <ReactTags tags={this.state.activeRules} suggestions={this.state.rules} + onDelete={this.onDelete.bind(this)} onAddition={this.onAddition.bind(this)} + minQueryLength={0} placeholderText="rule_name" + suggestionsFilter={(suggestion, query) => + suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} /> + </div> + + {redirect} + </div> + ); + } + +} + +export default withRouter(RulesConnectionsFilter); diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss new file mode 100644 index 0000000..d75066e --- /dev/null +++ b/frontend/src/components/filters/RulesConnectionsFilter.scss @@ -0,0 +1,136 @@ +@import '../../colors'; + +.react-tags { + position: relative; + padding: 0px 6px; + border-radius: 4px; + + background-color: $color-primary-2; + + + /* shared font styles */ + font-size: 12px; + + /* clicking anywhere will focus the input */ + cursor: text; + + z-index: 10; +} + +.react-tags.is-focused { + border-color: #B1B1B1; +} + +.react-tags__selected { + display: inline; +} + +.react-tags__selected-tag { + display: inline-block; + border: none; + margin: 0 6px 6px 0; + padding: 2px 4px; + border-radius: 2px; + background: $color-primary-4; + color: $color-primary-3; + + font-size: 11px; +} + +.react-tags__selected-tag:after { + content: '\2715'; + color: $color-primary-3; + margin-left: 8px; +} + +.react-tags__selected-tag:hover, +.react-tags__selected-tag:focus { + border-color: #B1B1B1; +} + +.react-tags__search { + display: inline-block; + padding: 7px 10px; + + /* prevent autoresize overflowing the container */ + max-width: 100%; +} + +@media screen and (min-width: 30em) { + + .react-tags__search { + /* this will become the offsetParent for suggestions */ + position: relative; + } + +} + +.react-tags__search-input { + max-width: 100%; + margin: 0; + padding: 0; + border: 0; + outline: none; + + background-color: $color-primary-2; + color: $color-primary-4; + + /* match the font styles */ + font-size: inherit; + line-height: inherit; +} + +.react-tags__search-input::-ms-clear { + display: none; +} + +.react-tags__suggestions { + position: absolute; + top: 100%; + left: 0; + width: 100%; +} + +@media screen and (min-width: 30em) { + + .react-tags__suggestions { + width: 240px; + } + +} + +.react-tags__suggestions ul { + margin: 4px -1px; + padding: 0; + list-style: none; + background: $color-primary-4; + border-radius: 2px; + color: $color-primary-1; + font-size: 12px; +} + +.react-tags__suggestions li { + border-bottom: 1px solid #ddd; + padding: 3px 5px; +} + +.react-tags__suggestions li mark { + text-decoration: underline; + background: none; + font-weight: 600; +} + +.react-tags__suggestions li:hover { + cursor: pointer; + background: $color-primary-0; + color: $color-primary-4; +} + +.react-tags__suggestions li.is-active { + background: #b7cfe0; +} + +.react-tags__suggestions li.is-disabled { + opacity: 0.5; + cursor: auto; +}
\ No newline at end of file diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js new file mode 100644 index 0000000..490a569 --- /dev/null +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -0,0 +1,146 @@ +import React, {Component} from 'react'; +import {withRouter} from "react-router-dom"; +import {Redirect} from "react-router"; +import './StringConnectionsFilter.scss'; + +const classNames = require('classnames'); + +class StringConnectionsFilter extends Component { + + constructor(props) { + super(props); + this.state = { + fieldValue: "", + filterValue: null, + timeoutHandle: null, + invalidValue: false + }; + this.needRedirect = false; + this.filterChanged = this.filterChanged.bind(this); + } + + componentDidMount() { + let params = new URLSearchParams(this.props.location.search); + this.updateStateFromFilterValue(params.get(this.props.filterName)); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + let urlParams = new URLSearchParams(this.props.location.search); + let filterValue = urlParams.get(this.props.filterName); + if (prevState.filterValue === this.state.filterValue && this.state.filterValue !== filterValue) { + this.updateStateFromFilterValue(filterValue); + } + } + + updateStateFromFilterValue(filterValue) { + if (filterValue !== null) { + let fieldValue = filterValue; + if (typeof this.props.decodeFunc === "function") { + fieldValue = this.props.decodeFunc(filterValue); + } + if (typeof this.props.replaceFunc === "function") { + fieldValue = this.props.replaceFunc(fieldValue); + } + if (this.isValueValid(fieldValue)) { + this.setState({ + fieldValue: fieldValue, + filterValue: filterValue + }); + } else { + this.setState({ + fieldValue: fieldValue, + invalidValue: true + }); + } + } else { + this.setState({fieldValue: "", filterValue: null}); + } + } + + isValueValid(value) { + return typeof this.props.validateFunc !== "function" || + (typeof this.props.validateFunc === "function" && this.props.validateFunc(value)); + } + + filterChanged(event) { + let fieldValue = event.target.value; + if (this.state.timeoutHandle !== null) { + clearTimeout(this.state.timeoutHandle); + } + + if (typeof this.props.replaceFunc === "function") { + fieldValue = this.props.replaceFunc(fieldValue); + } + + if (fieldValue === "") { + this.needRedirect = true; + this.setState({fieldValue: "", filterValue: null, invalidValue: false}); + return; + } + + if (this.isValueValid(fieldValue)) { + let filterValue = fieldValue; + if (filterValue !== "" && typeof this.props.encodeFunc === "function") { + filterValue = this.props.encodeFunc(filterValue); + } + + this.setState({ + fieldValue: fieldValue, + timeoutHandle: setTimeout(() => { + this.needRedirect = true; + this.setState({filterValue: filterValue}); + }, 500), + invalidValue: false + }); + } else { + this.needRedirect = true; + this.setState({ + fieldValue: fieldValue, + invalidValue: true + }); + } + } + + render() { + let redirect = null; + if (this.needRedirect) { + let urlParams = new URLSearchParams(this.props.location.search); + if (this.state.filterValue !== null) { + urlParams.set(this.props.filterName, this.state.filterValue); + } else { + urlParams.delete(this.props.filterName); + } + redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />; + this.needRedirect = false; + } + let active = this.state.filterValue !== null; + + return ( + <div className={classNames("filter", "d-inline-block", {"filter-active" : active}, + {"filter-invalid": this.state.invalidValue})} style={{"width": "200px"}}> + <div className="input-group"> + <div className="filter-name-wrapper"> + <span className="filter-name" id={`filter-${this.props.filterName}`}>{this.props.filterName}:</span> + </div> + <input placeholder={this.props.defaultFilterValue} aria-label={this.props.filterName} + aria-describedby={`filter-${this.props.filterName}`} className="form-control filter-value" + onChange={this.filterChanged} value={this.state.fieldValue} /> + </div> + + { active && + <div className="filter-delete"> + <span className="filter-delete-icon" onClick={() => { + this.needRedirect = true; + this.setState({fieldValue: "", filterValue: null}); + }}>del</span> + </div> + } + + {redirect} + </div> + ); + } + +} + +export default withRouter(StringConnectionsFilter); diff --git a/frontend/src/components/ServicePortFilter.scss b/frontend/src/components/filters/StringConnectionsFilter.scss index 2b23444..ecc8d0f 100644 --- a/frontend/src/components/ServicePortFilter.scss +++ b/frontend/src/components/filters/StringConnectionsFilter.scss @@ -1,4 +1,4 @@ -@import '../colors.scss'; +@import '../../colors'; .filter { margin: 0 10px; @@ -32,13 +32,20 @@ } .filter-value { - font-size: 13px; background-color: $color-primary-4; color: $color-primary-3; + } + } + + &.filter-invalid { + .filter-name-wrapper { + background-color: $color-secondary-2; + color: $color-primary-4; + } - &:focus { - background-color: $color-primary-4; - } + .filter-value { + background-color: $color-secondary-2; + color: $color-primary-4; } } diff --git a/frontend/src/utils.js b/frontend/src/utils.js index db9405c..26c10d3 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -1,14 +1,61 @@ +const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/; + export function createCurlCommand(subCommand, data) { let full = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); return `curl --request PUT \\\n --url ${full}/api${subCommand} \\\n ` + `--header 'content-type: application/json' \\\n --data '${JSON.stringify(data)}'`; } -export function objectToQueryString(obj) { - let str = []; - for (let p in obj) - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - return str.join("&"); +export function validateIpAddress(ipAddress) { + let regex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/; + return regex.test(ipAddress); +} + +export function validate24HourTime(time) { + return timeRegex.test(time); +} + +export function cleanNumber(number) { + return number.replace(/[^\d]/gi, "").replace(/^0+/g, ""); +} + +export function validateMin(min) { + return function (value) { + return parseInt(value) > min; + }; +} + +export function validateMax(max) { + return function (value) { + return parseInt(value) < max; + }; +} + +export function validatePort(port) { + return validateMin(0)(port) && validateMax(65565)(port); +} + +export function timeToTimestamp(time) { + let d = new Date(); + let matches = time.match(timeRegex); + + if (matches[1] !== undefined) { + d.setHours(matches[1]); + } + if (matches[2] !== undefined) { + d.setMinutes(matches[2]); + } + if (matches[3] !== undefined) { + d.setSeconds(matches[3]); + } + + return Math.round(d.getTime() / 1000); +} + +export function timestampToTime(timestamp) { + let d = new Date(timestamp * 1000); + let hours = d.getHours(); + let minutes = "0" + d.getMinutes(); + let seconds = "0" + d.getSeconds(); + return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); } diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index e3119aa..14ff7bf 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -5,26 +5,32 @@ import MainPane from "./MainPane"; import Footer from "./Footer"; import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; import Services from "./Services"; +import Filters from "./Filters"; class App extends Component { constructor(props) { super(props); this.state = { - servicesShow: false + servicesWindowOpen: false, + filterWindowOpen: false }; } render() { - let modal = ""; - if (this.state.servicesShow) { - modal = <Services onHide={() => this.setState({servicesShow: false})}/>; + let modal; + if (this.state.servicesWindowOpen) { + modal = <Services onHide={() => this.setState({servicesWindowOpen: false})}/>; + } + if (this.state.filterWindowOpen) { + modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>; } return ( <div className="app"> <Router> - <Header onOpenServices={() => this.setState({servicesShow: true})}/> + <Header onOpenServices={() => this.setState({servicesWindowOpen: true})} + onOpenFilters={() => this.setState({filterWindowOpen: true})}/> <Switch> <Route path="/connections/:id" children={<MainPane/>}/> <Route path="/" children={<MainPane/>}/> diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index 9b9fe35..62733d7 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import './Connections.scss'; -import axios from 'axios' +import axios from 'axios'; import Connection from "../components/Connection"; import Table from 'react-bootstrap/Table'; import {Redirect} from 'react-router'; @@ -15,7 +15,6 @@ class Connections extends Component { connections: [], firstConnection: null, lastConnection: null, - showHidden: false, prevParams: null, flagRule: null, rules: null, @@ -33,7 +32,7 @@ class Connections extends Component { } componentDidMount() { - this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}) + this.loadConnections({limit: this.queryLimit}) .then(() => this.setState({loaded: true})); } @@ -45,7 +44,7 @@ class Connections extends Component { componentDidUpdate(prevProps, prevState, snapshot) { if (this.state.loaded && prevProps.location.search !== this.props.location.search) { this.setState({queryString: this.props.location.search}); - this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}) + this.loadConnections({limit: this.queryLimit}) .then(() => console.log("Connections reloaded after query string update")); } } @@ -53,16 +52,12 @@ class Connections extends Component { handleScroll(e) { 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, - hidden: this.state.showHidden - }).then(() => console.log("Following connections loaded")); + this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,}) + .then(() => console.log("Following connections loaded")); } if (!this.state.loading && relativeScroll < this.scrollTopThreashold) { - this.loadConnections({ - to: this.state.firstConnection.id, limit: this.queryLimit, - hidden: this.state.showHidden - }).then(() => console.log("Previous connections loaded")); + this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,}) + .then(() => console.log("Previous connections loaded")); } } diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js new file mode 100644 index 0000000..23d3a00 --- /dev/null +++ b/frontend/src/views/Filters.js @@ -0,0 +1,98 @@ +import React, {Component} from 'react'; +import './Services.scss'; +import {Button, Col, Container, Modal, Row, Table} from "react-bootstrap"; +import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; + +class Filters extends Component { + + constructor(props) { + super(props); + this.state = {}; + filtersNames.forEach(elem => this.state[`${elem}_active`] = false); + } + + 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> + <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"> + <Button variant="red" onClick={this.props.onHide}>close</Button> + </Modal.Footer> + </Modal> + ); + } +} + +export default Filters; diff --git a/frontend/src/views/Filters.scss b/frontend/src/views/Filters.scss new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/frontend/src/views/Filters.scss diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index 5118ec3..007be74 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -2,15 +2,18 @@ import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; import {Button} from "react-bootstrap"; -import ServicePortFilter from "../components/ServicePortFilter"; +import StringConnectionsFilter from "../components/filters/StringConnectionsFilter"; +import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../utils"; +import RulesConnectionsFilter from "../components/filters/RulesConnectionsFilter"; +import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; class Header extends Component { constructor(props) { super(props); - this.state = { - servicesShow: false - }; + this.state = {}; + filtersNames.forEach(elem => this.state[`${elem}_active`] = false); + this.fetchStateFromLocalStorage = this.fetchStateFromLocalStorage.bind(this); } componentDidMount() { @@ -20,13 +23,33 @@ class Header extends Component { 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 => filtersDefinitions[name]) + .slice(0, 5); + return ( <header className="header container-fluid"> <div className="row"> @@ -41,16 +64,14 @@ class Header extends Component { <div className="col-auto"> <div className="filters-bar-wrapper"> <div className="filters-bar"> - <ServicePortFilter /> - {/*<ServicePortFilter name="started_before" default="infinity" />*/} - {/*<ServicePortFilter name="started_after" default="-infinity" />*/} - + {quickFilters} </div> </div> </div> <div className="col"> <div className="header-buttons"> + <Button onClick={this.props.onOpenFilters}>filters</Button> <Button variant="yellow" size="sm">pcaps</Button> <Button variant="blue">rules</Button> <Button variant="red" onClick={this.props.onOpenServices}> |