aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/panels/SearchPane.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/panels/SearchPane.jsx')
-rw-r--r--frontend/src/components/panels/SearchPane.jsx309
1 files changed, 309 insertions, 0 deletions
diff --git a/frontend/src/components/panels/SearchPane.jsx b/frontend/src/components/panels/SearchPane.jsx
new file mode 100644
index 0000000..6fe9dc7
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.jsx
@@ -0,0 +1,309 @@
+/*
+ * 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 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";
+
+import _ from '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) =>
+ <tr key={s.id} className="row-small row-clickable">
+ <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 || []).map((t) => {
+ 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))}/>
+ <TagField tags={(options["text_search"]["excluded_terms"] || []).map((t) => {
+ 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))}/>
+
+ <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 disabled={this.state.loading}
+ onClick={this.reset}/>
+ <ButtonField variant="green" name="perform_search" bordered
+ disabled={this.state.loading} onClick={this.performSearch}/>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default SearchPane;