diff options
author | Emiliano Ciavatta | 2020-10-12 18:08:45 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-10-12 18:08:45 +0000 |
commit | a44b70943ea4fce61261fc5fadf84a2a98fd2435 (patch) | |
tree | b5425d07fa87e8aae027107a3e6008628167b9c9 /frontend | |
parent | 828aeef9a7333aaabeaf9324a85aac56348b3805 (diff) |
Add search pane on frontend
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/src/components/Header.js | 9 | ||||
-rw-r--r-- | frontend/src/components/fields/CheckField.js | 2 | ||||
-rw-r--r-- | frontend/src/components/fields/TagField.js | 78 | ||||
-rw-r--r-- | frontend/src/components/fields/TagField.scss | 120 | ||||
-rw-r--r-- | frontend/src/components/filters/ExitSearchFilter.js | 52 | ||||
-rw-r--r-- | frontend/src/components/filters/FiltersDispatcher.js | 57 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.js | 2 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.scss | 193 | ||||
-rw-r--r-- | frontend/src/components/pages/MainPage.js | 5 | ||||
-rw-r--r-- | frontend/src/components/panels/SearchPane.js | 293 | ||||
-rw-r--r-- | frontend/src/components/panels/SearchPane.scss | 51 |
11 files changed, 763 insertions, 99 deletions
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; + } + } + } +} |