aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-09-27 15:48:38 +0000
committerEmiliano Ciavatta2020-09-27 15:48:38 +0000
commit1412a34f64e234dbc7d4e6815b841699f4dd104a (patch)
tree3575bfa3d8e4d926066d3a84991809ca14083d97 /frontend
parent44af615b32faf53c04cd38cb63782cf1b1332c94 (diff)
Add other custom fields
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/backend.js27
-rw-r--r--frontend/src/components/ConnectionContent.js28
-rw-r--r--frontend/src/components/MessageAction.js13
-rw-r--r--frontend/src/components/fields/ButtonField.js44
-rw-r--r--frontend/src/components/fields/ButtonField.scss119
-rw-r--r--frontend/src/components/fields/CheckField.js5
-rw-r--r--frontend/src/components/fields/ChoiceField.js59
-rw-r--r--frontend/src/components/fields/ChoiceField.scss69
-rw-r--r--frontend/src/components/fields/InputField.js7
-rw-r--r--frontend/src/components/fields/InputField.scss28
-rw-r--r--frontend/src/components/fields/TextField.js5
-rw-r--r--frontend/src/components/fields/TextField.scss25
-rw-r--r--frontend/src/components/fields/common.scss54
-rw-r--r--frontend/src/components/fields/extensions/ColorField.js63
-rw-r--r--frontend/src/components/fields/extensions/ColorField.scss39
-rw-r--r--frontend/src/components/fields/extensions/NumericField.js39
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js2
-rw-r--r--frontend/src/components/panels/PcapPane.js110
-rw-r--r--frontend/src/components/panels/RulePane.js166
-rw-r--r--frontend/src/components/panels/RulePane.scss16
-rw-r--r--frontend/src/index.scss116
-rw-r--r--frontend/src/views/Connections.js24
-rw-r--r--frontend/src/views/Header.js4
-rw-r--r--frontend/src/views/MainPane.js27
24 files changed, 841 insertions, 248 deletions
diff --git a/frontend/src/backend.js b/frontend/src/backend.js
index 5eb0e40..a02f7a8 100644
--- a/frontend/src/backend.js
+++ b/frontend/src/backend.js
@@ -1,5 +1,5 @@
-async function json(method, url, data, headers) {
+async function json(method, url, data, headers, returnJson) {
const options = {
method: method,
mode: "cors",
@@ -15,7 +15,18 @@ async function json(method, url, data, headers) {
options.body = JSON.stringify(data);
}
const result = await fetch(url, options);
- return result.json();
+ if (returnJson) {
+ if (result.status >= 200 && result.status < 300) {
+ return result.json();
+ } else {
+ return Promise.reject({
+ response: result,
+ json: await result.json()
+ });
+ }
+ } else {
+ return result;
+ }
}
async function file(url, data, headers) {
@@ -44,6 +55,18 @@ const backend = {
delete: (url = "", data = null, headers = null) => {
return json("DELETE", url, data, headers);
},
+ getJson: (url = "", headers = null) => {
+ return json("GET", url, null, headers, true);
+ },
+ postJson: (url = "", data = null, headers = null) => {
+ return json("POST", url, data, headers, true);
+ },
+ putJson: (url = "", data = null, headers = null) => {
+ return json("PUT", url, data, headers, true);
+ },
+ deleteJson: (url = "", data = null, headers = null) => {
+ return json("DELETE", url, data, headers, true);
+ },
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 0c00e8e..0069424 100644
--- a/frontend/src/components/ConnectionContent.js
+++ b/frontend/src/components/ConnectionContent.js
@@ -22,20 +22,30 @@ class ConnectionContent extends Component {
this.setFormat = this.setFormat.bind(this);
}
+ componentDidMount() {
+ if (this.props.connection != null) {
+ this.loadStream();
+ }
+ }
+
componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.props.connection !== null && (
+ if (this.props.connection != null && (
this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
- this.setState({loading: true});
- // TODO: limit workaround.
- backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
- this.setState({
- connectionContent: res,
- loading: false
- });
- });
+ this.loadStream();
}
}
+ loadStream = () => {
+ this.setState({loading: true});
+ // TODO: limit workaround.
+ backend.getJson(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
+ this.setState({
+ connectionContent: res,
+ loading: false
+ });
+ });
+ };
+
setFormat(format) {
if (this.validFormats.includes(format)) {
this.setState({format: format});
diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js
index 2c85d84..2c6ebbc 100644
--- a/frontend/src/components/MessageAction.js
+++ b/frontend/src/components/MessageAction.js
@@ -1,6 +1,8 @@
import React, {Component} from 'react';
import './MessageAction.scss';
-import {Button, FormControl, InputGroup, Modal} from "react-bootstrap";
+import {Modal} from "react-bootstrap";
+import TextField from "./fields/TextField";
+import ButtonField from "./fields/ButtonField";
class MessageAction extends Component {
@@ -35,14 +37,11 @@ class MessageAction extends Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <InputGroup>
- <FormControl as="textarea" className="message-action-value" readOnly={true}
- style={{"height": "300px"}} value={this.props.actionValue} ref={this.actionValue} />
- </InputGroup>
+ <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} />
</Modal.Body>
<Modal.Footer className="dialog-footer">
- <Button variant="green" onClick={this.copyActionValue}>{this.state.copyButtonText}</Button>
- <Button variant="red" onClick={this.props.onHide}>close</Button>
+ <ButtonField variant="green" onClick={this.copyActionValue} name={this.state.copyButtonText} />
+ <ButtonField variant="red" onClick={this.props.onHide} name="close" />
</Modal.Footer>
</Modal>
);
diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js
new file mode 100644
index 0000000..b32aee8
--- /dev/null
+++ b/frontend/src/components/fields/ButtonField.js
@@ -0,0 +1,44 @@
+import React, {Component} from 'react';
+import './ButtonField.scss';
+import './common.scss';
+
+const classNames = require('classnames');
+
+class ButtonField extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const handler = () => {
+ if (typeof this.props.onClick === "function") {
+ this.props.onClick();
+ }
+ };
+
+ let buttonClassnames = {
+ "button-bordered": this.props.bordered,
+ };
+ if (this.props.variant) {
+ buttonClassnames[`button-variant-${this.props.variant}`] = true;
+ }
+
+ let buttonStyle = {};
+ if (this.props.color) {
+ buttonStyle["backgroundColor"] = this.props.color;
+ }
+ if (this.props.border) {
+ buttonStyle["borderColor"] = this.props.border;
+ }
+
+ return (
+ <div className={classNames( "field", "button-field", {"field-small": this.props.small})}>
+ <button type="button" className={classNames(classNames(buttonClassnames))}
+ onClick={handler} style={buttonStyle}>{this.props.name}</button>
+ </div>
+ );
+ }
+}
+
+export default ButtonField;
diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss
new file mode 100644
index 0000000..933279e
--- /dev/null
+++ b/frontend/src/components/fields/ButtonField.scss
@@ -0,0 +1,119 @@
+@import '../../colors.scss';
+
+.button-field {
+ font-size: 0.9em;
+
+ .button-bordered {
+ border-bottom: 5px solid $color-primary-1;
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+
+ .button-variant-red {
+ color: $color-red-light;
+ background-color: $color-red;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-red-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-red-light;
+ background-color: $color-red-dark;
+ }
+ }
+
+ .button-variant-pink {
+ color: $color-pink-light;
+ background-color: $color-pink;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-pink-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-pink-light;
+ background-color: $color-pink-dark;
+ }
+ }
+
+ .button-variant-purple {
+ color: $color-purple-light;
+ background-color: $color-purple;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-purple-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-purple-light;
+ background-color: $color-purple-dark;
+ }
+ }
+
+ .button-variant-deep-purple {
+ color: $color-deep-purple-light;
+ background-color: $color-deep-purple;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-deep-purple-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-deep-purple-light;
+ background-color: $color-deep-purple-dark;
+ }
+ }
+
+ .button-variant-indigo {
+ color: $color-indigo-light;
+ background-color: $color-indigo;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-indigo-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-indigo-light;
+ background-color: $color-indigo-dark;
+ }
+ }
+
+ .button-variant-blue {
+ color: $color-blue-light;
+ background-color: $color-blue;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-blue-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-blue-light;
+ background-color: $color-blue-dark;
+ }
+ }
+
+ .button-variant-green {
+ color: $color-green-light;
+ background-color: $color-green;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-green-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-green-light;
+ background-color: $color-green-dark;
+ }
+ }
+
+}
diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js
index 5cceac4..33f4f83 100644
--- a/frontend/src/components/fields/CheckField.js
+++ b/frontend/src/components/fields/CheckField.js
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import './CheckField.scss';
+import './common.scss';
import {randomClassName} from "../../utils";
const classNames = require('classnames');
@@ -23,10 +24,10 @@ class CheckField extends Component {
};
return (
- <div className={classNames( "check-field", {"field-checked" : checked}, {"field-small": small})}>
+ <div className={classNames( "field", "check-field", {"field-checked" : checked}, {"field-small": small})}>
<div className="field-input">
<input type="checkbox" id={this.id} checked={checked} onChange={handler} />
- <label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + name}</label>
+ <label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + (name != null ? name : "")}</label>
</div>
</div>
);
diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js
new file mode 100644
index 0000000..d409b21
--- /dev/null
+++ b/frontend/src/components/fields/ChoiceField.js
@@ -0,0 +1,59 @@
+import React, {Component} from 'react';
+import './ChoiceField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+
+class ChoiceField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expanded: false
+ };
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ render() {
+ const name = this.props.name || null;
+ const inline = this.props.inline;
+
+ const collapse = () => this.setState({expanded: false});
+ const expand = () => this.setState({expanded: true});
+
+ const handler = (key) => {
+ collapse();
+ if (this.props.onChange) {
+ this.props.onChange(key);
+ }
+ };
+
+ const keys = this.props.keys || [];
+ const values = this.props.values || [];
+
+ const options = keys.map((key, i) =>
+ <span className="field-option" key={key} onClick={() => handler(key)}>{values[i]}</span>
+ );
+
+ return (
+ <div className={classNames( "field", "choice-field", {"field-inline" : inline},
+ {"field-small": this.props.small})}>
+ {!inline && name && <label className="field-name">{name}:</label>}
+ <div className={classNames("field-select", {"select-expanded": this.state.expanded})}
+ tabIndex={0} onBlur={collapse} onClick={() => this.state.expanded ? collapse() : expand()}>
+ <div className="field-value">
+ {((inline && name) ? (name + ": ") : "") + (this.props.value || "select a value")}
+ </div>
+ <div className="field-options">
+ {options}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ChoiceField;
diff --git a/frontend/src/components/fields/ChoiceField.scss b/frontend/src/components/fields/ChoiceField.scss
new file mode 100644
index 0000000..7f32b0e
--- /dev/null
+++ b/frontend/src/components/fields/ChoiceField.scss
@@ -0,0 +1,69 @@
+@import '../../colors.scss';
+
+.choice-field {
+ font-size: 0.9em;
+
+ .field-name {
+ margin: 0;
+ }
+
+ .field-select {
+ position: relative;
+ margin-top: 5px;
+
+ .field-value {
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ border-radius: 5px;
+ padding: 7px 10px;
+ cursor: pointer;
+
+ &:after {
+ content: "⋎";
+ position: absolute;
+ right: 10px;
+ }
+ }
+
+ .field-options {
+ position: absolute;
+ top: 35px;
+ width: 100%;
+ z-index: 20;
+ border-top: 3px solid $color-primary-0;
+ border-radius: 5px;
+ background-color: $color-primary-2;
+ display: none;
+
+ .field-option {
+ display: block;
+ padding: 5px 10px;
+ cursor: pointer;
+ border-radius: 5px;
+ }
+
+ .field-option:hover {
+ background-color: $color-primary-1;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .field-select.select-expanded {
+ .field-options {
+ display: block;
+ }
+
+ .field-value:after {
+ content: "⋏";
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+}
diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js
index af3b3df..6cf967a 100644
--- a/frontend/src/components/fields/InputField.js
+++ b/frontend/src/components/fields/InputField.js
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import './InputField.scss';
+import './common.scss';
import {randomClassName} from "../../utils";
const classNames = require('classnames');
@@ -36,12 +37,12 @@ class InputField extends Component {
};
let inputProps = {};
if (type !== "file") {
- inputProps["value"] = value;
+ inputProps["value"] = value || this.props.initialValue;
}
return (
- <div className={classNames("input-field", {"field-active" : active}, {"field-invalid": invalid},
- {"field-small": small}, {"field-inline": inline})}>
+ <div className={classNames("field", "input-field", {"field-active" : active},
+ {"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}>
<div className="field-wrapper">
{ name &&
<div className="field-name">
diff --git a/frontend/src/components/fields/InputField.scss b/frontend/src/components/fields/InputField.scss
index cdb8c9f..79e2b7e 100644
--- a/frontend/src/components/fields/InputField.scss
+++ b/frontend/src/components/fields/InputField.scss
@@ -14,42 +14,20 @@
position: relative;
.field-value {
- input, .file-label {
+ .file-label {
background-color: $color-primary-2;
+ margin: 0;
width: 100%;
- border: none;
color: $color-primary-4;
border-radius: 5px;
padding: 7px 10px;
-
- &:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- outline: none;
- }
-
- &[readonly] {
- background-color: $color-primary-2;
- border: none;
- color: $color-primary-4;
- }
-
- &[readonly]:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- }
+ cursor: pointer;
}
input[type="file"] {
display: none;
}
- .file-label {
- margin: 0;
- }
-
.file-label:after {
content: "Browse";
position: absolute;
diff --git a/frontend/src/components/fields/TextField.js b/frontend/src/components/fields/TextField.js
index 86b98ed..de68c21 100644
--- a/frontend/src/components/fields/TextField.js
+++ b/frontend/src/components/fields/TextField.js
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import './TextField.scss';
+import './common.scss';
import {randomClassName} from "../../utils";
const classNames = require('classnames');
@@ -28,11 +29,11 @@ class TextField extends Component {
};
return (
- <div className={classNames("text-field", {"field-active": this.props.active},
+ <div className={classNames("field", "text-field", {"field-active": this.props.active},
{"field-invalid": this.props.invalid}, {"field-small": this.props.small})}>
{name && <label htmlFor={this.id}>{name}:</label>}
<textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows}
- readOnly={this.props.readonly} value={this.props.value} />
+ readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef} />
{error && <div className="field-error">error: {error}</div>}
</div>
);
diff --git a/frontend/src/components/fields/TextField.scss b/frontend/src/components/fields/TextField.scss
index 606f537..de831fb 100644
--- a/frontend/src/components/fields/TextField.scss
+++ b/frontend/src/components/fields/TextField.scss
@@ -10,32 +10,7 @@
}
textarea {
- background-color: $color-primary-2;
- width: 100%;
- border: none;
- color: $color-primary-4;
- border-radius: 5px;
- padding: 7px 10px;
resize: none;
-
- &:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- outline: none;
- }
-
- &[readonly] {
- background-color: $color-primary-2;
- border: none;
- color: $color-primary-4;
- }
-
- &[readonly]:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- }
}
&.field-active {
diff --git a/frontend/src/components/fields/common.scss b/frontend/src/components/fields/common.scss
new file mode 100644
index 0000000..f83a988
--- /dev/null
+++ b/frontend/src/components/fields/common.scss
@@ -0,0 +1,54 @@
+@import '../../colors.scss';
+
+.field {
+
+ input, textarea {
+ background-color: $color-primary-2;
+ width: 100%;
+ border: none;
+ color: $color-primary-4;
+ border-radius: 5px;
+ padding: 7px 10px;
+
+ &:focus {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ box-shadow: none;
+ outline: none;
+ }
+
+ &[readonly] {
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ }
+
+ &[readonly]:focus {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ box-shadow: none;
+ }
+ }
+
+ button {
+ border-radius: 0;
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ outline: none;
+ padding: 5px 12px;
+ font-weight: 500;
+
+ &:hover,
+ &:active {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ }
+
+ &:focus,
+ &:active {
+ outline: none !important;
+ box-shadow: none !important;
+ }
+ }
+}
diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js
new file mode 100644
index 0000000..edcb720
--- /dev/null
+++ b/frontend/src/components/fields/extensions/ColorField.js
@@ -0,0 +1,63 @@
+import React, {Component} from 'react';
+import {Button, ButtonGroup, Form, OverlayTrigger, Popover} from "react-bootstrap";
+import './ColorField.scss';
+import InputField from "../InputField";
+
+class ColorField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ invalid: false
+ };
+
+ this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1",
+ "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"];
+ }
+
+ render() {
+ const colorButtons = this.colors.map((color) =>
+ <span key={"button" + color} className="color-input"
+ style={{"backgroundColor": color, "borderColor": this.state.color === color ? "#fff" : color}}
+ onClick={() => {
+ this.setState({color: color});
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange(color);
+ }
+ document.body.click(); // magic to close popup
+ }} />);
+
+ const popover = (
+ <Popover id="popover-basic">
+ <Popover.Title as="h3">choose a color</Popover.Title>
+ <Popover.Content>
+ <div className="colors-container">
+ <div className="colors-row">
+ {colorButtons.slice(0, 8)}
+ </div>
+ <div className="colors-row">
+ {colorButtons.slice(8, 18)}
+ </div>
+ </div>
+ </Popover.Content>
+ </Popover>
+ );
+
+ return (
+ <div className="color-field">
+ <InputField {...this.props} name="color" />
+
+ <div className="color-picker">
+ <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
+ <Button variant="picker" size="sm">pick</Button>
+ </OverlayTrigger>
+ </div>
+
+ </div>
+ );
+ }
+
+}
+
+export default ColorField;
diff --git a/frontend/src/components/fields/extensions/ColorField.scss b/frontend/src/components/fields/extensions/ColorField.scss
new file mode 100644
index 0000000..c8f617c
--- /dev/null
+++ b/frontend/src/components/fields/extensions/ColorField.scss
@@ -0,0 +1,39 @@
+@import '../../../colors.scss';
+
+.color-field {
+ display: flex;
+ align-items: flex-end;
+
+ .input-field {
+ flex: 1;
+
+ input {
+ border-bottom-right-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
+ }
+
+ .color-picker {
+ margin-bottom: 5.5px;
+
+ .btn-picker {
+ padding: 8.5px 15px;
+ border-bottom-right-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color: $color-indigo;
+ }
+ }
+
+
+}
+
+.colors-container {
+ width: 600px;
+
+ .color-input {
+ display: inline-block;
+ width: 31px;
+ height: 31px;
+ cursor: pointer;
+ }
+} \ No newline at end of file
diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js
new file mode 100644
index 0000000..8823c42
--- /dev/null
+++ b/frontend/src/components/fields/extensions/NumericField.js
@@ -0,0 +1,39 @@
+import React, {Component} from 'react';
+import InputField from "../InputField";
+
+class NumericField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ invalid: false
+ };
+ }
+
+ 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);
+ }
+ };
+
+ return (
+ <InputField {...this.props} onChange={handler} initialValue={this.props.initialValue || 0}
+ invalid={this.state.invalid} />
+ );
+ }
+
+}
+
+export default NumericField;
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 621b6d6..0741bea 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -24,7 +24,7 @@ class RulesConnectionsFilter extends Component {
let params = new URLSearchParams(this.props.location.search);
let activeRules = params.getAll("matched_rules") || [];
- backend.get("/api/rules").then(res => {
+ backend.getJson("/api/rules").then(res => {
let rules = res.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/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js
index 701edf2..7e0fa6c 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapPane.js
@@ -3,7 +3,7 @@ import './PcapPane.scss';
import Table from "react-bootstrap/Table";
import backend from "../../backend";
import {createCurlCommand, formatSize, timestampToTime2} from "../../utils";
-import {Button, Col, Container, Form, Row} from "react-bootstrap";
+import {Button, Col, Container, Row} from "react-bootstrap";
import InputField from "../fields/InputField";
import CheckField from "../fields/CheckField";
import TextField from "../fields/TextField";
@@ -15,45 +15,48 @@ class PcapPane extends Component {
this.state = {
sessions: [],
- isFileValid: true,
- isFileFocused: false,
- selectedFile: null,
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadSelectedFile: null,
uploadFlushAll: false,
uploadStatusCode: null,
- uploadOutput: null
+ uploadOutput: null,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ fileFlushAll: false,
+ fileStatusCode: null,
+ fileOutput: null,
+ deleteOriginalFile: false
};
-
- this.loadSessions = this.loadSessions.bind(this);
- this.handleFileChange = this.handleFileChange.bind(this);
- this.handleUploadPcap = this.handleUploadPcap.bind(this);
}
componentDidMount() {
this.loadSessions();
}
- loadSessions() {
- backend.get("/api/pcap/sessions").then(res => this.setState({sessions: res}));
- }
+ loadSessions = () => {
+ backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res}));
+ };
- handleFileChange(file) {
+ handleUploadFileChange = (file) => {
this.setState({
- isFileValid: file != null && file.type.endsWith("pcap"),
- isFileFocused: false,
- selectedFile: file
+ isUploadFileValid: file != null && file.type.endsWith("pcap"),
+ isUploadFileFocused: false,
+ uploadSelectedFile: file
});
- }
+ };
- handleUploadPcap() {
- if (this.state.selectedFile == null || !this.state.isFileValid) {
- this.setState({isFileFocused: true});
+ handleUploadPcap = () => {
+ if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) {
+ this.setState({isUploadFileFocused: true});
return;
}
const formData = new FormData();
formData.append(
"file",
- this.state.selectedFile
+ this.state.uploadSelectedFile
);
backend.postFile("/api/pcap/upload", formData).then(response =>
@@ -62,7 +65,33 @@ class PcapPane extends Component {
uploadOutput: JSON.stringify(result)
}))
);
- }
+ };
+
+ handleFileChange = (file) => {
+ this.setState({
+ isFileValid: file !== "" && file.endsWith("pcap"),
+ isFileFocused: false,
+ fileValue: file
+ });
+ };
+
+ handleProcessPcap = () => {
+ if (this.state.fileValue === "" || !this.state.isFileValid) {
+ this.setState({isFileFocused: true});
+ return;
+ }
+
+ backend.post("/api/pcap/file", {
+ file: this.state.fileValue,
+ flush_all: this.state.fileFlushAll,
+ delete_original_file: this.state.deleteOriginalFile
+ }).then(response =>
+ response.json().then(result => this.setState({
+ fileStatusCode: response.status + " " + response.statusText,
+ fileOutput: JSON.stringify(result)
+ }))
+ );
+ };
render() {
let sessions = this.state.sessions.map(s =>
@@ -82,7 +111,7 @@ class PcapPane extends Component {
const uploadOutput = this.state.uploadOutput != null ? this.state.uploadOutput :
createCurlCommand("pcap/upload", "POST", null, {
- file: "@" + ((this.state.selectedFile != null && this.state.isFileValid) ? this.state.selectedFile.name :
+ file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ? this.state.uploadSelectedFile.name :
"invalid.pcap"),
flush_all: this.state.uploadFlushAll
})
@@ -120,17 +149,17 @@ class PcapPane extends Component {
<div className="pane-section">
<Container className="p-0">
<Row>
- <Col>
+ <Col style={{"paddingRight": "0"}}>
<div className="section-header">
<span className="api-request">POST /api/pcap/upload</span>
<span className="api-response">{this.state.uploadStatusCode}</span>
</div>
<div className="section-content">
- <InputField type={"file"} name={"file"} invalid={!this.state.isFileValid}
- active={this.state.isFileFocused}
- onChange={this.handleFileChange} value={this.state.selectedFile}
- defaultValue={"No .pcap[ng] selected"}/>
+ <InputField type={"file"} name={"file"} invalid={!this.state.isUploadFileValid}
+ active={this.state.isUploadFileFocused}
+ onChange={this.handleUploadFileChange} value={this.state.uploadSelectedFile}
+ defaultValue={"no .pcap[ng] selected"}/>
<div className="upload-actions">
<div className="upload-options">
@@ -148,24 +177,31 @@ class PcapPane extends Component {
<Col>
<div className="section-header">
<span className="api-request">POST /api/pcap/file</span>
- <span className="api-response"></span>
+ <span className="api-response">{this.state.fileStatusCode}</span>
</div>
<div className="section-content">
- <Form.Control type="text" id="pcap-upload" className="custom-file"
- onChange={this.onLocalFileChange} placeholder="local .pcap/.pcapng"
- custom
- />
+ <InputField name="file" active={this.state.isUploadFileFocused}
+ onChange={this.handleFileChange} value={this.state.uploadSelectedFile}
+ defaultValue={"local .pcap[ng] path"} inline/>
+
+ <div className="upload-actions" style={{"marginTop": "11px"}}>
+ <div className="upload-options">
+ <CheckField name="flush_all" checked={this.state.uploadFlushAll}
+ onChange={v => this.setState({uploadFlushAll: v})}/>
+ <CheckField name="delete_original_file" checked={this.state.uploadFlushAll}
+ onChange={v => this.setState({uploadFlushAll: v})}/>
+ </div>
+ <Button variant="blue" onClick={this.handleUploadPcap}>process</Button>
+ </div>
+
+ <TextField value={uploadOutput} rows={4} readonly small={true}/>
</div>
</Col>
</Row>
</Container>
-
-
</div>
-
</div>
-
);
}
}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
new file mode 100644
index 0000000..fbc8785
--- /dev/null
+++ b/frontend/src/components/panels/RulePane.js
@@ -0,0 +1,166 @@
+import React, {Component} from 'react';
+import './RulePane.scss';
+import Table from "react-bootstrap/Table";
+import {Button, Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import CheckField from "../fields/CheckField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ChoiceField from "../fields/ChoiceField";
+import ButtonField from "../fields/ButtonField";
+
+class RulePane extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ rules: [],
+ };
+ }
+
+ componentDidMount() {
+ this.loadRules();
+ }
+
+ loadRules = () => {
+ backend.getJson("/api/rules").then(res => this.setState({rules: res}));
+ };
+
+
+ render() {
+ let rules = this.state.rules.map(r =>
+ <tr className="table-row">
+ <td>{r["id"].substring(0, 8)}</td>
+ <td>{r["name"]}</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>
+ );
+
+ return (
+ <div className="pane-container rule-pane">
+ <div className="pane-section">
+ <div className="section-header">
+ <span className="api-request">GET /api/rules</span>
+ <span className="api-response">200 OK</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>
+ </div>
+
+ <div className="pane-section">
+ <div className="section-header">
+ <span className="api-request">POST /api/rules</span>
+ <span className="api-response"></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} />
+ </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>
+
+ <Col>
+ <NumericField name="min_duration" inline />
+ <NumericField name="max_duration" inline />
+ <NumericField name="min_bytes" inline />
+ <NumericField name="max_bytes" inline />
+
+ </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>
+ <tr>
+ <th>regex</th>
+ <th>Aa</th>
+ <th>.*</th>
+ <th>\n+</th>
+ <th>UTF8</th>
+ <th>Uni_</th>
+ <th>min</th>
+ <th>max</th>
+ <th>direction</th>
+ <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 small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} value="both" /></td>
+ <td><Button variant="green" size="sm">add</Button></td>
+ </tr>
+ </tbody>
+ </Table>
+ </div>
+
+ <ButtonField name="add_rule" variant="green" bordered />
+ <br />
+ <ButtonField name="add_rule" small color="red"/>
+ <br />
+ <ButtonField name="add_rule" bordered border={"green"} />
+ </div>
+ </div>
+
+
+ </div>
+ );
+ }
+
+}
+
+export default RulePane;
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss
new file mode 100644
index 0000000..b030c6a
--- /dev/null
+++ b/frontend/src/components/panels/RulePane.scss
@@ -0,0 +1,16 @@
+
+.rule-pane {
+ .post-rules-actions {
+ display: flex;
+
+ .rules-options {
+ flex: 1;
+ }
+
+ button {
+ margin-left: 10px;
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/frontend/src/index.scss b/frontend/src/index.scss
index 5d1bbfa..9dcc692 100644
--- a/frontend/src/index.scss
+++ b/frontend/src/index.scss
@@ -21,118 +21,6 @@ pre {
font-size: 14px;
}
-.btn {
- border-radius: 0;
- background-color: $color-primary-2;
- border: none;
- border-bottom: 5px solid $color-primary-1;
- color: $color-primary-4;
- outline: none;
- padding: 5px 12px;
- font-weight: 500;
-
- &:hover,
- &:active {
- background-color: $color-primary-1;
- color: $color-primary-4;
- }
-
- &:focus,
- &:active {
- outline: none !important;
- box-shadow: none !important;
- }
-}
-
-.btn-sm {
- border: none;
- font-size: 12px;
-}
-
-.btn-red {
- color: $color-red-light;
- background-color: $color-red;
- border-bottom: 5px solid $color-red-dark;
-
- &:hover,
- &:active {
- color: $color-red-light;
- background-color: $color-red-dark;
- }
-}
-
-.btn-pink {
- color: $color-pink-light;
- background-color: $color-pink;
- border-bottom: 5px solid $color-pink-dark;
-
- &:hover,
- &:active {
- color: $color-pink-light;
- background-color: $color-pink-dark;
- }
-}
-
-.btn-purple {
- color: $color-purple-light;
- background-color: $color-purple;
- border-bottom: 5px solid $color-purple-dark;
-
- &:hover,
- &:active {
- color: $color-purple-light;
- background-color: $color-purple-dark;
- }
-}
-
-.btn-deep-purple {
- color: $color-deep-purple-light;
- background-color: $color-deep-purple;
- border-bottom: 5px solid $color-deep-purple-dark;
-
- &:hover,
- &:active {
- color: $color-deep-purple-light;
- background-color: $color-deep-purple-dark;
- }
-}
-
-.btn-indigo {
- color: $color-indigo-light;
- background-color: $color-indigo;
- border-bottom: 5px solid $color-indigo-dark;
-
- &:hover,
- &:active {
- color: $color-indigo-light;
- background-color: $color-indigo-dark;
- }
-}
-
-.btn-blue {
- color: $color-blue-light;
- background-color: $color-blue;
- border-bottom: 5px solid $color-blue-dark;
-
- &:hover,
- &:active {
- color: $color-blue-light;
- background-color: $color-blue-dark;
- }
-}
-
-.btn-green {
- color: $color-green-light;
- background-color: $color-green;
- border-bottom: 5px solid $color-green-dark;
-
- &:hover,
- &:active {
- color: $color-green-light;
- background-color: $color-green-dark;
- }
-}
-
a {
color: $color-primary-4;
@@ -190,3 +78,7 @@ textarea.form-control {
.text-muted {
color: $color-primary-4 !important;
}
+
+.popover-header {
+ color: $color-primary-1;
+} \ No newline at end of file
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index da8958b..f3fec64 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -25,21 +25,21 @@ class Connections extends Component {
this.scrollBottomThreashold = 0.99999;
this.maxConnections = 500;
this.queryLimit = 50;
-
- this.handleScroll = this.handleScroll.bind(this);
- this.connectionSelected = this.connectionSelected.bind(this);
- this.addServicePortFilter = this.addServicePortFilter.bind(this);
}
componentDidMount() {
this.loadConnections({limit: this.queryLimit})
.then(() => this.setState({loaded: true}));
+ if (this.props.initialConnection != null) {
+ this.setState({selected: this.props.initialConnection.id});
+ // TODO: scroll to initial connection
+ }
}
- connectionSelected(c) {
+ connectionSelected = (c) => {
this.setState({selected: c.id});
this.props.onSelected(c);
- }
+ };
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
@@ -49,7 +49,7 @@ class Connections extends Component {
}
}
- handleScroll(e) {
+ handleScroll = (e) => {
let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
@@ -59,13 +59,13 @@ class Connections extends Component {
this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
.then(() => console.log("Previous connections loaded"));
}
- }
+ };
- addServicePortFilter(port) {
+ addServicePortFilter = (port) => {
let urlParams = new URLSearchParams(this.props.location.search);
urlParams.set("service_port", port);
this.setState({queryString: "?" + urlParams});
- }
+ };
async loadConnections(params) {
let url = "/api/connections";
@@ -75,7 +75,7 @@ class Connections extends Component {
}
this.setState({loading: true, prevParams: params});
- let res = await backend.get(`${url}?${urlParams}`);
+ let res = await backend.getJson(`${url}?${urlParams}`);
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.get("/api/rules");
+ rules = await backend.getJson("/api/rules");
flagRule = rules.filter(rule => {
return rule.name === "flag";
})[0];
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index e2d0e6a..a022636 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -72,7 +72,9 @@ class Header extends Component {
<Link to="/pcaps">
<Button variant="purple">pcaps</Button>
</Link>
- <Button variant="deep-purple" onClick={this.props.onOpenRules}>rules</Button>
+ <Link to="/rules">
+ <Button variant="deep-purple">rules</Button>
+ </Link>
<Button variant="indigo" onClick={this.props.onOpenServices}>services</Button>
<Button variant="blue" onClick={this.props.onOpenConfig}
disabled={false}>config</Button>
diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js
index ce755d1..9d3f7b7 100644
--- a/frontend/src/views/MainPane.js
+++ b/frontend/src/views/MainPane.js
@@ -5,24 +5,25 @@ import ConnectionContent from "../components/ConnectionContent";
import {Route, Switch, withRouter} from "react-router-dom";
import PcapPane from "../components/panels/PcapPane";
import backend from "../backend";
+import RulePane from "../components/panels/RulePane";
class MainPane extends Component {
constructor(props) {
super(props);
this.state = {
- selectedConnection: null
+ selectedConnection: null,
+ loading: false
};
}
componentDidMount() {
- if ('id' in this.props.match.params) {
- const id = this.props.match.params.id;
- backend.get(`/api/connections/${id}`).then(res => {
- if (res.status === 200) {
- this.setState({selectedConnection: res});
- }
- });
+ const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
+ if (match != null) {
+ this.setState({loading: true});
+ backend.getJson(`/api/connections/${match[1]}`)
+ .then(connection => this.setState({selectedConnection: connection, loading: false}))
+ .catch(error => console.log(error));
}
}
@@ -32,12 +33,18 @@ class MainPane extends Component {
<div className="container-fluid">
<div className="row">
<div className="col-md-6 pane">
- <Connections onSelected={(c) => this.setState({selectedConnection: c})} />
+ {
+ !this.state.loading &&
+ <Connections onSelected={(c) => this.setState({selectedConnection: c})}
+ initialConnection={this.state.selectedConnection} />
+ }
</div>
<div className="col-md-6 pl-0 pane">
<Switch>
<Route path="/pcaps" children={<PcapPane />} />
- <Route children={<ConnectionContent connection={this.state.selectedConnection} />} />
+ <Route path="/rules" children={<RulePane />} />
+ <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} />
+ <Route children={<ConnectionContent />} />
</Switch>
</div>
</div>