From 9dae0115592424929ffe4045069740365bc91d52 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Sun, 9 Aug 2020 11:22:47 +0200 Subject: Update frontend filters --- frontend/src/components/ServicePortFilter.js | 91 ------------- frontend/src/components/ServicePortFilter.scss | 61 --------- .../components/filters/BooleanConnectionsFilter.js | 72 ++++++++++ .../filters/BooleanConnectionsFilter.scss | 26 ++++ .../src/components/filters/FiltersDefinitions.js | 69 ++++++++++ .../components/filters/RulesConnectionsFilter.js | 90 +++++++++++++ .../components/filters/RulesConnectionsFilter.scss | 136 +++++++++++++++++++ .../components/filters/StringConnectionsFilter.js | 146 +++++++++++++++++++++ .../filters/StringConnectionsFilter.scss | 68 ++++++++++ 9 files changed, 607 insertions(+), 152 deletions(-) delete mode 100644 frontend/src/components/ServicePortFilter.js delete mode 100644 frontend/src/components/ServicePortFilter.scss create mode 100644 frontend/src/components/filters/BooleanConnectionsFilter.js create mode 100644 frontend/src/components/filters/BooleanConnectionsFilter.scss create mode 100644 frontend/src/components/filters/FiltersDefinitions.js create mode 100644 frontend/src/components/filters/RulesConnectionsFilter.js create mode 100644 frontend/src/components/filters/RulesConnectionsFilter.scss create mode 100644 frontend/src/components/filters/StringConnectionsFilter.js create mode 100644 frontend/src/components/filters/StringConnectionsFilter.scss (limited to 'frontend/src/components') 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 = ; - } - let active = this.state.servicePort !== ""; - - return ( -
-
-
- service_port: -
-
- - { active && -
- this.setState({ - servicePort: "", - servicePortUrl: null - })}>del -
- } - - {redirect} -
- ); - } - -} - -export default withRouter(ServicePortFilter); diff --git a/frontend/src/components/ServicePortFilter.scss b/frontend/src/components/ServicePortFilter.scss deleted file mode 100644 index 2b23444..0000000 --- a/frontend/src/components/ServicePortFilter.scss +++ /dev/null @@ -1,61 +0,0 @@ -@import '../colors.scss'; - -.filter { - margin: 0 10px; - position: relative; - - .filter-name-wrapper { - background-color: $color-primary-2; - padding: 3px 7px; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - } - - .filter-name { - font-size: 13px; - } - - .filter-value { - font-size: 13px; - padding-left: 0; - border-radius: 5px; - - &:focus { - background-color: $color-primary-2; - } - } - - &.filter-active { - .filter-name-wrapper { - background-color: $color-primary-4; - color: $color-primary-3; - } - - .filter-value { - font-size: 13px; - background-color: $color-primary-4; - color: $color-primary-3; - - &:focus { - background-color: $color-primary-4; - } - } - } - - .filter-delete { - position: absolute; - right: 10px; - top: 10px; - - z-index: 10; - font-size: 11px; - letter-spacing: -0.5px; - - color: $color-primary-2; - cursor: pointer; - - .filter-delete-icon { - font-weight: 800; - } - } -} \ No newline at end of file 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 = ; + + this.needRedirect = false; + } + + return ( +
+
+ {this.props.filterName} +
+ + {redirect} +
+ ); + } + +} + +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: , + matched_rules: , + client_address: , + client_port: , + min_duration: , + max_duration: , + min_bytes: , + max_bytes: , + started_after: , + started_before: , + closed_after: , + closed_before: , + marked: , + 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 = ; + + this.needRedirect = false; + } + + return ( +
+
+ + suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} /> +
+ + {redirect} +
+ ); + } + +} + +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 = ; + this.needRedirect = false; + } + let active = this.state.filterValue !== null; + + return ( +
+
+
+ {this.props.filterName}: +
+ +
+ + { active && +
+ { + this.needRedirect = true; + this.setState({fieldValue: "", filterValue: null}); + }}>del +
+ } + + {redirect} +
+ ); + } + +} + +export default withRouter(StringConnectionsFilter); diff --git a/frontend/src/components/filters/StringConnectionsFilter.scss b/frontend/src/components/filters/StringConnectionsFilter.scss new file mode 100644 index 0000000..ecc8d0f --- /dev/null +++ b/frontend/src/components/filters/StringConnectionsFilter.scss @@ -0,0 +1,68 @@ +@import '../../colors'; + +.filter { + margin: 0 10px; + position: relative; + + .filter-name-wrapper { + background-color: $color-primary-2; + padding: 3px 7px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + + .filter-name { + font-size: 13px; + } + + .filter-value { + font-size: 13px; + padding-left: 0; + border-radius: 5px; + + &:focus { + background-color: $color-primary-2; + } + } + + &.filter-active { + .filter-name-wrapper { + background-color: $color-primary-4; + color: $color-primary-3; + } + + .filter-value { + background-color: $color-primary-4; + color: $color-primary-3; + } + } + + &.filter-invalid { + .filter-name-wrapper { + background-color: $color-secondary-2; + color: $color-primary-4; + } + + .filter-value { + background-color: $color-secondary-2; + color: $color-primary-4; + } + } + + .filter-delete { + position: absolute; + right: 10px; + top: 10px; + + z-index: 10; + font-size: 11px; + letter-spacing: -0.5px; + + color: $color-primary-2; + cursor: pointer; + + .filter-delete-icon { + font-weight: 800; + } + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2