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 && + } +
+ +
+
+ + + + + + + + + + + + + {searches} + +
idpatternoccurrencesstarted_atdurationactions
+
+
+
+ +
+
+ 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