diff options
Diffstat (limited to 'frontend/src/components')
-rw-r--r-- | frontend/src/components/Notifications.js | 5 | ||||
-rw-r--r-- | frontend/src/components/Notifications.scss | 6 | ||||
-rw-r--r-- | frontend/src/components/Timeline.js | 32 | ||||
-rw-r--r-- | frontend/src/components/dialogs/CommentDialog.js | 70 | ||||
-rw-r--r-- | frontend/src/components/dialogs/CopyDialog.js (renamed from frontend/src/components/objects/MessageAction.js) | 42 | ||||
-rw-r--r-- | frontend/src/components/objects/Connection.js | 30 | ||||
-rw-r--r-- | frontend/src/components/objects/MessageAction.scss | 8 | ||||
-rw-r--r-- | frontend/src/components/pages/MainPage.js | 2 | ||||
-rw-r--r-- | frontend/src/components/panels/ConfigPane.js | 39 | ||||
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.js | 2 | ||||
-rw-r--r-- | frontend/src/components/panels/StreamsPane.js | 15 |
11 files changed, 194 insertions, 57 deletions
diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 0b47b43..8356e80 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -66,6 +66,11 @@ class Notifications extends Component { n.description = `${n.message["processed_packets"]} packets processed`; n.variant = "blue"; return this.pushNotification(n); + case "timeline.range.large": + n.title = "timeline cropped"; + n.description = `the maximum range is 24h`; + n.variant = "red"; + return this.pushNotification(n); default: return null; } diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss index 5852c7d..5a032c8 100644 --- a/frontend/src/components/Notifications.scss +++ b/frontend/src/components/Notifications.scss @@ -45,5 +45,11 @@ border-left: 5px solid $color-blue-dark; background-color: $color-blue; } + + &.notification-red { + color: $color-red-light; + border-left: 5px solid $color-red-dark; + background-color: $color-red; + } } } diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js index 5443a3b..ec791e0 100644 --- a/frontend/src/components/Timeline.js +++ b/frontend/src/components/Timeline.js @@ -35,6 +35,7 @@ import ChoiceField from "./fields/ChoiceField"; import "./Timeline.scss"; const minutes = 60 * 1000; +const maxTimelineRange = 24 * 60 * minutes; const classNames = require("classnames"); const leftSelectionPaddingMultiplier = 24; @@ -109,8 +110,25 @@ class Timeline extends Component { const zeroFilledMetrics = []; const toTime = (m) => new Date(m["range_start"]).getTime(); - let i = 0; - for (let interval = toTime(metrics[0]) - minutes; interval <= toTime(metrics[metrics.length - 1]) + minutes; interval += minutes) { + + let i; + let timeStart = toTime(metrics[0]) - minutes; + for (i = 0; timeStart < 0 && i < metrics.length; i++) { // workaround to remove negative timestamps :( + timeStart = toTime(metrics[i]) - minutes; + } + + let timeEnd = toTime(metrics[metrics.length - 1]) + minutes; + if (timeEnd - timeStart > maxTimelineRange) { + timeEnd = timeStart + maxTimelineRange; + + const now = new Date().getTime(); + if (!this.lastDisplayNotificationTime || this.lastDisplayNotificationTime + minutes < now) { + this.lastDisplayNotificationTime = now; + dispatcher.dispatch("notifications", {event: "timeline.range.large"}); + } + } + + for (let interval = timeStart; interval <= timeEnd; interval += minutes) { if (i < metrics.length && interval === toTime(metrics[i])) { const m = metrics[i++]; m["range_start"] = new Date(m["range_start"]); @@ -204,10 +222,12 @@ class Timeline extends Component { }; handleConnectionUpdates = (payload) => { - this.setState({ - selection: new TimeRange(payload.from, payload.to), - }); - this.adjustSelection(); + if (payload.from >= this.state.start && payload.from < payload.to && payload.to <= this.state.end) { + this.setState({ + selection: new TimeRange(payload.from, payload.to), + }); + this.adjustSelection(); + } }; handleNotifications = (payload) => { diff --git a/frontend/src/components/dialogs/CommentDialog.js b/frontend/src/components/dialogs/CommentDialog.js new file mode 100644 index 0000000..970aa83 --- /dev/null +++ b/frontend/src/components/dialogs/CommentDialog.js @@ -0,0 +1,70 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {Modal} from "react-bootstrap"; +import backend from "../../backend"; +import log from "../../log"; +import ButtonField from "../fields/ButtonField"; +import TextField from "../fields/TextField"; + +class CommentDialog extends Component { + + state = {}; + + componentDidMount() { + this.setState({comment: this.props.initialComment || ""}); + } + + setComment = () => { + if (this.state.comment === this.props.initialComment) { + return this.close(); + } + const comment = this.state.comment || null; + backend.post(`/api/connections/${this.props.connectionId}/comment`, {comment}) + .then((_) => { + this.close(); + }).catch((e) => { + log.error(e); + this.setState({error: "failed to save comment"}); + }); + }; + + close = () => this.props.onSave(this.state.comment || null); + + render() { + return ( + <Modal show size="md" aria-labelledby="comment-dialog" centered> + <Modal.Header> + <Modal.Title id="comment-dialog"> + ~/.comment + </Modal.Title> + </Modal.Header> + <Modal.Body> + <TextField value={this.state.comment} onChange={(comment) => this.setState({comment})} + rows={7} error={this.state.error}/> + </Modal.Body> + <Modal.Footer className="dialog-footer"> + <ButtonField variant="red" bordered onClick={this.close} name="cancel"/> + <ButtonField variant="green" bordered onClick={this.setComment} name="save"/> + </Modal.Footer> + </Modal> + ); + } +} + +export default CommentDialog; diff --git a/frontend/src/components/objects/MessageAction.js b/frontend/src/components/dialogs/CopyDialog.js index e0c96e8..069fd2e 100644 --- a/frontend/src/components/objects/MessageAction.js +++ b/frontend/src/components/dialogs/CopyDialog.js @@ -19,51 +19,51 @@ import React, {Component} from "react"; import {Modal} from "react-bootstrap"; import ButtonField from "../fields/ButtonField"; import TextField from "../fields/TextField"; -import "./MessageAction.scss"; -class MessageAction extends Component { +class CopyDialog extends Component { + + state = { + copyButtonText: "copy" + }; constructor(props) { super(props); - this.state = { - copyButtonText: "copy" - }; - this.actionValue = React.createRef(); - this.copyActionValue = this.copyActionValue.bind(this); + this.textbox = React.createRef(); } - copyActionValue() { - this.actionValue.current.select(); + copyActionValue = () => { + this.textbox.current.select(); document.execCommand("copy"); this.setState({copyButtonText: "copied!"}); - setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + this.timeoutHandle = setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + }; + + componentWillUnmount() { + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + } } render() { return ( - <Modal - {...this.props} - show={true} - size="lg" - aria-labelledby="message-action-dialog" - centered - > + <Modal show={true} size="lg" aria-labelledby="message-action-dialog" centered> <Modal.Header> <Modal.Title id="message-action-dialog"> - {this.props.actionName} + {this.props.name} </Modal.Title> </Modal.Header> <Modal.Body> - <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15}/> + <TextField readonly={this.props.readonly} value={this.props.value} textRef={this.textbox} + rows={15}/> </Modal.Body> <Modal.Footer className="dialog-footer"> + <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> <ButtonField variant="green" bordered onClick={this.copyActionValue} name={this.state.copyButtonText}/> - <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> </Modal.Footer> </Modal> ); } } -export default MessageAction; +export default CopyDialog; diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js index b70b7f7..29c747a 100644 --- a/frontend/src/components/objects/Connection.js +++ b/frontend/src/components/objects/Connection.js @@ -16,11 +16,12 @@ */ import React, {Component} from "react"; -import {Form} from "react-bootstrap"; import backend from "../../backend"; import dispatcher from "../../dispatcher"; import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; +import CommentDialog from "../dialogs/CommentDialog"; import ButtonField from "../fields/ButtonField"; +import TextField from "../fields/TextField"; import "./Connection.scss"; import CopyLinkPopover from "./CopyLinkPopover"; import LinkPopover from "./LinkPopover"; @@ -33,15 +34,7 @@ class Connection extends Component { update: false }; - handleAction = (name) => { - if (name === "hide") { - const enabled = !this.props.data.hidden; - backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`) - .then((_) => { - this.props.onEnabled(!enabled); - this.setState({update: true}); - }); - } + handleAction = (name, comment) => { if (name === "mark") { const marked = this.props.data.marked; backend.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`) @@ -49,6 +42,9 @@ class Connection extends Component { this.props.onMarked(!marked); this.setState({update: true}); }); + } else if (name === "comment") { + this.props.onCommented(comment); + this.setState({showCommentDialog: false}); } }; @@ -70,9 +66,9 @@ class Connection extends Component { <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span> </div>; - const commentPopoverContent = <div> - <span>Click to <strong>{conn.comment.length > 0 ? "edit" : "add"}</strong> comment</span> - {conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>} + const commentPopoverContent = <div style={{"width": "250px"}}> + <span>Click to <strong>{conn.comment ? "edit" : "add"}</strong> comment</span> + {conn.comment && <TextField rows={3} value={conn.comment} readonly/>} </div>; return ( @@ -100,10 +96,16 @@ class Connection extends Component { onClick={() => this.handleAction("mark")}>!!</span>} content={<span>Mark this connection</span>} placement="right"/> <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.comment})} - onClick={() => this.handleAction("comment")}>@</span>} + onClick={() => this.setState({showCommentDialog: true})}>@</span>} content={commentPopoverContent} placement="right"/> <CopyLinkPopover text="#" value={conn.id} textClassName={classNames("connection-icon", {"icon-enabled": conn.hidden})}/> + { + this.state.showCommentDialog && + <CommentDialog onSave={(comment) => this.handleAction("comment", comment)} + initialComment={conn.comment} connectionId={conn.id}/> + } + </td> </tr> ); diff --git a/frontend/src/components/objects/MessageAction.scss b/frontend/src/components/objects/MessageAction.scss deleted file mode 100644 index 996007b..0000000 --- a/frontend/src/components/objects/MessageAction.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import "../../colors"; - -.message-action-value { - font-size: 13px; - padding: 15px; - color: $color-primary-4; - background-color: $color-primary-2; -} diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js index 3bf8065..6058edc 100644 --- a/frontend/src/components/pages/MainPage.js +++ b/frontend/src/components/pages/MainPage.js @@ -21,6 +21,7 @@ import "react-reflex/styles.css" import {Route, Switch} from "react-router-dom"; import Filters from "../dialogs/Filters"; import Header from "../Header"; +import ConfigPane from "../panels/ConfigPane"; import Connections from "../panels/ConnectionsPane"; import MainPane from "../panels/MainPane"; import PcapsPane from "../panels/PcapsPane"; @@ -75,6 +76,7 @@ class MainPage extends Component { <Route path="/pcaps" children={<PcapsPane/>}/> <Route path="/rules" children={<RulesPane/>}/> <Route path="/services" children={<ServicesPane/>}/> + <Route path="/config" children={<ConfigPane/>}/> <Route exact path="/connections/:id" children={<StreamsPane connection={this.state.selectedConnection}/>}/> <Route children={<MainPane version={this.props.version}/>}/> diff --git a/frontend/src/components/panels/ConfigPane.js b/frontend/src/components/panels/ConfigPane.js new file mode 100644 index 0000000..9710abd --- /dev/null +++ b/frontend/src/components/panels/ConfigPane.js @@ -0,0 +1,39 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import "./common.scss"; + +class ConfigPane extends Component { + + state = {}; + + render() { + return ( + <div className="pane-container"> + <div className="main-pane"> + <div className="pane-section"> + Not implemented + </div> + </div> + </div> + ); + } + +} + +export default ConfigPane; diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index 6418b3e..708fc86 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -287,7 +287,7 @@ class ConnectionsPane extends Component { return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)} selected={this.state.selected === c.id} onMarked={(marked) => c.marked = marked} - onEnabled={(enabled) => c.hidden = !enabled} + onCommented={(comment) => c.comment = comment} services={this.state.services}/>, c.matched_rules.length > 0 && <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules} diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js index 4c16cf1..29aff71 100644 --- a/frontend/src/components/panels/StreamsPane.js +++ b/frontend/src/components/panels/StreamsPane.js @@ -25,7 +25,7 @@ import rules from "../../model/rules"; import {downloadBlob, getHeaderValue} from "../../utils"; import ButtonField from "../fields/ButtonField"; import ChoiceField from "../fields/ChoiceField"; -import MessageAction from "../objects/MessageAction"; +import CopyDialog from "../dialogs/CopyDialog"; import "./StreamsPane.scss"; const reactStringReplace = require("react-string-replace"); @@ -105,10 +105,11 @@ class StreamsPane extends Component { if (contentType && contentType.includes("application/json")) { try { const json = JSON.parse(m.body); - body = <ReactJson src={json} theme="grayscale" collapsed={false} displayDataTypes={false}/>; + if (typeof json === "object") { + body = <ReactJson src={json} theme="grayscale" collapsed={false} displayDataTypes={false}/>; + } } catch (e) { log.error(e); - body = m.body; } } @@ -157,11 +158,11 @@ class StreamsPane extends Component { if (!connectionMessage.metadata["reproducers"]) { return; } - return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => - <ButtonField small key={actionName + "_button"} name={actionName} onClick={() => { + return Object.entries(connectionMessage.metadata["reproducers"]).map(([name, value]) => + <ButtonField small key={name + "_button"} name={name} onClick={() => { this.setState({ - messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue} - onHide={() => this.setState({messageActionDialog: null})}/> + messageActionDialog: <CopyDialog actionName={name} value={value} + onHide={() => this.setState({messageActionDialog: null})}/> }); }}/> ); |