diff options
author | Emiliano Ciavatta | 2020-09-30 13:58:16 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-09-30 13:58:16 +0000 |
commit | d5ed31be3b6c97f92be4e94b70d32d1b89932ae9 (patch) | |
tree | 0deffc50058f731c51ba4b804c9db7eaff1c94a0 | |
parent | 43135c255d82aa7c54ea83b14369c93425ae75f6 (diff) |
Complete services page
-rw-r--r-- | frontend/src/components/fields/InputField.js | 3 | ||||
-rw-r--r-- | frontend/src/components/fields/extensions/ColorField.js | 34 | ||||
-rw-r--r-- | frontend/src/components/fields/extensions/ColorField.scss | 42 | ||||
-rw-r--r-- | frontend/src/components/panels/RulePane.js | 16 | ||||
-rw-r--r-- | frontend/src/components/panels/ServicePane.js | 190 | ||||
-rw-r--r-- | frontend/src/components/panels/ServicePane.scss | 22 | ||||
-rw-r--r-- | frontend/src/utils.js | 4 | ||||
-rw-r--r-- | frontend/src/validation.js | 2 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 4 | ||||
-rw-r--r-- | frontend/src/views/MainPane.js | 2 |
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> |