diff options
Diffstat (limited to 'frontend')
23 files changed, 536 insertions, 301 deletions
diff --git a/frontend/package.json b/frontend/package.json index 3629e70..b13a7ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "bs-custom-file-input": "^1.3.4", "classnames": "^2.2.6", "eslint-config-react-app": "^5.2.1", + "lodash": "^4.17.20", "node-sass": "^4.14.0", "react": "^16.13.1", "react-bootstrap": "^1.0.1", diff --git a/frontend/src/backend.js b/frontend/src/backend.js index a02f7a8..2fbe920 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -1,5 +1,5 @@ -async function json(method, url, data, headers, returnJson) { +async function json(method, url, data, headers) { const options = { method: method, mode: "cors", @@ -14,18 +14,17 @@ async function json(method, url, data, headers, returnJson) { if (data != null) { options.body = JSON.stringify(data); } - const result = await fetch(url, options); - if (returnJson) { - if (result.status >= 200 && result.status < 300) { - return result.json(); - } else { - return Promise.reject({ - response: result, - json: await result.json() - }); - } - } else { + const response = await fetch(url, options); + const result = { + statusCode: response.status, + status: `${response.status} ${response.statusText}`, + json: await response.json() + }; + + if (response.status >= 200 && response.status < 300) { return result; + } else { + return Promise.reject(result); } } @@ -56,16 +55,16 @@ const backend = { return json("DELETE", url, data, headers); }, getJson: (url = "", headers = null) => { - return json("GET", url, null, headers, true); + return json("GET", url, null, headers); }, postJson: (url = "", data = null, headers = null) => { - return json("POST", url, data, headers, true); + return json("POST", url, data, headers); }, putJson: (url = "", data = null, headers = null) => { - return json("PUT", url, data, headers, true); + return json("PUT", url, data, headers); }, deleteJson: (url = "", data = null, headers = null) => { - return json("DELETE", url, data, headers, true); + return json("DELETE", url, data, headers); }, postFile: (url = "", data = null, headers = null) => { return file(url, data, headers); diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 318965c..a9d34d3 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import './ConnectionContent.scss'; -import {Dropdown, Row} from 'react-bootstrap'; +import {Row} from 'react-bootstrap'; import MessageAction from "./MessageAction"; import backend from "../backend"; import ButtonField from "./fields/ButtonField"; @@ -42,7 +42,7 @@ class ConnectionContent extends Component { // TODO: limit workaround. backend.getJson(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => { this.setState({ - connectionContent: res, + connectionContent: res.json, loading: false }); }); diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js index f2f02fd..cc32b0f 100644 --- a/frontend/src/components/fields/ButtonField.js +++ b/frontend/src/components/fields/ButtonField.js @@ -6,10 +6,6 @@ const classNames = require('classnames'); class ButtonField extends Component { - constructor(props) { - super(props); - } - render() { const handler = () => { if (typeof this.props.onClick === "function") { diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss index aabe80f..cfd20ff 100644 --- a/frontend/src/components/fields/ButtonField.scss +++ b/frontend/src/components/fields/ButtonField.scss @@ -11,7 +11,7 @@ font-size: 0.8em; button { - padding: 4px 12px; + padding: 3px 12px; } } diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js index 6cf967a..b790891 100644 --- a/frontend/src/components/fields/InputField.js +++ b/frontend/src/components/fields/InputField.js @@ -20,16 +20,17 @@ class InputField extends Component { const inline = this.props.inline || false; const name = this.props.name || null; const value = this.props.value || ""; + const defaultValue = this.props.defaultValue || ""; const type = this.props.type || "text"; const error = this.props.error || null; - const defaultValue = this.props.defaultValue || null; + const handler = (e) => { - if (this.props.onChange) { + if (typeof this.props.onChange === "function") { if (type === "file") { let file = e.target.files[0]; this.props.onChange(file); } else if (e == null) { - this.props.onChange(""); + this.props.onChange(defaultValue); } else { this.props.onChange(e.target.value); } @@ -37,7 +38,7 @@ class InputField extends Component { }; let inputProps = {}; if (type !== "file") { - inputProps["value"] = value || this.props.initialValue; + inputProps["value"] = value || defaultValue; } return ( @@ -52,8 +53,8 @@ class InputField extends Component { <div className="field-input"> <div className="field-value"> { type === "file" && <label for={this.id} className={"file-label"}> - {value.name || defaultValue}</label> } - <input type={type} placeholder={defaultValue} id={this.id} + {value.name || this.props.placeholder}</label> } + <input type={type} placeholder={this.props.placeholder} id={this.id} aria-describedby={this.id} onChange={handler} {...inputProps} /> </div> { type !== "file" && value !== "" && diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js index 8823c42..ed81ed7 100644 --- a/frontend/src/components/fields/extensions/NumericField.js +++ b/frontend/src/components/fields/extensions/NumericField.js @@ -11,25 +11,31 @@ class NumericField extends Component { }; } - render() { - const handler = (value) => { - value = value.replace(/[^\d]/gi, ''); - let intValue = 0; - if (value !== "") { - intValue = parseInt(value); - } - const valid = - (!this.props.validate || (typeof this.props.validate === "function" && this.props.validate(intValue))) && - (!this.props.min || (typeof this.props.min === "number" && intValue >= this.props.min)) && - (!this.props.max || (typeof this.props.max === "number" && intValue <= this.props.max)); - this.setState({invalid: !valid}); - if (this.props.onChange) { - this.props.onChange(intValue); - } - }; + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.value !== this.props.value) { + this.onChange(this.props.value); + } + } + onChange = (value) => { + value = value.toString().replace(/[^\d]/gi, ''); + let intValue = 0; + if (value !== "") { + intValue = parseInt(value); + } + const valid = + (!this.props.validate || (typeof this.props.validate === "function" && this.props.validate(intValue))) && + (!this.props.min || (typeof this.props.min === "number" && intValue >= this.props.min)) && + (!this.props.max || (typeof this.props.max === "number" && intValue <= this.props.max)); + this.setState({invalid: !valid}); + if (typeof this.props.onChange === "function") { + this.props.onChange(intValue); + } + }; + + render() { return ( - <InputField {...this.props} onChange={handler} initialValue={this.props.initialValue || 0} + <InputField {...this.props} onChange={this.onChange} defaultValue={this.props.defaultValue || "0"} invalid={this.state.invalid} /> ); } diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index 0741bea..f4d0b1c 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -25,7 +25,7 @@ class RulesConnectionsFilter extends Component { let activeRules = params.getAll("matched_rules") || []; backend.getJson("/api/rules").then(res => { - let rules = res.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []); + let rules = res.json.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []); activeRules = rules.filter(rule => activeRules.some(id => rule.id === id)); this.setState({rules, activeRules, mounted: true}); }); diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index a304198..f463593 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -115,7 +115,7 @@ class StringConnectionsFilter extends Component { return ( <div className="filter" style={{"width": `${this.props.width}px`}}> <InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName} - defaultValue={this.props.defaultFilterValue} onChange={this.filterChanged} + placeholder={this.props.defaultFilterValue} onChange={this.filterChanged} value={this.state.fieldValue} inline={true} small={true} /> {redirect} </div> diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js new file mode 100644 index 0000000..58b2f6a --- /dev/null +++ b/frontend/src/components/objects/LinkPopover.js @@ -0,0 +1,33 @@ +import React, {Component} from 'react'; +import {randomClassName} from "../../utils"; +import {OverlayTrigger, Popover} from "react-bootstrap"; +import './LinkPopover.scss'; + +class LinkPopover extends Component { + + constructor(props) { + super(props); + + this.id = `link-overlay-${randomClassName()}`; + } + + render() { + const popover = ( + <Popover id={this.id}> + {this.props.title && <Popover.Title as="h3">{this.props.title}</Popover.Title>} + <Popover.Content> + {this.props.content} + </Popover.Content> + </Popover> + ); + + return (this.props.content ? + <OverlayTrigger trigger="hover" placement={this.props.placement || "top"} overlay={popover}> + <span className="link-popover">{this.props.text}</span> + </OverlayTrigger> : + <span className="link-popover-empty">{this.props.text}</span> + ); + } +} + +export default LinkPopover; diff --git a/frontend/src/components/objects/LinkPopover.scss b/frontend/src/components/objects/LinkPopover.scss new file mode 100644 index 0000000..d5f4879 --- /dev/null +++ b/frontend/src/components/objects/LinkPopover.scss @@ -0,0 +1,7 @@ +@import '../../colors.scss'; + +.link-popover { + text-decoration: underline; + font-weight: 500; + cursor: pointer; +}
\ No newline at end of file diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js index a491dff..cfba037 100644 --- a/frontend/src/components/panels/PcapPane.js +++ b/frontend/src/components/panels/PcapPane.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; -import './PcapPane.scss'; +// import './PcapPane.scss'; +import './common.scss'; import Table from "react-bootstrap/Table"; import backend from "../../backend"; import {createCurlCommand, formatSize, timestampToTime2} from "../../utils"; @@ -37,7 +38,7 @@ class PcapPane extends Component { } loadSessions = () => { - backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res})); + backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res.json})); }; handleUploadFileChange = (file) => { @@ -104,7 +105,7 @@ class PcapPane extends Component { <td>{s["processed_packets"]}</td> <td>{s["invalid_packets"]}</td> <td>undefined</td> - <td className="table-cell-action"><a target="_blank" + <td className="table-cell-action"><a target="_blank" rel="noopener noreferrer" href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a> </td> </tr> diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss index ce28227..3c4d10b 100644 --- a/frontend/src/components/panels/PcapPane.scss +++ b/frontend/src/components/panels/PcapPane.scss @@ -1,51 +1,10 @@ @import '../../colors.scss'; .pane-container { - background-color: $color-primary-3; - padding: 10px 10px 0; - height: 100%; - .section-header { - background-color: $color-primary-2; - padding: 5px 10px; - height: 31px; - - font-weight: 500; - font-size: 14px; - - .api-response { - float: right; - } - } - - .section-table { - margin-top: 10px; - - .table-row { - background-color: $color-primary-0; - border-top: 3px solid $color-primary-3; - border-bottom: 3px solid $color-primary-3; - } - - .table-cell-action { - font-size: 13px; - font-weight: 600; - } - } - - .section-content { - background-color: $color-primary-0; - padding: 10px; - } - - th { - background-color: $color-primary-2; - border-top: 3px solid $color-primary-3; - border-bottom: 3px solid $color-primary-3; - font-size: 13.5px; - position: sticky; - top: 10px; - padding: 5px; + .table-cell-action { + font-size: 13px; + font-weight: 600; } .upload-actions { diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js index 2e91d91..01e37fa 100644 --- a/frontend/src/components/panels/RulePane.js +++ b/frontend/src/components/panels/RulePane.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import './common.scss'; import './RulePane.scss'; import Table from "react-bootstrap/Table"; import {Col, Container, Row} from "react-bootstrap"; @@ -10,6 +11,11 @@ import NumericField from "../fields/extensions/NumericField"; import ColorField from "../fields/extensions/ColorField"; import ChoiceField from "../fields/ChoiceField"; import ButtonField from "../fields/ButtonField"; +import validation from "../../validation"; +import LinkPopover from "../objects/LinkPopover"; + +const classNames = require('classnames'); +const _ = require('lodash'); class RulePane extends Component { @@ -18,103 +24,352 @@ class RulePane extends Component { this.state = { rules: [], + newRule: this.emptyRule, + newPattern: this.emptyPattern + }; + + this.directions = { + 0: "both", + 1: "c->s", + 2: "s->c" }; } componentDidMount() { + this.reset(); this.loadRules(); } + emptyRule = { + "name": "", + "color": "", + "notes": "", + "enabled": true, + "patterns": [], + "filter": { + "service_port": 0, + "client_address": "", + "client_port": 0, + "min_duration": 0, + "max_duration": 0, + "min_bytes": 0, + "max_bytes": 0 + }, + "version": 0 + }; + + emptyPattern = { + "regex": "", + "flags": { + "caseless": false, + "dot_all": false, + "multi_line": false, + "utf_8_mode": false, + "unicode_property": false + }, + "min_occurrences": 0, + "max_occurrences": 0, + "direction": 0 + }; + loadRules = () => { - backend.getJson("/api/rules").then(res => this.setState({rules: res})); + backend.getJson("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status})) + .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)})); + }; + + addRule = () => { + if (this.validateRule(this.state.newRule)) { + backend.postJson("/api/rules", this.state.newRule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + updateRule = () => { + const rule = this.state.selectedRule; + if (this.validateRule(rule)) { + backend.putJson(`/api/rules/${rule.id}`, rule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateRule = (rule) => { + let valid = true; + if (rule.name.length < 3) { + this.setState({ruleNameError: "name.length < 3"}); + valid = false; + } + if (!validation.isValidColor(rule.color)) { + this.setState({ruleColorError: "color is not hexcolor"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.service_port)) { + this.setState({ruleServicePortError: "service_port > 65565"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.client_port)) { + this.setState({ruleClientPortError: "client_port > 65565"}); + valid = false; + } + if (!validation.isValidAddress(rule.filter.client_address)) { + this.setState({ruleClientAddressError: "client_address is not ip_address"}); + valid = false; + } + if (rule.filter.min_duration > rule.filter.max_duration) { + this.setState({ruleDurationError: "min_duration > max_dur."}); + valid = false; + } + if (rule.filter.min_bytes > rule.filter.max_bytes) { + this.setState({ruleBytesError: "min_bytes > max_bytes"}); + valid = false; + } + if (rule.patterns.length < 1) { + this.setState({rulePatternsError: "patterns.length < 1"}); + valid = false; + } + + return valid; }; + reset = () => { + const newRule = _.cloneDeep(this.emptyRule); + const newPattern = _.cloneDeep(this.emptyPattern); + this.setState({ + selectedRule: null, + newRule: newRule, + selectedPattern: null, + newPattern: newPattern, + patternRegexFocused: false, + patternOccurrencesFocused: false, + ruleNameError: null, + ruleColorError: null, + ruleServicePortError: null, + ruleClientPortError: null, + ruleClientAddressError: null, + ruleDurationError: null, + ruleBytesError: null, + rulePatternsError: null, + ruleStatusCode: null, + rulesStatusCode: null, + ruleResponse: null, + rulesResponse: null + }); + }; + + updateParam = (callback) => { + const updatedRule = this.currentRule(); + callback(updatedRule); + this.setState({newRule: updatedRule}); + }; + + currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule; + + addPattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + const newPattern = _.cloneDeep(this.emptyPattern); + this.currentRule().patterns.push(pattern); + this.setState({ + newPattern: newPattern + }); + }; + + editPattern = (pattern) => { + this.setState({ + selectedPattern: pattern + }); + }; + + updatePattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + this.setState({ + selectedPattern: null + }); + }; + + validatePattern = (pattern) => { + let valid = true; + if (pattern.regex === "") { + valid = false; + this.setState({patternRegexFocused: true}); + } + if (pattern.min_occurrences > pattern.max_occurrences) { + valid = false; + this.setState({patternOccurrencesFocused: true}); + } + return valid; + }; render() { + const isUpdate = this.state.selectedRule != null; + const rule = this.currentRule(); + const pattern = this.state.selectedPattern || this.state.newPattern; + let rules = this.state.rules.map(r => - <tr className="table-row"> + <tr onClick={() => { + this.reset(); + this.setState({selectedRule: _.cloneDeep(r)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}> <td>{r["id"].substring(0, 8)}</td> <td>{r["name"]}</td> + <td><ButtonField name={r["color"]} color={r["color"]} small /></td> <td>{r["notes"]}</td> - {/*<td>{((new Date(s["completed_at"]) - new Date(s["started_at"])) / 1000).toFixed(3)}s</td>*/} - {/*<td>{formatSize(s["size"])}</td>*/} - {/*<td>{s["processed_packets"]}</td>*/} - {/*<td>{s["invalid_packets"]}</td>*/} - {/*<td>undefined</td>*/} - {/*<td className="table-cell-action"><a target="_blank"*/} - {/* href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>*/} - {/*</td>*/} + </tr> + ); + + let patterns = (this.state.selectedPattern == null && !isUpdate ? + rule.patterns.concat(this.state.newPattern) : + rule.patterns + ).map(p => p === pattern ? + <tr> + <td style={{"width": "500px"}}> + <InputField small active={this.state.patternRegexFocused} value={pattern.regex} + onChange={(v) => { + this.updateParam(() => pattern.regex = v); + this.setState({patternRegexFocused: pattern.regex === ""}); + }} /> + </td> + <td><CheckField small checked={pattern.flags.caseless} + onChange={(v) => this.updateParam(() => pattern.flags.caseless = v)}/></td> + <td><CheckField small checked={pattern.flags.dot_all} + onChange={(v) => this.updateParam(() => pattern.flags.dot_all = v)}/></td> + <td><CheckField small checked={pattern.flags.multi_line} + onChange={(v) => this.updateParam(() => pattern.flags.multi_line = v)}/></td> + <td><CheckField small checked={pattern.flags.utf_8_mode} + onChange={(v) => this.updateParam(() => pattern.flags.utf_8_mode = v)}/></td> + <td><CheckField small checked={pattern.flags.unicode_property} + onChange={(v) => this.updateParam(() => pattern.flags.unicode_property = v)}/></td> + <td style={{"width": "70px"}}> + <NumericField small value={pattern.min_occurrences} + active={this.state.patternOccurrencesFocused} + onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)} /> + </td> + <td style={{"width": "70px"}}> + <NumericField small value={pattern.max_occurrences} + active={this.state.patternOccurrencesFocused} + onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)} /> + </td> + <td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} + value={this.directions[pattern.direction]} + onChange={(v) => this.updateParam(() => pattern.direction = v)} /></td> + <td>{this.state.selectedPattern == null ? + <ButtonField variant="green" small name="add" inline rounded onClick={() => this.addPattern(p)}/> : + <ButtonField variant="green" small name="save" inline rounded onClick={() => this.updatePattern(p)}/>} + </td> + </tr> + : + <tr className="row-small"> + <td>{p.regex}</td> + <td>{p.flags.caseless ? "yes": "no"}</td> + <td>{p.flags.dot_all ? "yes": "no"}</td> + <td>{p.flags.multi_line ? "yes": "no"}</td> + <td>{p.flags.utf_8_mode ? "yes": "no"}</td> + <td>{p.flags.unicode_property ? "yes": "no"}</td> + <td>{p.min_occurrences}</td> + <td>{p.max_occurrences}</td> + <td>{this.directions[p.direction]}</td> + {!isUpdate && <td><ButtonField variant="blue" small rounded name="edit" + onClick={() => this.editPattern(p) }/></td>} </tr> ); return ( <div className="pane-container rule-pane"> - <div className="pane-section"> + <div className="pane-section rules-list"> <div className="section-header"> <span className="api-request">GET /api/rules</span> - <span className="api-response">200 OK</span> + {this.state.rulesStatusCode && + <span className="api-response"><LinkPopover text={this.state.rulesStatusCode} + content={this.state.rulesResponse} + placement="left" /></span>} </div> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>id</th> - <th>name</th> - <th>notes</th> - </tr> - </thead> - <tbody> - {rules} - </tbody> - </Table> + <div className="section-content"> + <div className="section-table"> + <Table borderless size="sm"> + <thead> + <tr> + <th>id</th> + <th>name</th> + <th>color</th> + <th>notes</th> + </tr> + </thead> + <tbody> + {rules} + </tbody> + </Table> + </div> </div> </div> - <div className="pane-section"> + <div className="pane-section rule-edit"> <div className="section-header"> - <span className="api-request">POST /api/rules</span> - <span className="api-response"></span> + <span className="api-request"> + {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} + </span> + <span className="api-response"><LinkPopover text={this.state.ruleStatusCode} + content={this.state.ruleResponse} + placement="left" /></span> </div> <div className="section-content"> <Container className="p-0"> <Row> <Col> - <InputField name="name" inline /> - <ColorField value={this.state.test1} onChange={(e) => this.setState({test1: e})} inline /> - <TextField name="notes" rows={2} /> + <InputField name="name" inline value={rule.name} + onChange={(v) => this.updateParam((r) => r.name = v)} + error={this.state.ruleNameError} /> + <ColorField inline value={rule.color} error={this.state.ruleColorError} + onChange={(v) => this.updateParam((r) => r.color = v)} /> + <TextField name="notes" rows={2} value={rule.notes} + onChange={(v) => this.updateParam((r) => r.notes = v)} /> </Col> - <Col> - <div >filters:</div> - <NumericField name="service_port" inline value={this.state.test} onChange={(e) => this.setState({test: e})} validate={(e) => e%2 === 0} /> - - <NumericField name="client_port" inline /> - <InputField name="client_address" /> + <Col style={{"paddingTop": "6px"}}> + <span>filters:</span> + <NumericField name="service_port" inline value={rule.filter.service_port} + onChange={(v) => this.updateParam((r) => r.filter.service_port = v)} + min={0} max={65565} error={this.state.ruleServicePortError} /> + <NumericField name="client_port" inline value={rule.filter.client_port} + onChange={(v) => this.updateParam((r) => r.filter.client_port = v)} + min={0} max={65565} error={this.state.ruleClientPortError} /> + <InputField name="client_address" value={rule.filter.client_address} + error={this.state.ruleClientAddressError} + onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} /> </Col> - <Col> - <NumericField name="min_duration" inline /> - <NumericField name="max_duration" inline /> - <NumericField name="min_bytes" inline /> - <NumericField name="max_bytes" inline /> - + <Col style={{"paddingTop": "11px"}}> + <NumericField name="min_duration" inline value={rule.filter.min_duration} + error={this.state.ruleDurationError} + onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} /> + <NumericField name="max_duration" inline value={rule.filter.max_duration} + error={this.state.ruleDurationError} + onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} /> + <NumericField name="min_bytes" inline value={rule.filter.min_bytes} + error={this.state.ruleBytesError} + onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} /> + <NumericField name="max_bytes" inline value={rule.filter.max_bytes} + error={this.state.ruleBytesError} + onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} /> </Col> </Row> </Container> - <div className="post-rules-actions"> - <label>options:</label> - <div className="rules-options"> - <CheckField name={"enabled"} /> - </div> - - <ButtonField variant="blue" name="clear" bordered /> - <ButtonField variant="green" name="add_rule" bordered /> - </div> - - patterns: <div className="section-table"> <Table borderless size="sm"> <thead> @@ -128,29 +383,24 @@ class RulePane extends Component { <th>min</th> <th>max</th> <th>direction</th> - <th>actions</th> + {!isUpdate && <th>actions</th> } </tr> </thead> <tbody> - <tr> - <td style={{"width": "500px"}}><InputField small /></td> - <td><CheckField small /></td> - <td><CheckField small /></td> - <td><CheckField small /></td> - <td><CheckField small /></td> - <td><CheckField small /></td> - <td style={{"width": "70px"}}><NumericField small /></td> - <td style={{"width": "70px"}}><NumericField small /></td> - <td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} value="both" /></td> - <td><ButtonField variant="green" small name="add" inline rounded /></td> - </tr> + {patterns} </tbody> </Table> + {this.state.rulePatternsError != null && + <span className="table-error">error: {this.state.rulePatternsError}</span>} </div> </div> - </div> - + <div className="section-footer"> + {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>} + <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_rule" : "add_rule"} + bordered onClick={isUpdate ? this.updateRule : this.addRule} /> + </div> + </div> </div> ); } diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss index b030c6a..d45c366 100644 --- a/frontend/src/components/panels/RulePane.scss +++ b/frontend/src/components/panels/RulePane.scss @@ -1,16 +1,32 @@ .rule-pane { - .post-rules-actions { - display: flex; + display: flex; + flex-direction: column; - .rules-options { - flex: 1; + .rules-list { + flex: 2 1; + overflow: hidden; + + .section-content { + height: 100%; } - button { - margin-left: 10px; + .section-table { + height: calc(100% - 30px); } } + .rule-edit { + flex: 3 0; + display: flex; + flex-direction: column; -}
\ No newline at end of file + .section-content { + flex: 1; + } + + .section-table { + max-height: 150px; + } + } +} diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss new file mode 100644 index 0000000..cab0de1 --- /dev/null +++ b/frontend/src/components/panels/common.scss @@ -0,0 +1,85 @@ +@import '../../colors.scss'; + +.pane-container { + background-color: $color-primary-3; + padding: 10px 10px 0; + height: 100%; + + .pane-section { + margin-bottom: 10px; + background-color: $color-primary-0; + + .section-header { + background-color: $color-primary-2; + padding: 5px 10px; + font-weight: 500; + font-size: 0.9em; + display: flex; + + .api-request { + flex: 1; + } + } + + .section-content { + padding: 10px; + } + + .section-table { + position: relative; + overflow-y: scroll; + + .table-error { + font-size: 0.8em; + color: $color-secondary-0; + margin-left: 10px; + } + } + + table { + margin-bottom: 0; + + tbody tr { + background-color: $color-primary-3; + border-top: 3px solid $color-primary-0; + border-bottom: 3px solid $color-primary-0; + } + + th { + background-color: $color-primary-2; + font-size: 0.8em; + position: sticky; + top: 0; + padding: 2px 5px; + border: none; + } + + .row-small { + font-size: 0.9em; + } + + .row-clickable { + cursor: pointer; + + &:hover { + background-color: $color-primary-0; + } + } + + .row-selected { + background-color: $color-primary-0; + } + } + } + + .section-footer { + display: flex; + padding: 10px; + background-color: $color-primary-0; + justify-content: flex-end; + + .button-field { + margin-left: 10px; + } + } +} diff --git a/frontend/src/validation.js b/frontend/src/validation.js new file mode 100644 index 0000000..eac8774 --- /dev/null +++ b/frontend/src/validation.js @@ -0,0 +1,8 @@ + +const validation = { + isValidColor: (color) => true, // TODO + isValidPort: (port, required) => parseInt(port) > (required ? 0 : -1) && parseInt(port) <= 65565, + isValidAddress: (address) => true // TODO +}; + +export default validation; diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index ccfdb3a..8bd5e46 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -5,7 +5,6 @@ import Footer from "./Footer"; import {BrowserRouter as Router} from "react-router-dom"; import Services from "./Services"; import Filters from "./Filters"; -import Rules from "./Rules"; import Config from "./Config"; class App extends Component { @@ -15,7 +14,6 @@ class App extends Component { this.state = { servicesWindowOpen: false, filterWindowOpen: false, - rulesWindowOpen: false, configWindowOpen: false, configDone: false }; @@ -40,9 +38,6 @@ class App extends Component { if (this.state.filterWindowOpen) { modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>; } - if (this.state.rulesWindowOpen) { - modal = <Rules onHide={() => this.setState({rulesWindowOpen: false})}/>; - } if (this.state.configWindowOpen) { modal = <Config onHide={() => this.setState({configWindowOpen: false})} onDone={() => this.setState({configDone: true})}/>; @@ -53,7 +48,6 @@ class App extends Component { <Router> <Header onOpenServices={() => this.setState({servicesWindowOpen: true})} onOpenFilters={() => this.setState({filterWindowOpen: true})} - onOpenRules={() => this.setState({rulesWindowOpen: true})} onOpenConfig={() => this.setState({configWindowOpen: true})} onOpenUpload={() => this.setState({uploadWindowOpen: true})} onConfigDone={this.state.configDone} diff --git a/frontend/src/views/Config.js b/frontend/src/views/Config.js index a770378..b11f827 100644 --- a/frontend/src/views/Config.js +++ b/frontend/src/views/Config.js @@ -68,8 +68,6 @@ class Config extends Component { }) }; - let msg = ""; - fetch('/setup', requestOptions) .then(response => { if (response.status === 202 ){ diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index f3fec64..64068c5 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -75,7 +75,7 @@ class Connections extends Component { } this.setState({loading: true, prevParams: params}); - let res = await backend.getJson(`${url}?${urlParams}`); + let res = (await backend.getJson(`${url}?${urlParams}`)).json; let connections = this.state.connections; let firstConnection = this.state.firstConnection; @@ -115,7 +115,7 @@ class Connections extends Component { let flagRule = this.state.flagRule; let rules = this.state.rules; if (flagRule === null) { - rules = await backend.getJson("/api/rules"); + rules = (await backend.getJson("/api/rules")).json; flagRule = rules.filter(rule => { return rule.name === "flag"; })[0]; diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index 0b82011..06cb20e 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -1,7 +1,6 @@ import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; -import {Button} from "react-bootstrap"; import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; import {Link} from "react-router-dom"; import ButtonField from "../components/fields/ButtonField"; diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js index 9d3f7b7..6f4e3cd 100644 --- a/frontend/src/views/MainPane.js +++ b/frontend/src/views/MainPane.js @@ -22,7 +22,7 @@ class MainPane extends Component { if (match != null) { this.setState({loading: true}); backend.getJson(`/api/connections/${match[1]}`) - .then(connection => this.setState({selectedConnection: connection, loading: false})) + .then(res => this.setState({selectedConnection: res.json, loading: false})) .catch(error => console.log(error)); } } diff --git a/frontend/src/views/Rules.js b/frontend/src/views/Rules.js deleted file mode 100644 index bbc3bb6..0000000 --- a/frontend/src/views/Rules.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, {Component} from 'react'; -import './Services.scss'; -import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap"; -import backend from "../backend"; - -class Rules extends Component { - - constructor(props) { - super(props); - - this.state = { - rules: [] - }; - } - - componentDidMount() { - this.loadRules(); - } - - loadRules() { - backend.get("/api/rules").then(res => this.setState({rules: res.data})); - } - - render() { - let rulesRows = this.state.rules.map(rule => - <tr key={rule.id}> - <td><Button variant="btn-edit" size="sm" - style={{"backgroundColor": rule.color}}>edit</Button></td> - <td>{rule.name}</td> - </tr> - ); - - - return ( - <Modal - {...this.props} - show="true" - size="lg" - aria-labelledby="rules-dialog" - centered - > - <Modal.Header> - <Modal.Title id="rules-dialog"> - ~/rules - </Modal.Title> - </Modal.Header> - <Modal.Body> - <Container> - <Row> - <Col md={7}> - <Table borderless size="sm" className="rules-list"> - <thead> - <tr> - <th><Button size="sm" >new</Button></th> - <th>name</th> - </tr> - </thead> - <tbody> - {rulesRows} - - </tbody> - </Table> - </Col> - <Col md={5}> - <Form> - <Form.Group controlId="servicePort"> - <Form.Label>port:</Form.Label> - <Form.Control type="text" /> - </Form.Group> - - <Form.Group controlId="serviceName"> - <Form.Label>name:</Form.Label> - <Form.Control type="text" /> - </Form.Group> - - <Form.Group controlId="serviceColor"> - <Form.Label>color:</Form.Label> - <ButtonGroup aria-label="Basic example"> - - </ButtonGroup> - <ButtonGroup aria-label="Basic example"> - - </ButtonGroup> - </Form.Group> - - <Form.Group controlId="serviceNotes"> - <Form.Label>notes:</Form.Label> - <Form.Control as="textarea" rows={3} /> - </Form.Group> - </Form> - - - </Col> - - </Row> - - <Row> - <Col md={12}> - <InputGroup> - <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} - /> - </InputGroup> - - </Col> - </Row> - - - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="red" onClick={this.props.onHide}>close</Button> - </Modal.Footer> - </Modal> - ); - } -} - -export default Rules; |