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
---
.../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 ++++++++++
7 files changed, 607 insertions(+)
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/filters')
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