diff options
Diffstat (limited to 'frontend/src/components')
-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 |
8 files changed, 551 insertions, 96 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; } } |