aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-12 18:08:45 +0000
committerEmiliano Ciavatta2020-10-12 18:08:45 +0000
commita44b70943ea4fce61261fc5fadf84a2a98fd2435 (patch)
treeb5425d07fa87e8aae027107a3e6008628167b9c9
parent828aeef9a7333aaabeaf9324a85aac56348b3805 (diff)
Add search pane on frontend
-rw-r--r--connections_controller.go2
-rw-r--r--frontend/src/components/Header.js9
-rw-r--r--frontend/src/components/fields/CheckField.js2
-rw-r--r--frontend/src/components/fields/TagField.js78
-rw-r--r--frontend/src/components/fields/TagField.scss120
-rw-r--r--frontend/src/components/filters/ExitSearchFilter.js52
-rw-r--r--frontend/src/components/filters/FiltersDispatcher.js57
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js2
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.scss193
-rw-r--r--frontend/src/components/pages/MainPage.js5
-rw-r--r--frontend/src/components/panels/SearchPane.js293
-rw-r--r--frontend/src/components/panels/SearchPane.scss51
12 files changed, 764 insertions, 100 deletions
diff --git a/connections_controller.go b/connections_controller.go
index a293a80..924cb53 100644
--- a/connections_controller.go
+++ b/connections_controller.go
@@ -151,7 +151,7 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect
performedSearchID, _ := RowIDFromHex(filter.PerformedSearch)
if !performedSearchID.IsZero() {
performedSearch := cc.searchController.GetPerformedSearch(performedSearchID)
- if !performedSearch.ID.IsZero() {
+ if !performedSearch.ID.IsZero() && len(performedSearch.AffectedConnections) > 0 {
query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$in": performedSearch.AffectedConnections}}})
}
}
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
index 4d29364..b72b532 100644
--- a/frontend/src/components/Header.js
+++ b/frontend/src/components/Header.js
@@ -21,6 +21,7 @@ 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";
class Header extends Component {
@@ -50,7 +51,7 @@ class Header extends Component {
componentWillUnmount() {
this.typed.destroy();
- if (typeof window !== "undefined") {
+ if (typeof window) {
window.removeEventListener("quick-filters", this.fetchStateFromLocalStorage);
}
}
@@ -82,12 +83,16 @@ class Header extends Component {
<div className="col-auto">
<div className="filters-bar">
{quickFilters}
+ <ExitSearchFilter />
</div>
</div>
<div className="col">
<div className="header-buttons">
- <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/>
+ {/*<ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered/>*/}
+ <Link to={"/searches" + this.props.location.search}>
+ <ButtonField variant="pink" name="searches" bordered/>
+ </Link>
<Link to={"/pcaps" + this.props.location.search}>
<ButtonField variant="purple" name="pcaps" bordered/>
</Link>
diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js
index dd44970..a0e2706 100644
--- a/frontend/src/components/fields/CheckField.js
+++ b/frontend/src/components/fields/CheckField.js
@@ -35,7 +35,7 @@ class CheckField extends Component {
const small = this.props.small || false;
const name = this.props.name || null;
const handler = () => {
- if (this.props.onChange) {
+ if (!this.props.readonly && this.props.onChange) {
this.props.onChange(!checked);
}
};
diff --git a/frontend/src/components/fields/TagField.js b/frontend/src/components/fields/TagField.js
new file mode 100644
index 0000000..f1a48bd
--- /dev/null
+++ b/frontend/src/components/fields/TagField.js
@@ -0,0 +1,78 @@
+/*
+ * 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 './TagField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+import ReactTags from "react-tag-autocomplete";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class TagField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ state = {
+
+ };
+
+ onAddition = (tag) => {
+ if (typeof this.props.onChange === "function") {
+ const tags = [].concat(this.wrappedTags(), tag);
+ this.props.onChange(tags.map(t => t.name), true, tag); // true == addition
+ }
+ };
+
+ onDelete = (i) => {
+ if (typeof this.props.onChange === "function") {
+ const tags = this.wrappedTags();
+ const tag = tags[i];
+ tags.splice(i, 1);
+ this.props.onChange(tags.map(t => t.name), true, tag); // false == delete
+ }
+ };
+
+ wrappedTags = () => this.props.tags.map(t => new Object({"name": t}));
+
+ render() {
+ const small = this.props.small || false;
+ const name = this.props.name || null;
+
+ return (
+ <div className={classNames( "field", "tag-field", {"field-small": small})}>
+ { name &&
+ <div className="field-name">
+ <label>{name}:</label>
+ </div>
+ }
+ <ReactTags tags={this.wrappedTags() || []}
+ autoresize={false}
+ allowNew={this.props.allowNew || true}
+ onDelete={this.onDelete} onAddition={this.onAddition}
+ minQueryLength={this.props.min} placeholderText={this.props.placeholder || ""} />
+ </div>
+ );
+ }
+}
+
+export default TagField;
diff --git a/frontend/src/components/fields/TagField.scss b/frontend/src/components/fields/TagField.scss
new file mode 100644
index 0000000..e77db97
--- /dev/null
+++ b/frontend/src/components/fields/TagField.scss
@@ -0,0 +1,120 @@
+@import "../../colors.scss";
+
+.tag-field {
+ .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;
+ }
+} \ No newline at end of file
diff --git a/frontend/src/components/filters/ExitSearchFilter.js b/frontend/src/components/filters/ExitSearchFilter.js
new file mode 100644
index 0000000..cfee298
--- /dev/null
+++ b/frontend/src/components/filters/ExitSearchFilter.js
@@ -0,0 +1,52 @@
+/*
+ * 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 CheckField from "../fields/CheckField";
+import dispatcher from "../../dispatcher";
+
+class ExitSearchFilter extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ let params = new URLSearchParams(this.props.location.search);
+ this.setState({performedSearch: params.get("performed_search")});
+
+ dispatcher.register("connections_filters", payload => {
+ if (this.state.performedSearch !== payload["performed_search"]) {
+ this.setState({performedSearch: payload["performed_search"]});
+ }
+ });
+ }
+
+ 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/FiltersDispatcher.js b/frontend/src/components/filters/FiltersDispatcher.js
new file mode 100644
index 0000000..3769055
--- /dev/null
+++ b/frontend/src/components/filters/FiltersDispatcher.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 {Redirect} from "react-router";
+import dispatcher from "../../dispatcher";
+
+class FiltersDispatcher extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ let params = new URLSearchParams(this.props.location.search);
+ this.setState({params});
+
+ dispatcher.register("connections_filters", payload => {
+ const params = this.state.params;
+
+ Object.entries(payload).forEach(([key, value]) => {
+ if (value == null) {
+ params.delete(key);
+ } else {
+ params.set(key, value);
+ }
+ });
+
+ this.needRedirect = true;
+ this.setState({params});
+ });
+ }
+
+ render() {
+ if (this.needRedirect) {
+ this.needRedirect = false;
+ return <Redirect push to={`${this.props.location.pathname}?${this.state.params}`}/>;
+ }
+
+ return null;
+ }
+}
+
+export default withRouter(FiltersDispatcher);
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 48affb0..fc0ad4d 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -89,7 +89,7 @@ class RulesConnectionsFilter extends Component {
return (
<div className={classNames("filter", "d-inline-block", {"filter-active" : this.state.filterActive === "true"})}>
- <div className="filter-booleanq">
+ <div className="filter-rules">
<ReactTags tags={this.state.activeRules} suggestions={this.state.rules}
onDelete={this.onDelete.bind(this)} onAddition={this.onAddition.bind(this)}
minQueryLength={0} placeholderText="rule_name"
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss
index 71efd0d..0bb4952 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.scss
+++ b/frontend/src/components/filters/RulesConnectionsFilter.scss
@@ -1,118 +1,121 @@
@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;
-}
+.filter-rules {
+ .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__selected {
- display: inline;
-}
+ .react-tags.is-focused {
+ border-color: #b1b1b1;
+ }
-.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 {
+ display: inline;
+ }
-.react-tags__selected-tag::after {
- margin-left: 8px;
- content: "\2715";
- color: $color-primary-3;
-}
+ .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:hover,
-.react-tags__selected-tag:focus {
- border-color: #b1b1b1;
-}
+ .react-tags__selected-tag::after {
+ margin-left: 8px;
+ content: "\2715";
+ color: $color-primary-3;
+ }
-.react-tags__search {
- display: inline-block;
- max-width: 100%;
- padding: 7px 10px;
-}
+ .react-tags__selected-tag:hover,
+ .react-tags__selected-tag:focus {
+ border-color: #b1b1b1;
+ }
-@media screen and (min-width: 30em) {
.react-tags__search {
- position: relative;
+ display: inline-block;
+ max-width: 100%;
+ padding: 7px 10px;
}
-}
-.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;
-}
+ @media screen and (min-width: 30em) {
+ .react-tags__search {
+ position: relative;
+ }
+ }
-.react-tags__search-input::-ms-clear {
- display: none;
-}
+ .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__suggestions {
- position: absolute;
- top: 100%;
- left: 0;
- width: 100%;
-}
+ .react-tags__search-input::-ms-clear {
+ display: none;
+ }
-@media screen and (min-width: 30em) {
.react-tags__suggestions {
- width: 240px;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
}
-}
-.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;
-}
+ @media screen and (min-width: 30em) {
+ .react-tags__suggestions {
+ width: 240px;
+ }
+ }
-.react-tags__suggestions li {
- padding: 3px 5px;
- border-bottom: 1px solid #ddd;
-}
+ .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 mark {
- font-weight: 600;
- text-decoration: underline;
- background: none;
-}
+ .react-tags__suggestions li {
+ padding: 3px 5px;
+ border-bottom: 1px solid #ddd;
+ }
-.react-tags__suggestions li:hover {
- cursor: pointer;
- color: $color-primary-4;
- background: $color-primary-0;
-}
+ .react-tags__suggestions li mark {
+ font-weight: 600;
+ text-decoration: underline;
+ background: none;
+ }
-.react-tags__suggestions li.is-active {
- background: #b7cfe0;
-}
+ .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;
+ }
-.react-tags__suggestions li.is-disabled {
- cursor: auto;
- opacity: 0.5;
}
diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js
index 4632bbd..da57e1a 100644
--- a/frontend/src/components/pages/MainPage.js
+++ b/frontend/src/components/pages/MainPage.js
@@ -28,6 +28,8 @@ import ServicesPane from "../panels/ServicesPane";
import Header from "../Header";
import Filters from "../dialogs/Filters";
import MainPane from "../panels/MainPane";
+import SearchPane from "../panels/SearchPane";
+import FiltersDispatcher from "../filters/FiltersDispatcher";
class MainPage extends Component {
@@ -52,6 +54,7 @@ class MainPage extends Component {
</div>
<div className="pane details-pane">
<Switch>
+ <Route path="/searches" children={<SearchPane/>}/>
<Route path="/pcaps" children={<PcapsPane/>}/>
<Route path="/rules" children={<RulesPane/>}/>
<Route path="/services" children={<ServicesPane/>}/>
@@ -67,6 +70,8 @@ class MainPage extends Component {
<div className="page-footer">
<Timeline/>
</div>
+
+ <FiltersDispatcher />
</Router>
</div>
);
diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js
new file mode 100644
index 0000000..21ba139
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.js
@@ -0,0 +1,293 @@
+/*
+ * 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 './common.scss';
+import './SearchPane.scss';
+import Table from "react-bootstrap/Table";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import ButtonField from "../fields/ButtonField";
+import LinkPopover from "../objects/LinkPopover";
+import {createCurlCommand, dateTimeToTime, durationBetween} from "../../utils";
+import dispatcher from "../../dispatcher";
+import TagField from "../fields/TagField";
+import CheckField from "../fields/CheckField";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class SearchPane extends Component {
+
+ searchOptions = {
+ "text_search": {
+ "terms": null,
+ "excluded_terms": null,
+ "exact_phrase": "",
+ "case_sensitive": false
+ },
+ "regex_search": {
+ "pattern": "",
+ "not_pattern": "",
+ "case_insensitive": false,
+ "multi_line": false,
+ "ignore_whitespaces": false,
+ "dot_character": false
+ },
+ "timeout": 10
+ };
+
+ state = {
+ searches: [],
+ currentSearchOptions: this.searchOptions,
+ };
+
+ componentDidMount() {
+ this.reset();
+ this.loadSearches();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "searches.new") {
+ this.loadSearches();
+ }
+ });
+
+ document.title = "caronte:~/searches$";
+ }
+
+ loadSearches = () => {
+ backend.get("/api/searches")
+ .then(res => this.setState({searches: res.json, searchesStatusCode: res.status}))
+ .catch(res => this.setState({searchesStatusCode: res.status, searchesResponse: JSON.stringify(res.json)}));
+ };
+
+ performSearch = () => {
+ const options = this.state.currentSearchOptions;
+ if (this.validateSearch(options)) {
+ backend.post("/api/searches/perform", options).then(res => {
+ this.reset();
+ this.setState({searchStatusCode: res.status});
+ this.loadSearches();
+ this.viewSearch(res.json.id);
+ }).catch(res => {
+ this.setState({searchStatusCode: res.status, searchResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ reset = () => {
+ this.setState({
+ currentSearchOptions: _.cloneDeep(this.searchOptions),
+ exactPhraseError: null,
+ patternError: null,
+ notPatternError: null,
+ searchStatusCode: null,
+ searchesStatusCode: null,
+ searchResponse: null,
+ searchesResponse: null
+ });
+ };
+
+ validateSearch = (options) => {
+ let valid = true;
+ if (options.text_search.exact_phrase && options.text_search.exact_phrase.length < 3) {
+ this.setState({exactPhraseError: "text_search.exact_phrase.length < 3"});
+ valid = false;
+ }
+ if (options.regex_search.pattern && options.regex_search.pattern.length < 3) {
+ this.setState({patternError: "regex_search.pattern.length < 3"});
+ valid = false;
+ }
+ if (options.regex_search.not_pattern && options.regex_search.not_pattern.length < 3) {
+ this.setState({exactPhraseError: "regex_search.not_pattern.length < 3"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentSearchOptions);
+ this.setState({currentSearchOptions: this.state.currentSearchOptions});
+ };
+
+ extractPattern = (options) => {
+ let pattern = "";
+ if (_.isEqual(options.regex_search, this.searchOptions.regex_search)) { // is text search
+ if (options.text_search.exact_phrase) {
+ pattern += `"${options.text_search.exact_phrase}"`;
+ } else {
+ pattern += options.text_search.terms.join(" ");
+ if (options.text_search.excluded_terms) {
+ pattern += " -" + options.text_search.excluded_terms.join(" -");
+ }
+ }
+ options.text_search.case_sensitive && (pattern += "/s");
+ } else { // is regex search
+ if (options.regex_search.pattern) {
+ pattern += "/" + options.regex_search.pattern + "/";
+ } else {
+ pattern += "!/" + options.regex_search.not_pattern + "/";
+ }
+ options.regex_search.case_insensitive && (pattern += "i");
+ options.regex_search.multi_line && (pattern += "m");
+ options.regex_search.ignore_whitespaces && (pattern += "x");
+ options.regex_search.dot_character && (pattern += "s");
+ }
+
+ return pattern;
+ };
+
+ viewSearch = (searchId) => {
+ dispatcher.dispatch("connections_filters", {"performed_search": searchId});
+ };
+
+ render() {
+ const options = this.state.currentSearchOptions;
+
+ let searches = this.state.searches.map(s =>
+ <tr key={s.id} className={classNames("row-small", "row-clickable", {"row-selected": false})}>
+ <td>{s.id.substring(0, 8)}</td>
+ <td>{this.extractPattern(s["search_options"])}</td>
+ <td>{s["affected_connections_count"]}</td>
+ <td>{dateTimeToTime(s["started_at"])}</td>
+ <td>{durationBetween(s["started_at"], s["finished_at"])}</td>
+ <td><ButtonField name="view" variant="green" small onClick={() => this.viewSearch(s.id)}/></td>
+ </tr>
+ );
+
+ const textOptionsModified = !_.isEqual(this.searchOptions.text_search, options.text_search);
+ const regexOptionsModified = !_.isEqual(this.searchOptions.regex_search, options.regex_search);
+
+ const curlCommand = createCurlCommand("/searches/perform", "POST", options);
+
+ return (
+ <div className="pane-container search-pane">
+ <div className="pane-section searches-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/searches</span>
+ {this.state.searchesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.searchesStatusCode}
+ content={this.state.searchesResponse}
+ placement="left"/></span>}
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>id</th>
+ <th>pattern</th>
+ <th>occurrences</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {searches}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="pane-section search-new">
+ <div className="section-header">
+ <span className="api-request">POST /api/searches/perform</span>
+ <span className="api-response"><LinkPopover text={this.state.searchStatusCode}
+ content={this.state.searchResponse}
+ placement="left"/></span>
+ </div>
+
+ <div className="section-content">
+ <span className="notes">
+ NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex.
+ </span>
+
+ <div className="content-row">
+ <div className="text-search">
+ <TagField tags={options.text_search.terms || []} name="terms" min={3} inline
+ readonly={regexOptionsModified || options.text_search.exact_phrase}
+ onChange={(tags) => this.updateParam(s => s.text_search.terms = tags)}/>
+ <TagField tags={options.text_search.excluded_terms || []} inline
+ name="excluded_terms" min={3}
+ readonly={regexOptionsModified || options.text_search.exact_phrase}
+ onChange={(tags) => this.updateParam(s => s.text_search.excluded_terms = tags)}/>
+
+ <span className="exclusive-separator">or</span>
+
+ <InputField name="exact_phrase" value={options.text_search.exact_phrase} inline
+ error={this.state.exactPhraseError}
+ onChange={v => this.updateParam(s => s.text_search.exact_phrase = v)}
+ readonly={regexOptionsModified || (Array.isArray(options.text_search.terms) && options.text_search.terms.length > 0)}/>
+
+ <CheckField checked={options.text_search.case_sensitive} name="case_sensitive"
+ readonly={regexOptionsModified} small
+ onChange={(v) => this.updateParam(s => s.text_search.case_sensitive = v)}/>
+ </div>
+
+ <div className="separator">
+ <span>or</span>
+ </div>
+
+ <div className="regex-search">
+ <InputField name="pattern" value={options.regex_search.pattern} inline
+ error={this.state.patternError}
+ readonly={textOptionsModified || options.regex_search.not_pattern}
+ onChange={v => this.updateParam(s => s.regex_search.pattern = v)}/>
+ <span className="exclusive-separator">or</span>
+ <InputField name="not_pattern" value={options.regex_search.not_pattern} inline
+ error={this.state.notPatternError}
+ readonly={textOptionsModified || options.regex_search.pattern}
+ onChange={v => this.updateParam(s => s.regex_search.not_pattern = v)}/>
+
+ <div className="checkbox-line">
+ <CheckField checked={options.regex_search.case_insensitive} name="case_insensitive"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam(s => s.regex_search.case_insensitive = v)}/>
+ <CheckField checked={options.regex_search.multi_line} name="multi_line"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam(s => s.regex_search.multi_line = v)}/>
+ <CheckField checked={options.regex_search.ignore_whitespaces}
+ name="ignore_whitespaces"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam(s => s.regex_search.ignore_whitespaces = v)}/>
+ <CheckField checked={options.regex_search.dot_character} name="dot_character"
+ readonly={textOptionsModified} small
+ onChange={(v) => this.updateParam(s => s.regex_search.dot_character = v)}/>
+ </div>
+ </div>
+ </div>
+
+ <TextField value={curlCommand} rows={3} readonly small={true}/>
+ </div>
+
+ <div className="section-footer">
+ <ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>
+ <ButtonField variant="green" name="perform_search" bordered onClick={this.performSearch}/>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default SearchPane;
diff --git a/frontend/src/components/panels/SearchPane.scss b/frontend/src/components/panels/SearchPane.scss
new file mode 100644
index 0000000..15fc7da
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.scss
@@ -0,0 +1,51 @@
+.search-pane {
+ display: flex;
+ flex-direction: column;
+
+ .searches-list {
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .search-new {
+ .content-row {
+ display: flex;
+
+ .text-search,
+ .regex-search {
+ flex: 1;
+ }
+
+ .exclusive-separator {
+ font-size: 0.8em;
+ display: block;
+ text-align: center;
+ }
+
+ .separator {
+ font-size: 0.9em;
+ flex: 0;
+ margin: auto 10px;
+ }
+ }
+
+ .notes {
+ font-size: 0.8em;
+ }
+
+ .checkbox-line {
+ .check-field {
+ display: inline-block;
+ margin-top: 0;
+ margin-right: 10px;
+ }
+ }
+ }
+}