diff options
author | Emiliano Ciavatta | 2020-10-15 07:54:25 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-10-15 07:54:25 +0000 |
commit | 97b0ee38fcf1e78e66dfe2a2816c95c3c3b10705 (patch) | |
tree | ed36b83d39163d872e55d297caada23092b7fcd4 /frontend | |
parent | 08456e7f2e1c1af6fc8fdbf580c0178a25b93f8b (diff) |
Update filters
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/.eslintrc.js | 216 | ||||
-rw-r--r-- | frontend/src/components/Header.js | 47 | ||||
-rw-r--r-- | frontend/src/components/Header.scss | 9 | ||||
-rw-r--r-- | frontend/src/components/dialogs/Filters.js | 108 | ||||
-rw-r--r-- | frontend/src/components/dialogs/Filters.scss | 5 | ||||
-rw-r--r-- | frontend/src/components/fields/ButtonField.js | 3 | ||||
-rw-r--r-- | frontend/src/components/fields/ButtonField.scss | 7 | ||||
-rw-r--r-- | frontend/src/components/filters/AdvancedFilters.js | 54 | ||||
-rw-r--r-- | frontend/src/components/filters/BooleanConnectionsFilter.js | 2 | ||||
-rw-r--r-- | frontend/src/components/filters/FiltersDefinitions.js | 73 | ||||
-rw-r--r-- | frontend/src/components/filters/StringConnectionsFilter.js | 2 | ||||
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.js | 22 | ||||
-rw-r--r-- | frontend/src/dispatcher.js | 2 | ||||
-rw-r--r-- | frontend/src/index.js | 6 | ||||
-rw-r--r-- | frontend/src/utils.js | 16 |
15 files changed, 158 insertions, 414 deletions
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js deleted file mode 100644 index 16ff228..0000000 --- a/frontend/.eslintrc.js +++ /dev/null @@ -1,216 +0,0 @@ -const OFF = 0, WARN = 1, ERROR = 2; - -module.exports = exports = { - "env": { - "es6": true - }, - - "ecmaFeatures": { - // env=es6 doesn't include modules, which we are using - "modules": true - }, - - "extends": "eslint:recommended", - - "rules": { - // Possible Errors (overrides from recommended set) - "no-extra-parens": ERROR, - "no-unexpected-multiline": ERROR, - // All JSDoc comments must be valid - "valid-jsdoc": [ ERROR, { - "requireReturn": false, - "requireReturnDescription": false, - "requireParamDescription": true, - "prefer": { - "return": "returns" - } - }], - - // Best Practices - - // Allowed a getter without setter, but all setters require getters - "accessor-pairs": [ ERROR, { - "getWithoutSet": false, - "setWithoutGet": true - }], - "block-scoped-var": WARN, - "consistent-return": ERROR, - "curly": ERROR, - "default-case": WARN, - // the dot goes with the property when doing multiline - "dot-location": [ WARN, "property" ], - "dot-notation": WARN, - "eqeqeq": [ ERROR, "smart" ], - "guard-for-in": WARN, - "no-alert": ERROR, - "no-caller": ERROR, - "no-case-declarations": WARN, - "no-div-regex": WARN, - "no-else-return": WARN, - "no-empty-label": WARN, - "no-empty-pattern": WARN, - "no-eq-null": WARN, - "no-eval": ERROR, - "no-extend-native": ERROR, - "no-extra-bind": WARN, - "no-floating-decimal": WARN, - "no-implicit-coercion": [ WARN, { - "boolean": true, - "number": true, - "string": true - }], - "no-implied-eval": ERROR, - "no-invalid-this": ERROR, - "no-iterator": ERROR, - "no-labels": WARN, - "no-lone-blocks": WARN, - "no-loop-func": ERROR, - "no-magic-numbers": WARN, - "no-multi-spaces": ERROR, - "no-multi-str": WARN, - "no-native-reassign": ERROR, - "no-new-func": ERROR, - "no-new-wrappers": ERROR, - "no-new": ERROR, - "no-octal-escape": ERROR, - "no-param-reassign": ERROR, - "no-process-env": WARN, - "no-proto": ERROR, - "no-redeclare": ERROR, - "no-return-assign": ERROR, - "no-script-url": ERROR, - "no-self-compare": ERROR, - "no-throw-literal": ERROR, - "no-unused-expressions": ERROR, - "no-useless-call": ERROR, - "no-useless-concat": ERROR, - "no-void": WARN, - // Produce warnings when something is commented as TODO or FIXME - "no-warning-comments": [ WARN, { - "terms": [ "TODO", "FIXME" ], - "location": "start" - }], - "no-with": WARN, - "radix": WARN, - "vars-on-top": ERROR, - // Enforces the style of wrapped functions - "wrap-iife": [ ERROR, "outside" ], - "yoda": ERROR, - - // Strict Mode - for ES6, never use strict. - "strict": [ ERROR, "never" ], - - // Variables - "init-declarations": [ ERROR, "always" ], - "no-catch-shadow": WARN, - "no-delete-var": ERROR, - "no-label-var": ERROR, - "no-shadow-restricted-names": ERROR, - "no-shadow": WARN, - // We require all vars to be initialized (see init-declarations) - // If we NEED a var to be initialized to undefined, it needs to be explicit - "no-undef-init": OFF, - "no-undef": ERROR, - "no-undefined": OFF, - "no-unused-vars": WARN, - // Disallow hoisting - let & const don't allow hoisting anyhow - "no-use-before-define": ERROR, - - // Node.js and CommonJS - "callback-return": [ WARN, [ "callback", "next" ]], - "global-require": ERROR, - "handle-callback-err": WARN, - "no-mixed-requires": WARN, - "no-new-require": ERROR, - // Use path.concat instead - "no-path-concat": ERROR, - "no-process-exit": ERROR, - "no-restricted-modules": OFF, - "no-sync": WARN, - - // ECMAScript 6 support - "arrow-body-style": [ ERROR, "always" ], - "arrow-parens": [ ERROR, "always" ], - "arrow-spacing": [ ERROR, { "before": true, "after": true }], - "constructor-super": ERROR, - "generator-star-spacing": [ ERROR, "before" ], - "no-arrow-condition": ERROR, - "no-class-assign": ERROR, - "no-const-assign": ERROR, - "no-dupe-class-members": ERROR, - "no-this-before-super": ERROR, - "no-var": WARN, - "object-shorthand": [ WARN, "never" ], - "prefer-arrow-callback": WARN, - "prefer-spread": WARN, - "prefer-template": WARN, - "require-yield": ERROR, - - // Stylistic - everything here is a warning because of style. - "array-bracket-spacing": [ WARN, "always" ], - "block-spacing": [ WARN, "always" ], - "brace-style": [ WARN, "1tbs", { "allowSingleLine": false } ], - "camelcase": WARN, - "comma-spacing": [ WARN, { "before": false, "after": true } ], - "comma-style": [ WARN, "last" ], - "computed-property-spacing": [ WARN, "never" ], - "consistent-this": [ WARN, "self" ], - "eol-last": WARN, - "func-names": WARN, - "func-style": [ WARN, "declaration" ], - "id-length": [ WARN, { "min": 2, "max": 32 } ], - "indent": [ WARN, 4 ], - "jsx-quotes": [ WARN, "prefer-double" ], - "linebreak-style": [ WARN, "unix" ], - "lines-around-comment": [ WARN, { "beforeBlockComment": true } ], - "max-depth": [ WARN, 8 ], - "max-len": [ WARN, 132 ], - "max-nested-callbacks": [ WARN, 8 ], - "max-params": [ WARN, 8 ], - "new-cap": WARN, - "new-parens": WARN, - "no-array-constructor": WARN, - "no-bitwise": OFF, - "no-continue": OFF, - "no-inline-comments": OFF, - "no-lonely-if": WARN, - "no-mixed-spaces-and-tabs": WARN, - "no-multiple-empty-lines": WARN, - "no-negated-condition": OFF, - "no-nested-ternary": WARN, - "no-new-object": WARN, - "no-plusplus": OFF, - "no-spaced-func": WARN, - "no-ternary": OFF, - "no-trailing-spaces": WARN, - "no-underscore-dangle": WARN, - "no-unneeded-ternary": WARN, - "object-curly-spacing": [ WARN, "always" ], - "one-var": OFF, - "operator-assignment": [ WARN, "never" ], - "operator-linebreak": [ WARN, "after" ], - "padded-blocks": [ WARN, "never" ], - "quote-props": [ WARN, "consistent-as-needed" ], - "quotes": [ WARN, "single" ], - "require-jsdoc": [ WARN, { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - "ClassDeclaration": false - } - }], - "semi-spacing": [ WARN, { "before": false, "after": true }], - "semi": [ ERROR, "always" ], - "sort-vars": OFF, - "space-after-keywords": [ WARN, "always" ], - "space-before-blocks": [ WARN, "always" ], - "space-before-function-paren": [ WARN, "never" ], - "space-before-keywords": [ WARN, "always" ], - "space-in-parens": [ WARN, "never" ], - "space-infix-ops": [ WARN, { "int32Hint": true } ], - "space-return-throw-case": ERROR, - "space-unary-ops": ERROR, - "spaced-comment": [ WARN, "always" ], - "wrap-regex": WARN - } -}; diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index b4a2177..119958b 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -18,21 +18,17 @@ import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; -import {filtersDefinitions, filtersNames} from "./filters/FiltersDefinitions"; import {Link, withRouter} from "react-router-dom"; import ButtonField from "./fields/ButtonField"; import ExitSearchFilter from "./filters/ExitSearchFilter"; +import {cleanNumber, validatePort} from "../utils"; +import StringConnectionsFilter from "./filters/StringConnectionsFilter"; +import RulesConnectionsFilter from "./filters/RulesConnectionsFilter"; +import BooleanConnectionsFilter from "./filters/BooleanConnectionsFilter"; +import AdvancedFilters from "./filters/AdvancedFilters"; class Header extends Component { - constructor(props) { - super(props); - let newState = {}; - filtersNames.forEach(elem => newState[`${elem}_active`] = false); - this.state = newState; - this.fetchStateFromLocalStorage = this.fetchStateFromLocalStorage.bind(this); - } - componentDidMount() { const options = { strings: ["caronte$ "], @@ -40,33 +36,13 @@ 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) { - 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 => <React.Fragment key={name}>{filtersDefinitions[name]}</React.Fragment>) - .slice(0, 5); - return ( <header className="header container-fluid"> <div className="row"> @@ -82,14 +58,21 @@ class Header extends Component { <div className="col-auto"> <div className="filters-bar"> - {quickFilters} - <ExitSearchFilter /> + <StringConnectionsFilter filterName="service_port" + defaultFilterValue="all_ports" + replaceFunc={cleanNumber} + validateFunc={validatePort} + key="service_port_filter" + width={200} small inline/> + <RulesConnectionsFilter/> + <BooleanConnectionsFilter filterName={"marked"}/> + <ExitSearchFilter/> + <AdvancedFilters onClick={this.props.onOpenFilters} /> </div> </div> <div className="col"> <div className="header-buttons"> - <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/> <Link to={"/searches" + this.props.location.search}> <ButtonField variant="pink" name="searches" bordered/> </Link> diff --git a/frontend/src/components/Header.scss b/frontend/src/components/Header.scss index e2e8e1c..fff28e6 100644 --- a/frontend/src/components/Header.scss +++ b/frontend/src/components/Header.scss @@ -26,9 +26,16 @@ .filters-bar { padding: 3px 0; - .filter { + .filter, + .button-field { display: inline-block; margin-right: 10px; } + + .button-field button { + font-weight: 400; + padding: 7px 10px; + border-radius: 5px; + } } } diff --git a/frontend/src/components/dialogs/Filters.js b/frontend/src/components/dialogs/Filters.js index dfd554b..d2cce4f 100644 --- a/frontend/src/components/dialogs/Filters.js +++ b/frontend/src/components/dialogs/Filters.js @@ -16,91 +16,63 @@ */ import React, {Component} from 'react'; -import {Col, Container, Modal, Row, Table} from "react-bootstrap"; -import {filtersDefinitions, filtersNames} from "../filters/FiltersDefinitions"; +import {Col, Container, Modal, Row} from "react-bootstrap"; import ButtonField from "../fields/ButtonField"; +import './Filters.scss'; +import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils"; +import StringConnectionsFilter from "../filters/StringConnectionsFilter"; class Filters extends Component { - constructor(props) { - super(props); - let newState = {}; - filtersNames.forEach(elem => newState[`${elem}_active`] = false); - this.state = newState; - } - - 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 key={name}> - <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" + show={true} size="lg" aria-labelledby="filters-dialog" centered > <Modal.Header> <Modal.Title id="filters-dialog"> - ~/filters + ~/advanced_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"])} - </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", "marked"])} - </tbody> - </Table> - </Col> - </Row> - </Container> + <div className="advanced-filters d-flex"> + <div className="flex-fill"> + <StringConnectionsFilter filterName="client_address" + defaultFilterValue="all_addresses" + validateFunc={validateIpAddress} + key="client_address_filter"/> + <StringConnectionsFilter filterName="min_duration" + defaultFilterValue="0" + replaceFunc={cleanNumber} + validateFunc={validateMin(0)} + key="min_duration_filter"/> + <StringConnectionsFilter filterName="min_bytes" + defaultFilterValue="0" + replaceFunc={cleanNumber} + validateFunc={validateMin(0)} + key="min_bytes_filter"/> + </div> + + <div className="flex-fill"> + <StringConnectionsFilter filterName="client_port" + defaultFilterValue="all_ports" + replaceFunc={cleanNumber} + validateFunc={validatePort} + key="client_port_filter"/> + <StringConnectionsFilter filterName="max_duration" + defaultFilterValue="∞" + replaceFunc={cleanNumber} + key="max_duration_filter"/> + <StringConnectionsFilter filterName="max_bytes" + defaultFilterValue="∞" + replaceFunc={cleanNumber} + key="max_bytes_filter"/> + </div> + </div> </Modal.Body> <Modal.Footer className="dialog-footer"> <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> diff --git a/frontend/src/components/dialogs/Filters.scss b/frontend/src/components/dialogs/Filters.scss new file mode 100644 index 0000000..7d09380 --- /dev/null +++ b/frontend/src/components/dialogs/Filters.scss @@ -0,0 +1,5 @@ +.advanced-filters { + .filter { + margin: 10px; + } +} diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js index 193339c..850f837 100644 --- a/frontend/src/components/fields/ButtonField.js +++ b/frontend/src/components/fields/ButtonField.js @@ -55,7 +55,8 @@ class ButtonField extends Component { } return ( - <div className={classNames("field", "button-field", {"field-small": this.props.small})}> + <div className={classNames("field", "button-field", {"field-small": this.props.small}, + {"field-active": this.props.active})}> <button type="button" className={classNames(buttonClassnames)} onClick={handler} style={buttonStyle}>{this.props.name}</button> </div> diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss index 9e46b9f..99afe08 100644 --- a/frontend/src/components/fields/ButtonField.scss +++ b/frontend/src/components/fields/ButtonField.scss @@ -15,6 +15,13 @@ } } + &.field-active { + button { + color: $color-primary-1; + background-color: $color-primary-4; + } + } + .button-variant-red { color: $color-red-light; background-color: $color-red; diff --git a/frontend/src/components/filters/AdvancedFilters.js b/frontend/src/components/filters/AdvancedFilters.js new file mode 100644 index 0000000..f5ba825 --- /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 ButtonField from "../fields/ButtonField"; +import {updateParams} from "../../utils"; + +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: 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 c611a0d..9558323 100644 --- a/frontend/src/components/filters/BooleanConnectionsFilter.js +++ b/frontend/src/components/filters/BooleanConnectionsFilter.js @@ -59,7 +59,7 @@ class BooleanConnectionsFilter extends Component { return ( <div className="filter" style={{"width": `${this.props.width}px`}}> <CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName} - onChange={this.filterChanged}/> + onChange={this.filterChanged} small/> </div> ); } diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js deleted file mode 100644 index 9fb3b18..0000000 --- a/frontend/src/components/filters/FiltersDefinitions.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 {cleanNumber, 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", "marked"]; - -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}/>, - contains_string: <StringConnectionsFilter filterName="contains_string" - defaultFilterValue="" - key="contains_string_filter" - width={320}/>, - marked: <BooleanConnectionsFilter filterName={"marked"}/> -}; diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index c833220..18b3784 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -127,7 +127,7 @@ class StringConnectionsFilter extends Component { <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}/> + value={this.state.fieldValue} inline={this.props.inline} small={this.props.small}/> </div> ); } diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index 33dd7c1..89859e6 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -26,6 +26,7 @@ import log from "../../log"; import ButtonField from "../fields/ButtonField"; import dispatcher from "../../dispatcher"; import {Redirect} from "react-router"; +import {updateParams} from "../../utils"; const classNames = require('classnames'); @@ -67,29 +68,16 @@ class ConnectionsPane extends Component { this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded")); this.connectionsFiltersCallback = payload => { - const params = this.state.urlParams; - const initialParams = params.toString(); - - Object.entries(payload).forEach(([key, value]) => { - if (value == null) { - params.delete(key); - } else if (Array.isArray(value)) { - params.delete(key); - value.forEach(v => params.append(key, v)); - } else { - params.set(key, value); - } - }); - - if (initialParams === params.toString()) { + const newParams = updateParams(this.state.urlParams, payload); + if (this.state.urlParams.toString() === newParams.toString()) { return; } log.debug("Update following url params:", payload); this.queryStringRedirect = true; - this.setState({urlParams}); + this.setState({urlParams: newParams}); - this.loadConnections({limit: this.queryLimit}, urlParams) + this.loadConnections({limit: this.queryLimit}, newParams) .then(() => log.info("ConnectionsPane reloaded after query string update")); }; dispatcher.register("connections_filters", this.connectionsFiltersCallback); diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js index fa08d48..881970f 100644 --- a/frontend/src/dispatcher.js +++ b/frontend/src/dispatcher.js @@ -47,7 +47,7 @@ class Dispatcher { }; unregister = (callback) => { - this.listeners = _.without(callback); + _.remove(this.listeners, l => l.callback === callback); }; } diff --git a/frontend/src/index.js b/frontend/src/index.js index ca1273a..4bc2730 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -26,9 +26,9 @@ import notifications from "./notifications"; notifications.createWebsocket(); ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, + // <React.StrictMode> + <App />, + // </React.StrictMode>, document.getElementById('root') ); diff --git a/frontend/src/utils.js b/frontend/src/utils.js index f333f0d..06414ac 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -146,3 +146,19 @@ export function downloadBlob(blob, fileName) { a.click(); window.URL.revokeObjectURL(url); } + +export function updateParams(urlParams, payload) { + const params = new URLSearchParams(urlParams.toString()); + Object.entries(payload).forEach(([key, value]) => { + if (value == null) { + params.delete(key); + } else if (Array.isArray(value)) { + params.delete(key); + value.forEach(v => params.append(key, v)); + } else { + params.set(key, value); + } + }); + + return params; +} |