diff options
Diffstat (limited to 'frontend/src/components/filters')
7 files changed, 264 insertions, 367 deletions
diff --git a/frontend/src/components/filters/AdvancedFilters.js b/frontend/src/components/filters/AdvancedFilters.js new file mode 100644 index 0000000..8598185 --- /dev/null +++ b/frontend/src/components/filters/AdvancedFilters.js @@ -0,0 +1,54 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {withRouter} from "react-router-dom"; +import dispatcher from "../../dispatcher"; +import {updateParams} from "../../utils"; +import ButtonField from "../fields/ButtonField"; + +class AdvancedFilters extends Component { + + state = {}; + + componentDidMount() { + this.urlParams = new URLSearchParams(this.props.location.search); + + this.connectionsFiltersCallback = (payload) => { + this.urlParams = updateParams(this.urlParams, payload); + const active = ["client_address", "client_port", "min_duration", "max_duration", "min_bytes", "max_bytes"] + .some((f) => this.urlParams.has(f)); + if (this.state.active !== active) { + this.setState({active}); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); + } + + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); + } + + render() { + return ( + <ButtonField onClick={this.props.onClick} name="advanced_filters" small active={this.state.active}/> + ); + } + +} + +export default withRouter(AdvancedFilters); diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js index 4c5a78a..0355167 100644 --- a/frontend/src/components/filters/BooleanConnectionsFilter.js +++ b/frontend/src/components/filters/BooleanConnectionsFilter.js @@ -1,64 +1,65 @@ -import React, {Component} from 'react'; +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; +import dispatcher from "../../dispatcher"; import CheckField from "../fields/CheckField"; class BooleanConnectionsFilter extends Component { - constructor(props) { - super(props); - this.state = { - filterActive: "false" - }; - - this.filterChanged = this.filterChanged.bind(this); - this.needRedirect = false; - } + state = { + filterActive: "false" + }; componentDidMount() { let params = new URLSearchParams(this.props.location.search); this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()}); + + this.connectionsFiltersCallback = (payload) => { + const name = this.props.filterName; + if (name in payload && this.state.filterActive !== payload[name]) { + this.setState({filterActive: payload[name]}); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - 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()}); - } + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - toBoolean(value) { + toBoolean = (value) => { return value !== null && value.toLowerCase() === "true"; - } + }; - filterChanged() { - this.needRedirect = true; - this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()}); - } + filterChanged = () => { + const newValue = (!this.toBoolean(this.state.filterActive)).toString(); + const urlParams = {}; + urlParams[this.props.filterName] = newValue === "true" ? "true" : null; + dispatcher.dispatch("connections_filters", urlParams); + this.setState({filterActive: newValue}); + }; 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="filter" style={{"width": `${this.props.width}px`}}> <CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName} - onChange={this.filterChanged} /> - {redirect} + onChange={this.filterChanged} small/> </div> ); } diff --git a/frontend/src/components/filters/ExitSearchFilter.js b/frontend/src/components/filters/ExitSearchFilter.js new file mode 100644 index 0000000..0aacfd6 --- /dev/null +++ b/frontend/src/components/filters/ExitSearchFilter.js @@ -0,0 +1,57 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {withRouter} from "react-router-dom"; +import dispatcher from "../../dispatcher"; +import CheckField from "../fields/CheckField"; + +class ExitSearchFilter extends Component { + + state = {}; + + componentDidMount() { + let params = new URLSearchParams(this.props.location.search); + this.setState({performedSearch: params.get("performed_search")}); + + this.connectionsFiltersCallback = (payload) => { + if (this.state.performedSearch !== payload["performed_search"]) { + this.setState({performedSearch: payload["performed_search"]}); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); + } + + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); + } + + render() { + return ( + <> + {this.state.performedSearch && + <div className="filter" style={{"width": `${this.props.width}px`}}> + <CheckField checked={true} name="exit_search" onChange={() => + dispatcher.dispatch("connections_filters", {"performed_search": null})} small/> + </div>} + </> + ); + } + +} + +export default withRouter(ExitSearchFilter); diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js deleted file mode 100644 index 02ccb42..0000000 --- a/frontend/src/components/filters/FiltersDefinitions.js +++ /dev/null @@ -1,90 +0,0 @@ -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} - key="service_port_filter" - width={200} />, - matched_rules: <RulesConnectionsFilter />, - client_address: <StringConnectionsFilter filterName="client_address" - defaultFilterValue="all_addresses" - validateFunc={validateIpAddress} - key="client_address_filter" - width={320} />, - client_port: <StringConnectionsFilter filterName="client_port" - defaultFilterValue="all_ports" - replaceFunc={cleanNumber} - validateFunc={validatePort} - key="client_port_filter" - width={200} />, - min_duration: <StringConnectionsFilter filterName="min_duration" - defaultFilterValue="0" - replaceFunc={cleanNumber} - validateFunc={validateMin(0)} - key="min_duration_filter" - width={200} />, - max_duration: <StringConnectionsFilter filterName="max_duration" - defaultFilterValue="∞" - replaceFunc={cleanNumber} - key="max_duration_filter" - width={200} />, - min_bytes: <StringConnectionsFilter filterName="min_bytes" - defaultFilterValue="0" - replaceFunc={cleanNumber} - validateFunc={validateMin(0)} - key="min_bytes_filter" - 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"} /> -}; diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index 8366189..210ee36 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -1,86 +1,79 @@ -import React, {Component} from 'react'; +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +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 backend from "../../backend"; +import dispatcher from "../../dispatcher"; +import TagField from "../fields/TagField"; -const classNames = require('classnames'); +const classNames = require("classnames"); +const _ = require("lodash"); class RulesConnectionsFilter extends Component { - constructor(props) { - super(props); - this.state = { - mounted: false, - rules: [], - activeRules: [] - }; - - this.needRedirect = false; - } + state = { + rules: [], + activeRules: [] + }; componentDidMount() { - let params = new URLSearchParams(this.props.location.search); + const params = new URLSearchParams(this.props.location.search); let activeRules = params.getAll("matched_rules") || []; - backend.get("/api/rules").then(res => { - let rules = res.json.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}); + backend.get("/api/rules").then((res) => { + let rules = res.json.flatMap((rule) => rule.enabled ? [{id: rule.id, name: rule.name}] : []); + activeRules = rules.filter((rule) => activeRules.some((id) => rule.id === id)); + this.setState({rules, activeRules}); }); - } - 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))}); - } + this.connectionsFiltersCallback = (payload) => { + if ("matched_rules" in payload && !_.isEqual(payload["matched_rules"].sort(), this.state.activeRules.sort())) { + const newRules = this.state.rules.filter((r) => payload["matched_rules"].includes(r.id)); + this.setState({ + activeRules: newRules.map((r) => { + return {id: r.id, name: r.name}; + }) + }); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - onDelete(i) { - const activeRules = this.state.activeRules.slice(0); - activeRules.splice(i, 1); - this.needRedirect = true; - this.setState({ activeRules }); + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - onAddition(rule) { - if (!this.state.activeRules.includes(rule)) { - const activeRules = [].concat(this.state.activeRules, rule); - this.needRedirect = true; + onChange = (activeRules) => { + if (!_.isEqual(activeRules.sort(), this.state.activeRules.sort())) { this.setState({activeRules}); + dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map((r) => r.id)}); } - } + }; 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 + className={classNames("filter", "d-inline-block", {"filter-active": this.state.filterActive === "true"})}> + <div className="filter-rules"> + <TagField tags={this.state.activeRules} onChange={this.onChange} + suggestions={_.differenceWith(this.state.rules, this.state.activeRules, _.isEqual)} + minQueryLength={0} name="matched_rules" inline small placeholder="rule_name"/> </div> - - {redirect} </div> ); } diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss deleted file mode 100644 index 71efd0d..0000000 --- a/frontend/src/components/filters/RulesConnectionsFilter.scss +++ /dev/null @@ -1,118 +0,0 @@ -@import "../../colors"; - -.react-tags { - font-size: 12px; - position: relative; - z-index: 10; - padding: 0 6px; - cursor: text; - border-radius: 4px; - background-color: $color-primary-2; -} - -.react-tags.is-focused { - border-color: #b1b1b1; -} - -.react-tags__selected { - display: inline; -} - -.react-tags__selected-tag { - font-size: 11px; - display: inline-block; - margin: 0 6px 6px 0; - padding: 2px 4px; - color: $color-primary-3; - border: none; - border-radius: 2px; - background: $color-primary-4; -} - -.react-tags__selected-tag::after { - margin-left: 8px; - content: "\2715"; - color: $color-primary-3; -} - -.react-tags__selected-tag:hover, -.react-tags__selected-tag:focus { - border-color: #b1b1b1; -} - -.react-tags__search { - display: inline-block; - max-width: 100%; - padding: 7px 10px; -} - -@media screen and (min-width: 30em) { - .react-tags__search { - position: relative; - } -} - -.react-tags__search-input { - font-size: inherit; - line-height: inherit; - max-width: 100%; - margin: 0; - padding: 0; - color: $color-primary-4; - border: 0; - outline: none; - background-color: $color-primary-2; -} - -.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 { - font-size: 12px; - margin: 4px -1px; - padding: 0; - list-style: none; - color: $color-primary-1; - border-radius: 2px; - background: $color-primary-4; -} - -.react-tags__suggestions li { - padding: 3px 5px; - border-bottom: 1px solid #ddd; -} - -.react-tags__suggestions li mark { - font-weight: 600; - text-decoration: underline; - background: none; -} - -.react-tags__suggestions li:hover { - cursor: pointer; - color: $color-primary-4; - background: $color-primary-0; -} - -.react-tags__suggestions li.is-active { - background: #b7cfe0; -} - -.react-tags__suggestions li.is-disabled { - cursor: auto; - opacity: 0.5; -} diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index f463593..c5d7075 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -1,36 +1,52 @@ -import React, {Component} from 'react'; +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; +import dispatcher from "../../dispatcher"; import InputField from "../fields/InputField"; 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); - } + state = { + fieldValue: "", + filterValue: null, + timeoutHandle: null, + invalidValue: false + }; componentDidMount() { let params = new URLSearchParams(this.props.location.search); this.updateStateFromFilterValue(params.get(this.props.filterName)); + + this.connectionsFiltersCallback = (payload) => { + const name = this.props.filterName; + if (name in payload && this.state.filterValue !== payload[name]) { + this.updateStateFromFilterValue(payload[name]); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - 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); - } + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - updateStateFromFilterValue(filterValue) { + updateStateFromFilterValue = (filterValue) => { if (filterValue !== null) { let fieldValue = filterValue; if (typeof this.props.decodeFunc === "function") { @@ -40,28 +56,28 @@ class StringConnectionsFilter extends Component { fieldValue = this.props.replaceFunc(fieldValue); } if (this.isValueValid(fieldValue)) { - this.setState({ - fieldValue: fieldValue, - filterValue: filterValue - }); + this.setState({fieldValue, filterValue}); } else { - this.setState({ - fieldValue: fieldValue, - invalidValue: true - }); + this.setState({fieldValue, invalidValue: true}); } } else { this.setState({fieldValue: "", filterValue: null}); } - } + }; - isValueValid(value) { + isValueValid = (value) => { return typeof this.props.validateFunc !== "function" || (typeof this.props.validateFunc === "function" && this.props.validateFunc(value)); - } + }; - filterChanged(fieldValue) { - if (this.state.timeoutHandle !== null) { + changeFilterValue = (value) => { + const urlParams = {}; + urlParams[this.props.filterName] = value; + dispatcher.dispatch("connections_filters", urlParams); + }; + + filterChanged = (fieldValue) => { + if (this.state.timeoutHandle) { clearTimeout(this.state.timeoutHandle); } @@ -70,11 +86,11 @@ class StringConnectionsFilter extends Component { } if (fieldValue === "") { - this.needRedirect = true; this.setState({fieldValue: "", filterValue: null, invalidValue: false}); - return; + return this.changeFilterValue(null); } + if (this.isValueValid(fieldValue)) { let filterValue = fieldValue; if (filterValue !== "" && typeof this.props.encodeFunc === "function") { @@ -82,42 +98,26 @@ class StringConnectionsFilter extends Component { } this.setState({ - fieldValue: fieldValue, + fieldValue, timeoutHandle: setTimeout(() => { - this.needRedirect = true; - this.setState({filterValue: filterValue}); + this.setState({filterValue}); + this.changeFilterValue(filterValue); }, 500), invalidValue: false }); } else { - this.needRedirect = true; - this.setState({ - fieldValue: fieldValue, - invalidValue: true - }); + this.setState({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="filter" style={{"width": `${this.props.width}px`}}> <InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName} placeholder={this.props.defaultFilterValue} onChange={this.filterChanged} - value={this.state.fieldValue} inline={true} small={true} /> - {redirect} + value={this.state.fieldValue} inline={this.props.inline} small={this.props.small}/> </div> ); } |