From a44b70943ea4fce61261fc5fadf84a2a98fd2435 Mon Sep 17 00:00:00 2001
From: Emiliano Ciavatta
Date: Mon, 12 Oct 2020 20:08:45 +0200
Subject: Add search pane on frontend
---
frontend/src/components/panels/SearchPane.js | 293 +++++++++++++++++++++++++
frontend/src/components/panels/SearchPane.scss | 51 +++++
2 files changed, 344 insertions(+)
create mode 100644 frontend/src/components/panels/SearchPane.js
create mode 100644 frontend/src/components/panels/SearchPane.scss
(limited to 'frontend/src/components/panels')
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 .
+ */
+
+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 =>
+
+ {s.id.substring(0, 8)} |
+ {this.extractPattern(s["search_options"])} |
+ {s["affected_connections_count"]} |
+ {dateTimeToTime(s["started_at"])} |
+ {durationBetween(s["started_at"], s["finished_at"])} |
+ this.viewSearch(s.id)}/> |
+
+ );
+
+ 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 (
+
+
+
+ GET /api/searches
+ {this.state.searchesStatusCode &&
+ }
+
+
+
+
+
+
+
+ id |
+ pattern |
+ occurrences |
+ started_at |
+ duration |
+ actions |
+
+
+
+ {searches}
+
+
+
+
+
+
+
+
+ POST /api/searches/perform
+
+
+
+
+
+ NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex.
+
+
+
+
+ this.updateParam(s => s.text_search.terms = tags)}/>
+ this.updateParam(s => s.text_search.excluded_terms = tags)}/>
+
+ or
+
+ this.updateParam(s => s.text_search.exact_phrase = v)}
+ readonly={regexOptionsModified || (Array.isArray(options.text_search.terms) && options.text_search.terms.length > 0)}/>
+
+ this.updateParam(s => s.text_search.case_sensitive = v)}/>
+
+
+
+ or
+
+
+
+
this.updateParam(s => s.regex_search.pattern = v)}/>
+ or
+ this.updateParam(s => s.regex_search.not_pattern = v)}/>
+
+
+ this.updateParam(s => s.regex_search.case_insensitive = v)}/>
+ this.updateParam(s => s.regex_search.multi_line = v)}/>
+ this.updateParam(s => s.regex_search.ignore_whitespaces = v)}/>
+ this.updateParam(s => s.regex_search.dot_character = v)}/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
+
+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;
+ }
+ }
+ }
+}
--
cgit v1.2.3-70-g09d2