From d5ed31be3b6c97f92be4e94b70d32d1b89932ae9 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Wed, 30 Sep 2020 15:58:16 +0200 Subject: Complete services page --- frontend/src/components/fields/InputField.js | 3 +- .../src/components/fields/extensions/ColorField.js | 34 +++- .../components/fields/extensions/ColorField.scss | 42 +++-- frontend/src/components/panels/RulePane.js | 16 +- frontend/src/components/panels/ServicePane.js | 190 +++++++++++++++++++++ frontend/src/components/panels/ServicePane.scss | 22 +++ frontend/src/utils.js | 4 +- frontend/src/validation.js | 2 +- frontend/src/views/Header.js | 4 +- frontend/src/views/MainPane.js | 2 + 10 files changed, 281 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/panels/ServicePane.js create mode 100644 frontend/src/components/panels/ServicePane.scss 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" && } + aria-describedby={this.id} onChange={handler} {...inputProps} + readOnly={this.props.readonly} /> { type !== "file" && value !== "" &&
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) => { - 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 (
- -
- - - +
+ +
+ + + +
+ {this.props.error &&
{this.props.error}
}
); } 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 { filters: 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} /> 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} /> this.updateParam((r) => r.filter.client_address = v)} /> this.updateParam((r) => r.filter.min_duration = v)} /> this.updateParam((r) => r.filter.max_duration = v)} /> this.updateParam((r) => r.filter.min_bytes = v)} /> this.updateParam((r) => r.filter.max_bytes = v)} /> 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 => + { + this.reset(); + this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> + {s["port"]} + {s["name"]} + + {s["notes"]} + + ); + + const curlCommand = createCurlCommand("/services", "PUT", service); + + return ( +
+
+
+ GET /api/services + {this.state.servicesStatusCode && + } +
+ +
+
+ + + + + + + + + + + {services} + +
portnamecolornotes
+
+
+
+ +
+
+ PUT /api/services + +
+ +
+ + + + this.updateParam((s) => s.port = v)} + min={0} max={65565} error={this.state.servicePortError} /> + this.updateParam((s) => s.name = v)} + error={this.state.serviceNameError} /> + this.updateParam((s) => s.color = v)} /> + + + + this.updateParam((s) => s.notes = v)} /> + + + + + +
+ +
+ {} + +
+
+
+ ); + } + +} + +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 { - + + +
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 { } /> } /> + } /> } /> } /> -- cgit v1.2.3-70-g09d2