/* * 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 Table from "react-bootstrap/Table"; import backend from "../../backend"; import dispatcher from "../../dispatcher"; import {createCurlCommand, dateTimeToTime, durationBetween} from "../../utils"; import ButtonField from "../fields/ButtonField"; import CheckField from "../fields/CheckField"; import InputField from "../fields/InputField"; import TagField from "../fields/TagField"; import TextField from "../fields/TextField"; import LinkPopover from "../objects/LinkPopover"; import "./common.scss"; import "./SearchPane.scss"; 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", this.handleNotification); document.title = "caronte:~/searches$"; } componentWillUnmount() { dispatcher.unregister(this.handleNotification); } 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; this.setState({loading: true}); if (this.validateSearch(options)) { backend.post("/api/searches/perform", options).then((res) => { this.reset(); this.setState({searchStatusCode: res.status, loading: false}); this.loadSearches(); this.viewSearch(res.json.id); }).catch((res) => { this.setState({ searchStatusCode: res.status, searchResponse: JSON.stringify(res.json), loading: false }); }); } }; 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}); }; handleNotification = (payload) => { if (payload.event === "searches.new") { this.loadSearches(); } }; 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}
id pattern occurrences started_at duration actions
POST /api/searches/perform
NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex.
{ return {name: t}; })} name="terms" min={3} inline allowNew={true} readonly={regexOptionsModified || options["text_search"]["exact_phrase"]} onChange={(tags) => this.updateParam((s) => s["text_search"].terms = tags.map((t) => t.name))}/> { return {name: t}; })} name="excluded_terms" min={3} inline allowNew={true} readonly={regexOptionsModified || options["text_search"]["exact_phrase"]} onChange={(tags) => this.updateParam((s) => s["text_search"]["excluded_terms"] = tags.map((t) => t.name))}/> 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;