aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-09-30 13:58:16 +0000
committerEmiliano Ciavatta2020-09-30 13:58:16 +0000
commitd5ed31be3b6c97f92be4e94b70d32d1b89932ae9 (patch)
tree0deffc50058f731c51ba4b804c9db7eaff1c94a0 /frontend
parent43135c255d82aa7c54ea83b14369c93425ae75f6 (diff)
Complete services page
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/components/fields/InputField.js3
-rw-r--r--frontend/src/components/fields/extensions/ColorField.js34
-rw-r--r--frontend/src/components/fields/extensions/ColorField.scss42
-rw-r--r--frontend/src/components/panels/RulePane.js16
-rw-r--r--frontend/src/components/panels/ServicePane.js190
-rw-r--r--frontend/src/components/panels/ServicePane.scss22
-rw-r--r--frontend/src/utils.js4
-rw-r--r--frontend/src/validation.js2
-rw-r--r--frontend/src/views/Header.js4
-rw-r--r--frontend/src/views/MainPane.js2
10 files changed, 281 insertions, 38 deletions
diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js
index b790891..84c981b 100644
--- a/frontend/src/components/fields/InputField.js
+++ b/frontend/src/components/fields/InputField.js
@@ -55,7 +55,8 @@ class InputField extends Component {
{ type === "file" && <label for={this.id} className={"file-label"}>
{value.name || this.props.placeholder}</label> }
<input type={type} placeholder={this.props.placeholder} id={this.id}
- aria-describedby={this.id} onChange={handler} {...inputProps} />
+ aria-describedby={this.id} onChange={handler} {...inputProps}
+ readOnly={this.props.readonly} />
</div>
{ type !== "file" && value !== "" &&
<div className="field-clear">
diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js
index c1c210f..96ebc49 100644
--- a/frontend/src/components/fields/extensions/ColorField.js
+++ b/frontend/src/components/fields/extensions/ColorField.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
import {OverlayTrigger, Popover} from "react-bootstrap";
import './ColorField.scss';
import InputField from "../InputField";
+import validation from "../../../validation";
class ColorField extends Component {
@@ -15,11 +16,24 @@ class ColorField extends Component {
"#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"];
}
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (prevProps.value !== this.props.value) {
+ this.onChange(this.props.value);
+ }
+ }
+
+ onChange = (value) => {
+ this.setState({invalid: value !== "" && !validation.isValidColor(value)});
+
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange(value);
+ }
+ };
+
render() {
const colorButtons = this.colors.map((color) =>
<span key={"button" + color} className="color-input" style={{"backgroundColor": color}}
onClick={() => {
- this.setState({color: color});
if (typeof this.props.onChange === "function") {
this.props.onChange(color);
}
@@ -43,18 +57,22 @@ class ColorField extends Component {
);
let buttonStyles = {};
- if (this.state.color) {
- buttonStyles["backgroundColor"] = this.state.color;
+ if (this.props.value) {
+ buttonStyles["backgroundColor"] = this.props.value;
}
return (
<div className="field color-field">
- <InputField {...this.props} name="color" />
- <div className="color-picker">
- <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
- <button type="button" className="picker-button" style={buttonStyles}>pick</button>
- </OverlayTrigger>
+ <div className="color-input">
+ <InputField {...this.props} onChange={this.onChange} invalid={this.state.invalid} name="color"
+ error={null} />
+ <div className="color-picker">
+ <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
+ <button type="button" className="picker-button" style={buttonStyles}>pick</button>
+ </OverlayTrigger>
+ </div>
</div>
+ {this.props.error && <div className="color-error">{this.props.error}</div>}
</div>
);
}
diff --git a/frontend/src/components/fields/extensions/ColorField.scss b/frontend/src/components/fields/extensions/ColorField.scss
index 1c3931f..6eabbda 100644
--- a/frontend/src/components/fields/extensions/ColorField.scss
+++ b/frontend/src/components/fields/extensions/ColorField.scss
@@ -1,29 +1,37 @@
@import '../../../colors.scss';
.color-field {
- display: flex;
- align-items: flex-end;
+ .color-input {
+ display: flex;
+ align-items: flex-end;
- .input-field {
- flex: 1;
+ .input-field {
+ flex: 1;
- input {
- border-bottom-right-radius: 0 !important;
- border-top-right-radius: 0 !important;
+ input {
+ border-bottom-right-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
}
- }
- .color-picker {
- margin-bottom: 5.5px;
+ .color-picker {
+ margin-bottom: 5px;
- .picker-button {
- font-size: 0.8em;
- padding: 8px 15px;
- border-bottom-right-radius: 5px;
- border-top-right-radius: 5px;
- background-color: $color-primary-1;
+ .picker-button {
+ font-size: 0.8em;
+ padding: 8px 15px;
+ border-bottom-right-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color: $color-primary-1;
+ }
}
}
+
+ .color-error {
+ font-size: 0.8em;
+ color: $color-secondary-0;
+ margin-left: 10px;
+ }
}
.colors-container {
@@ -35,4 +43,4 @@
height: 31px;
cursor: pointer;
}
-} \ No newline at end of file
+}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
index d4c5460..7cb849c 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulePane.js
@@ -344,27 +344,29 @@ class RulePane extends Component {
<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} />
+ min={0} max={65565} error={this.state.ruleServicePortError}
+ readonly={isUpdate} />
<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} />
+ min={0} max={65565} error={this.state.ruleClientPortError}
+ readonly={isUpdate} />
<InputField name="client_address" value={rule.filter.client_address}
- error={this.state.ruleClientAddressError}
+ error={this.state.ruleClientAddressError} readonly={isUpdate}
onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} />
</Col>
<Col style={{"paddingTop": "11px"}}>
<NumericField name="min_duration" inline value={rule.filter.min_duration}
- error={this.state.ruleDurationError}
+ error={this.state.ruleDurationError} readonly={isUpdate}
onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} />
<NumericField name="max_duration" inline value={rule.filter.max_duration}
- error={this.state.ruleDurationError}
+ error={this.state.ruleDurationError} readonly={isUpdate}
onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} />
<NumericField name="min_bytes" inline value={rule.filter.min_bytes}
- error={this.state.ruleBytesError}
+ error={this.state.ruleBytesError} readonly={isUpdate}
onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} />
<NumericField name="max_bytes" inline value={rule.filter.max_bytes}
- error={this.state.ruleBytesError}
+ error={this.state.ruleBytesError} readonly={isUpdate}
onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} />
</Col>
</Row>
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js
new file mode 100644
index 0000000..b21ad6c
--- /dev/null
+++ b/frontend/src/components/panels/ServicePane.js
@@ -0,0 +1,190 @@
+import React, {Component} from 'react';
+import './common.scss';
+import './ServicePane.scss';
+import Table from "react-bootstrap/Table";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ButtonField from "../fields/ButtonField";
+import validation from "../../validation";
+import LinkPopover from "../objects/LinkPopover";
+import {createCurlCommand} from "../../utils";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class ServicePane extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ services: [],
+ currentService: this.emptyService,
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadServices();
+ }
+
+ emptyService = {
+ "port": 0,
+ "name": "",
+ "color": "",
+ "notes": ""
+ };
+
+ loadServices = () => {
+ backend.get("/api/services")
+ .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
+ .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
+ };
+
+ updateService = () => {
+ const service = this.state.currentService;
+ if (this.validateService(service)) {
+ backend.put("/api/services", service).then(res => {
+ this.reset();
+ this.setState({serviceStatusCode: res.status});
+ this.loadServices();
+ }).catch(res => {
+ this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateService = (service) => {
+ let valid = true;
+ if (!validation.isValidPort(service.port, true)) {
+ this.setState({servicePortError: "port < 0 || port > 65565"});
+ valid = false;
+ }
+ if (service.name.length < 3) {
+ this.setState({serviceNameError: "name.length < 3"});
+ valid = false;
+ }
+ if (!validation.isValidColor(service.color)) {
+ this.setState({serviceColorError: "color is not hexcolor"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ reset = () => {
+ this.setState({
+ isUpdate: false,
+ currentService: _.cloneDeep(this.emptyService),
+ servicePortError: null,
+ serviceNameError: null,
+ serviceColorError: null,
+ serviceStatusCode: null,
+ servicesStatusCode: null,
+ serviceResponse: null,
+ servicesResponse: null
+ });
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentService);
+ this.setState({currentService: this.state.currentService});
+ };
+
+ render() {
+ const isUpdate = this.state.isUpdate;
+ const service = this.state.currentService;
+
+ let services = this.state.services.map(s =>
+ <tr onClick={() => {
+ this.reset();
+ this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
+ }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
+ <td>{s["port"]}</td>
+ <td>{s["name"]}</td>
+ <td><ButtonField name={s["color"]} color={s["color"]} small /></td>
+ <td>{s["notes"]}</td>
+ </tr>
+ );
+
+ const curlCommand = createCurlCommand("/services", "PUT", service);
+
+ return (
+ <div className="pane-container service-pane">
+ <div className="pane-section services-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/services</span>
+ {this.state.servicesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
+ content={this.state.servicesResponse}
+ placement="left" /></span>}
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>port</th>
+ <th>name</th>
+ <th>color</th>
+ <th>notes</th>
+ </tr>
+ </thead>
+ <tbody>
+ {services}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="pane-section service-edit">
+ <div className="section-header">
+ <span className="api-request">PUT /api/services</span>
+ <span className="api-response"><LinkPopover text={this.state.serviceStatusCode}
+ content={this.state.serviceResponse}
+ placement="left" /></span>
+ </div>
+
+ <div className="section-content">
+ <Container className="p-0">
+ <Row>
+ <Col>
+ <NumericField name="port" value={service.port}
+ onChange={(v) => this.updateParam((s) => s.port = v)}
+ min={0} max={65565} error={this.state.servicePortError} />
+ <InputField name="name" value={service.name}
+ onChange={(v) => this.updateParam((s) => s.name = v)}
+ error={this.state.serviceNameError} />
+ <ColorField value={service.color} error={this.state.serviceColorError}
+ onChange={(v) => this.updateParam((s) => s.color = v)} />
+ </Col>
+
+ <Col>
+ <TextField name="notes" rows={7} value={service.notes}
+ onChange={(v) => this.updateParam((s) => s.notes = v)} />
+ </Col>
+ </Row>
+ </Container>
+
+ <TextField value={curlCommand} rows={3} readonly small={true}/>
+ </div>
+
+ <div className="section-footer">
+ {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
+ <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_service" : "add_service"}
+ bordered onClick={this.updateService} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default ServicePane;
diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicePane.scss
new file mode 100644
index 0000000..0b154e6
--- /dev/null
+++ b/frontend/src/components/panels/ServicePane.scss
@@ -0,0 +1,22 @@
+
+.service-pane {
+ display: flex;
+ flex-direction: column;
+
+ .services-list {
+ flex: 1;
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .service-edit {
+ flex: 0;
+ }
+}
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index 0707575..e71067a 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -1,5 +1,3 @@
-import React from "react";
-
const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
export function createCurlCommand(subCommand, method = null, json = null, data = null) {
@@ -111,5 +109,5 @@ export function formatSize(size) {
}
export function randomClassName() {
- return Math.random().toString(36).slice(2)
+ return Math.random().toString(36).slice(2);
}
diff --git a/frontend/src/validation.js b/frontend/src/validation.js
index eac8774..8f3409f 100644
--- a/frontend/src/validation.js
+++ b/frontend/src/validation.js
@@ -1,6 +1,6 @@
const validation = {
- isValidColor: (color) => true, // TODO
+ isValidColor: (color) => /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(color),
isValidPort: (port, required) => parseInt(port) > (required ? 0 : -1) && parseInt(port) <= 65565,
isValidAddress: (address) => true // TODO
};
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index 06cb20e..c38c22d 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -75,7 +75,9 @@ class Header extends Component {
<Link to="/rules">
<ButtonField variant="deep-purple" name="rules" bordered />
</Link>
- <ButtonField variant="indigo" onClick={this.props.onOpenServices} name="services" bordered />
+ <Link to="/services">
+ <ButtonField variant="indigo" name="services" bordered />
+ </Link>
<ButtonField variant="blue" onClick={this.props.onOpenConfig}
disabled={false} name="config" bordered />
</div>
diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js
index b9ebadb..d2950ab 100644
--- a/frontend/src/views/MainPane.js
+++ b/frontend/src/views/MainPane.js
@@ -6,6 +6,7 @@ import {Route, Switch, withRouter} from "react-router-dom";
import PcapPane from "../components/panels/PcapPane";
import backend from "../backend";
import RulePane from "../components/panels/RulePane";
+import ServicePane from "../components/panels/ServicePane";
class MainPane extends Component {
@@ -43,6 +44,7 @@ class MainPane extends Component {
<Switch>
<Route path="/pcaps" children={<PcapPane />} />
<Route path="/rules" children={<RulePane />} />
+ <Route path="/services" children={<ServicePane />} />
<Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} />
<Route children={<ConnectionContent />} />
</Switch>