From 2e6af3b2623da3d002816a6de325133d626858c9 Mon Sep 17 00:00:00 2001 From: VaiTon Date: Mon, 5 Jun 2023 16:54:12 +0200 Subject: Frontend to jsx --- frontend/src/components/App.jsx | 70 +++ frontend/src/components/Header.jsx | 99 +++++ frontend/src/components/Notifications.jsx | 147 +++++++ frontend/src/components/Timeline.jsx | 405 ++++++++++++++++++ frontend/src/components/dialogs/CommentDialog.jsx | 70 +++ frontend/src/components/dialogs/CopyDialog.jsx | 69 +++ frontend/src/components/dialogs/Filters.jsx | 85 ++++ frontend/src/components/fields/ButtonField.jsx | 78 ++++ frontend/src/components/fields/CheckField.jsx | 67 +++ frontend/src/components/fields/ChoiceField.jsx | 85 ++++ frontend/src/components/fields/InputField.jsx | 95 +++++ frontend/src/components/fields/TagField.jsx | 75 ++++ frontend/src/components/fields/TextField.jsx | 60 +++ .../components/fields/extensions/ColorField.jsx | 98 +++++ .../components/fields/extensions/NumericField.jsx | 62 +++ .../src/components/filters/AdvancedFilters.jsx | 54 +++ .../filters/BooleanConnectionsFilter.jsx | 69 +++ .../src/components/filters/ExitSearchFilter.jsx | 57 +++ .../components/filters/RulesConnectionsFilter.jsx | 83 ++++ .../components/filters/StringConnectionsFilter.jsx | 127 ++++++ frontend/src/components/objects/Connection.jsx | 116 +++++ .../components/objects/ConnectionMatchedRules.jsx | 51 +++ .../src/components/objects/CopyLinkPopover.jsx | 54 +++ frontend/src/components/objects/LinkPopover.jsx | 51 +++ .../src/components/pages/ConfigurationPage.jsx | 183 ++++++++ frontend/src/components/pages/MainPage.jsx | 97 +++++ .../components/pages/ServiceUnavailablePage.jsx | 34 ++ frontend/src/components/panels/ConnectionsPane.jsx | 310 ++++++++++++++ frontend/src/components/panels/MainPane.jsx | 112 +++++ frontend/src/components/panels/PcapsPane.jsx | 287 +++++++++++++ frontend/src/components/panels/RulesPane.jsx | 469 +++++++++++++++++++++ frontend/src/components/panels/SearchPane.jsx | 309 ++++++++++++++ frontend/src/components/panels/ServicesPane.jsx | 233 ++++++++++ frontend/src/components/panels/StatsPane.jsx | 274 ++++++++++++ frontend/src/components/panels/StreamsPane.jsx | 453 ++++++++++++++++++++ frontend/src/index.jsx | 32 ++ 36 files changed, 5020 insertions(+) create mode 100644 frontend/src/components/App.jsx create mode 100644 frontend/src/components/Header.jsx create mode 100644 frontend/src/components/Notifications.jsx create mode 100644 frontend/src/components/Timeline.jsx create mode 100644 frontend/src/components/dialogs/CommentDialog.jsx create mode 100644 frontend/src/components/dialogs/CopyDialog.jsx create mode 100644 frontend/src/components/dialogs/Filters.jsx create mode 100644 frontend/src/components/fields/ButtonField.jsx create mode 100644 frontend/src/components/fields/CheckField.jsx create mode 100644 frontend/src/components/fields/ChoiceField.jsx create mode 100644 frontend/src/components/fields/InputField.jsx create mode 100644 frontend/src/components/fields/TagField.jsx create mode 100644 frontend/src/components/fields/TextField.jsx create mode 100644 frontend/src/components/fields/extensions/ColorField.jsx create mode 100644 frontend/src/components/fields/extensions/NumericField.jsx create mode 100644 frontend/src/components/filters/AdvancedFilters.jsx create mode 100644 frontend/src/components/filters/BooleanConnectionsFilter.jsx create mode 100644 frontend/src/components/filters/ExitSearchFilter.jsx create mode 100644 frontend/src/components/filters/RulesConnectionsFilter.jsx create mode 100644 frontend/src/components/filters/StringConnectionsFilter.jsx create mode 100644 frontend/src/components/objects/Connection.jsx create mode 100644 frontend/src/components/objects/ConnectionMatchedRules.jsx create mode 100644 frontend/src/components/objects/CopyLinkPopover.jsx create mode 100644 frontend/src/components/objects/LinkPopover.jsx create mode 100644 frontend/src/components/pages/ConfigurationPage.jsx create mode 100644 frontend/src/components/pages/MainPage.jsx create mode 100644 frontend/src/components/pages/ServiceUnavailablePage.jsx create mode 100644 frontend/src/components/panels/ConnectionsPane.jsx create mode 100644 frontend/src/components/panels/MainPane.jsx create mode 100644 frontend/src/components/panels/PcapsPane.jsx create mode 100644 frontend/src/components/panels/RulesPane.jsx create mode 100644 frontend/src/components/panels/SearchPane.jsx create mode 100644 frontend/src/components/panels/ServicesPane.jsx create mode 100644 frontend/src/components/panels/StatsPane.jsx create mode 100644 frontend/src/components/panels/StreamsPane.jsx create mode 100644 frontend/src/index.jsx diff --git a/frontend/src/components/App.jsx b/frontend/src/components/App.jsx new file mode 100644 index 0000000..96083cd --- /dev/null +++ b/frontend/src/components/App.jsx @@ -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 . + */ + +import React, {Component} from "react"; +import {BrowserRouter as Router} from "react-router-dom"; +import dispatcher from "../dispatcher"; +import Notifications from "./Notifications"; +import ConfigurationPage from "./pages/ConfigurationPage"; +import MainPage from "./pages/MainPage"; +import ServiceUnavailablePage from "./pages/ServiceUnavailablePage"; + +class App extends Component { + + state = {}; + + componentDidMount() { + dispatcher.register("notifications", this.handleNotifications); + + setInterval(() => { + if (document.title.endsWith("❚")) { + document.title = document.title.slice(0, -1); + } else { + document.title += "❚"; + } + }, 500); + } + + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (payload) => { + if (payload.event === "connected") { + this.setState({ + connected: true, + configured: payload.message["is_configured"], + version: payload.message["version"] + }); + } + }; + + render() { + return ( + + + {this.state.connected ? + (this.state.configured ? : + this.setState({configured: true})}/>) : + + } + + ); + } +} + +export default App; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..4695bd9 --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,99 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {Link, withRouter} from "react-router-dom"; +import Typed from "typed.js"; +import {cleanNumber, validatePort} from "../utils"; +import ButtonField from "./fields/ButtonField"; +import AdvancedFilters from "./filters/AdvancedFilters"; +import BooleanConnectionsFilter from "./filters/BooleanConnectionsFilter"; +import ExitSearchFilter from "./filters/ExitSearchFilter"; +import RulesConnectionsFilter from "./filters/RulesConnectionsFilter"; +import StringConnectionsFilter from "./filters/StringConnectionsFilter"; +import "./Header.scss"; + +import classNames from 'classnames'; + +class Header extends Component { + + componentDidMount() { + const options = { + strings: ["caronte$ "], + typeSpeed: 50, + cursorChar: "❚" + }; + this.typed = new Typed(this.el, options); + } + + componentWillUnmount() { + this.typed.destroy(); + } + + render() { + return ( +
+
+

+ + { + this.el = el; + }}/> + +

+ + {this.props.configured && +
+ + + + + +
+ } + + {this.props.configured && +
+ + + + + + + + + + + + + + + +
+ } +
+
+ ); + } +} + +export default withRouter(Header); diff --git a/frontend/src/components/Notifications.jsx b/frontend/src/components/Notifications.jsx new file mode 100644 index 0000000..5afdb7b --- /dev/null +++ b/frontend/src/components/Notifications.jsx @@ -0,0 +1,147 @@ +/* + * 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 . + */ + +import React, { Component } from "react"; +import dispatcher from "../dispatcher"; +import { randomClassName } from "../utils"; +import "./Notifications.scss"; +import _ from "lodash"; +import classNames from "classnames"; + +class Notifications extends Component { + state = { + notifications: [], + closedNotifications: [], + }; + + componentDidMount() { + dispatcher.register("notifications", this.handleNotifications); + } + + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (n) => this.notificationHandler(n); + + notificationHandler = (n) => { + switch (n.event) { + case "connected": + n.title = "connected"; + n.description = `number of active clients: ${n.message["connected_clients"]}`; + return this.pushNotification(n); + case "services.edit": + n.title = "services updated"; + n.description = `updated "${n.message["name"]}" on port ${n.message["port"]}`; + n.variant = "blue"; + return this.pushNotification(n); + case "rules.new": + n.title = "rules updated"; + n.description = `new rule added: ${n.message["name"]}`; + n.variant = "green"; + return this.pushNotification(n); + case "rules.edit": + n.title = "rules updated"; + n.description = `existing rule updated: ${n.message["name"]}`; + n.variant = "blue"; + return this.pushNotification(n); + case "pcap.completed": + n.title = "new pcap analyzed"; + 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; + } + }; + + pushNotification = (notification) => { + const notifications = this.state.notifications; + notification.id = randomClassName(); + notifications.push(notification); + this.setState({ notifications }); + setTimeout(() => { + const notifications = this.state.notifications; + notification.open = true; + this.setState({ notifications }); + }, 100); + + const hideHandle = setTimeout(() => { + const notifications = _.without(this.state.notifications, notification); + const closedNotifications = this.state.closedNotifications.concat([ + notification, + ]); + notification.closed = true; + this.setState({ notifications, closedNotifications }); + }, 5000); + + const removeHandle = setTimeout(() => { + const closedNotifications = _.without( + this.state.closedNotifications, + notification + ); + this.setState({ closedNotifications }); + }, 6000); + + notification.onClick = () => { + clearTimeout(hideHandle); + clearTimeout(removeHandle); + const notifications = _.without(this.state.notifications, notification); + this.setState({ notifications }); + }; + }; + + render() { + return ( +
+
+ {this.state.closedNotifications + .concat(this.state.notifications) + .map((n) => { + const notificationClassnames = { + notification: true, + "notification-closed": n.closed, + "notification-open": n.open, + }; + if (n.variant) { + notificationClassnames[`notification-${n.variant}`] = true; + } + return ( +
+

{n.title}

+
+                    {n.description}
+                  
+
+ ); + })} +
+
+ ); + } +} + +export default Notifications; diff --git a/frontend/src/components/Timeline.jsx b/frontend/src/components/Timeline.jsx new file mode 100644 index 0000000..faaa8de --- /dev/null +++ b/frontend/src/components/Timeline.jsx @@ -0,0 +1,405 @@ +/* + * 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 . + */ + +import React, { Component } from "react"; +import { withRouter } from "react-router-dom"; + +import ChartContainer from "react-timeseries-charts/lib/components/ChartContainer"; +import ChartRow from "react-timeseries-charts/lib/components/ChartRow"; +import Charts from "react-timeseries-charts/lib/components/Charts"; +import LineChart from "react-timeseries-charts/lib/components/LineChart"; +import MultiBrush from "react-timeseries-charts/lib/components/MultiBrush"; +import Resizable from "react-timeseries-charts/lib/components/Resizable"; +import YAxis from "react-timeseries-charts/lib/components/YAxis"; +import { TimeRange, TimeSeries } from "pondjs"; +import styler from "react-timeseries-charts/lib/js/styler"; + +import backend from "../backend"; +import dispatcher from "../dispatcher"; +import log from "../log"; +import ChoiceField from "./fields/ChoiceField"; +import "./Timeline.scss"; + +const minutes = 60 * 1000; +const maxTimelineRange = 24 * 60 * minutes; +import classNames from "classnames"; + +const leftSelectionPaddingMultiplier = 24; +const rightSelectionPaddingMultiplier = 8; + +class Timeline extends Component { + state = { + metric: "connections_per_service", + }; + + constructor() { + super(); + + this.disableTimeSeriesChanges = false; + this.selectionTimeout = null; + } + + componentDidMount() { + const urlParams = new URLSearchParams(this.props.location.search); + this.setState({ + servicePortFilter: urlParams.get("service_port") || null, + matchedRulesFilter: urlParams.getAll("matched_rules") || null, + }); + + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics loaded after mount") + ); + dispatcher.register( + "connections_filters", + this.handleConnectionsFiltersCallback + ); + dispatcher.register("connection_updates", this.handleConnectionUpdates); + dispatcher.register("notifications", this.handleNotifications); + dispatcher.register("pulse_timeline", this.handlePulseTimeline); + } + + componentWillUnmount() { + dispatcher.unregister(this.handleConnectionsFiltersCallback); + dispatcher.unregister(this.handleConnectionUpdates); + dispatcher.unregister(this.handleNotifications); + dispatcher.unregister(this.handlePulseTimeline); + } + + loadStatistics = async (metric) => { + const urlParams = new URLSearchParams(); + urlParams.set("metric", metric); + + let columns = []; + if (metric === "matched_rules") { + let rules = await this.loadRules(); + if (this.state.matchedRulesFilter.length > 0) { + this.state.matchedRulesFilter.forEach((id) => { + urlParams.append("rules_ids", id); + }); + columns = this.state.matchedRulesFilter; + } else { + columns = rules.map((r) => r.id); + } + } else { + let services = await this.loadServices(); + const filteredPort = this.state.servicePortFilter; + if (filteredPort && services[filteredPort]) { + const service = services[filteredPort]; + services = {}; + services[filteredPort] = service; + } + + columns = Object.keys(services); + columns.forEach((port) => urlParams.append("ports", port)); + } + + const metrics = (await backend.get("/api/statistics?" + urlParams)).json; + if (metrics.length === 0) { + return; + } + + const zeroFilledMetrics = []; + const toTime = (m) => new Date(m["range_start"]).getTime(); + + 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"]); + zeroFilledMetrics.push(m); + } else { + const m = {}; + m["range_start"] = new Date(interval); + m[metric] = {}; + columns.forEach((c) => (m[metric][c] = 0)); + zeroFilledMetrics.push(m); + } + } + + const series = new TimeSeries({ + name: "statistics", + columns: ["time"].concat(columns), + points: zeroFilledMetrics.map((m) => + [m["range_start"]].concat( + columns.map((c) => + metric in m && m[metric] != null ? m[metric][c] || 0 : 0 + ) + ) + ), + }); + + const start = series.range().begin(); + const end = series.range().end(); + + this.setState({ + metric, + series, + timeRange: new TimeRange(start, end), + columns, + start, + end, + }); + }; + + loadServices = async () => { + const services = (await backend.get("/api/services")).json; + this.setState({ services }); + return services; + }; + + loadRules = async () => { + const rules = (await backend.get("/api/rules")).json; + this.setState({ rules }); + return rules; + }; + + createStyler = () => { + if (this.state.metric === "matched_rules") { + return styler( + this.state.rules.map((rule) => { + return { key: rule.id, color: rule.color, width: 2 }; + }) + ); + } else { + return styler( + Object.keys(this.state.services).map((port) => { + return { + key: port, + color: this.state.services[port].color, + width: 2, + }; + }) + ); + } + }; + + handleTimeRangeChange = (timeRange) => { + if (!this.disableTimeSeriesChanges) { + this.setState({ timeRange }); + } + }; + + handleSelectionChange = (timeRange) => { + this.disableTimeSeriesChanges = true; + + this.setState({ selection: timeRange }); + if (this.selectionTimeout) { + clearTimeout(this.selectionTimeout); + } + this.selectionTimeout = setTimeout(() => { + dispatcher.dispatch("timeline_updates", { + from: timeRange.begin(), + to: timeRange.end(), + }); + this.selectionTimeout = null; + this.disableTimeSeriesChanges = false; + }, 1000); + }; + + handleConnectionsFiltersCallback = (payload) => { + if ( + "service_port" in payload && + this.state.servicePortFilter !== payload["service_port"] + ) { + this.setState({ servicePortFilter: payload["service_port"] }); + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics reloaded after service port changed") + ); + } + if ( + "matched_rules" in payload && + this.state.matchedRulesFilter !== payload["matched_rules"] + ) { + this.setState({ matchedRulesFilter: payload["matched_rules"] }); + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics reloaded after matched rules changed") + ); + } + }; + + handleConnectionUpdates = (payload) => { + 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) => { + if ( + payload.event === "services.edit" && + this.state.metric !== "matched_rules" + ) { + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics reloaded after services updates") + ); + } else if ( + payload.event.startsWith("rules") && + this.state.metric === "matched_rules" + ) { + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics reloaded after rules updates") + ); + } else if (payload.event === "pcap.completed") { + this.loadStatistics(this.state.metric).then(() => + log.debug("Statistics reloaded after pcap processed") + ); + } + }; + + handlePulseTimeline = (payload) => { + this.setState({ pulseTimeline: true }); + setTimeout(() => this.setState({ pulseTimeline: false }), payload.duration); + }; + + adjustSelection = () => { + const seriesRange = this.state.series.range(); + const selection = this.state.selection; + const delta = selection.end() - selection.begin(); + const start = Math.max( + selection.begin().getTime() - delta * leftSelectionPaddingMultiplier, + seriesRange.begin().getTime() + ); + const end = Math.min( + selection.end().getTime() + delta * rightSelectionPaddingMultiplier, + seriesRange.end().getTime() + ); + this.setState({ timeRange: new TimeRange(start, end) }); + }; + + aggregateSeries = (func) => { + const values = this.state.series + .columns() + .map((c) => this.state.series[func](c)); + return Math[func](...values); + }; + + render() { + if (!this.state.series) { + return null; + } + + return ( +
+
+ + + + + + + + + + + + + +
+ + this.loadStatistics(metric).then(() => + log.debug("Statistics loaded after metric changes") + ) + } + value={this.state.metric} + /> +
+
+
+ ); + } +} + +export default withRouter(Timeline); diff --git a/frontend/src/components/dialogs/CommentDialog.jsx b/frontend/src/components/dialogs/CommentDialog.jsx new file mode 100644 index 0000000..970aa83 --- /dev/null +++ b/frontend/src/components/dialogs/CommentDialog.jsx @@ -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 . + */ + +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 ( + + + + ~/.comment + + + + this.setState({comment})} + rows={7} error={this.state.error}/> + + + + + + + ); + } +} + +export default CommentDialog; diff --git a/frontend/src/components/dialogs/CopyDialog.jsx b/frontend/src/components/dialogs/CopyDialog.jsx new file mode 100644 index 0000000..069fd2e --- /dev/null +++ b/frontend/src/components/dialogs/CopyDialog.jsx @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {Modal} from "react-bootstrap"; +import ButtonField from "../fields/ButtonField"; +import TextField from "../fields/TextField"; + +class CopyDialog extends Component { + + state = { + copyButtonText: "copy" + }; + + constructor(props) { + super(props); + this.textbox = React.createRef(); + } + + copyActionValue = () => { + this.textbox.current.select(); + document.execCommand("copy"); + this.setState({copyButtonText: "copied!"}); + this.timeoutHandle = setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); + }; + + componentWillUnmount() { + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + } + } + + render() { + return ( + + + + {this.props.name} + + + + + + + + + + + ); + } +} + +export default CopyDialog; diff --git a/frontend/src/components/dialogs/Filters.jsx b/frontend/src/components/dialogs/Filters.jsx new file mode 100644 index 0000000..a2407df --- /dev/null +++ b/frontend/src/components/dialogs/Filters.jsx @@ -0,0 +1,85 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {Modal} from "react-bootstrap"; +import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils"; +import ButtonField from "../fields/ButtonField"; +import StringConnectionsFilter from "../filters/StringConnectionsFilter"; +import "./Filters.scss"; + +class Filters extends Component { + + render() { + return ( + + + + ~/advanced_filters + + + +
+
+ + + +
+ +
+ + + +
+
+
+ + + +
+ ); + } +} + +export default Filters; diff --git a/frontend/src/components/fields/ButtonField.jsx b/frontend/src/components/fields/ButtonField.jsx new file mode 100644 index 0000000..ae5b33a --- /dev/null +++ b/frontend/src/components/fields/ButtonField.jsx @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +import React, { Component } from "react"; +import "./ButtonField.scss"; +import "./common.scss"; +import classNames from "classnames"; + +class ButtonField extends Component { + 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; + } + if (this.props.fullSpan) { + buttonStyle["width"] = "100%"; + } + if (this.props.rounded) { + buttonStyle["borderRadius"] = "3px"; + } + if (this.props.inline) { + buttonStyle["marginTop"] = "8px"; + } + + return ( +
+ +
+ ); + } +} + +export default ButtonField; diff --git a/frontend/src/components/fields/CheckField.jsx b/frontend/src/components/fields/CheckField.jsx new file mode 100644 index 0000000..1a04f18 --- /dev/null +++ b/frontend/src/components/fields/CheckField.jsx @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +import React, { Component } from "react"; +import { randomClassName } from "../../utils"; +import "./CheckField.scss"; +import "./common.scss"; + +import classNames from "classnames"; + +class CheckField extends Component { + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const checked = this.props.checked || false; + const small = this.props.small || false; + const name = this.props.name || null; + const handler = () => { + if (!this.props.readonly && this.props.onChange) { + this.props.onChange(!checked); + } + }; + + return ( +
+
+ + +
+
+ ); + } +} + +export default CheckField; diff --git a/frontend/src/components/fields/ChoiceField.jsx b/frontend/src/components/fields/ChoiceField.jsx new file mode 100644 index 0000000..c44d0a9 --- /dev/null +++ b/frontend/src/components/fields/ChoiceField.jsx @@ -0,0 +1,85 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./ChoiceField.scss"; +import "./common.scss"; + +import classNames from '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) => + handler(key)}>{values[i]} + ); + + let fieldValue = ""; + if (inline && name) { + fieldValue = name; + } + if (!this.props.onlyName && inline && name) { + fieldValue += ": "; + } + if (!this.props.onlyName) { + fieldValue += this.props.value || "select a value"; + } + + return ( +
+ {!inline && name && } +
this.state.expanded ? collapse() : expand()}> +
{fieldValue}
+
+ {options} +
+
+
+ ); + } +} + +export default ChoiceField; diff --git a/frontend/src/components/fields/InputField.jsx b/frontend/src/components/fields/InputField.jsx new file mode 100644 index 0000000..f9609df --- /dev/null +++ b/frontend/src/components/fields/InputField.jsx @@ -0,0 +1,95 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./InputField.scss"; + +import classNames from 'classnames'; + +class InputField extends Component { + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const active = this.props.active || false; + const invalid = this.props.invalid || false; + const small = this.props.small || false; + const inline = this.props.inline || false; + const name = this.props.name || null; + const value = this.props.value || ""; + const defaultValue = this.props.defaultValue || ""; + const type = this.props.type || "text"; + const error = this.props.error || null; + + const handler = (e) => { + if (typeof this.props.onChange === "function") { + if (type === "file") { + let file = e.target.files[0]; + this.props.onChange(file); + } else if (e == null) { + this.props.onChange(defaultValue); + } else { + this.props.onChange(e.target.value); + } + } + }; + let inputProps = {}; + if (type !== "file") { + inputProps["value"] = value || defaultValue; + } + + return ( +
+
+ {name && +
+ +
+ } +
+
+ {type === "file" && } + +
+ {type !== "file" && value !== "" && !this.props.readonly && +
+ handler(null)}>del +
+ } +
+
+ {error && +
+ error: {error} +
+ } +
+ ); + } +} + +export default InputField; diff --git a/frontend/src/components/fields/TagField.jsx b/frontend/src/components/fields/TagField.jsx new file mode 100644 index 0000000..79e2314 --- /dev/null +++ b/frontend/src/components/fields/TagField.jsx @@ -0,0 +1,75 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import ReactTags from "react-tag-autocomplete"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./TagField.scss"; + +import classNames from 'classnames'; +import _ from 'lodash'; + +class TagField extends Component { + + state = {}; + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + onAddition = (tag) => { + if (typeof this.props.onChange === "function") { + this.props.onChange([].concat(this.props.tags, tag), true, tag); // true == addition + } + }; + + onDelete = (i) => { + if (typeof this.props.onChange === "function") { + const tags = _.clone(this.props.tags); + const tag = tags[i]; + tags.splice(i, 1); + this.props.onChange(tags, true, tag); // false == delete + } + }; + + + render() { + const small = this.props.small || false; + const name = this.props.name || null; + + return ( +
+ {name && +
+ +
+ } +
+ +
+
+ ); + } +} + +export default TagField; diff --git a/frontend/src/components/fields/TextField.jsx b/frontend/src/components/fields/TextField.jsx new file mode 100644 index 0000000..1138ffc --- /dev/null +++ b/frontend/src/components/fields/TextField.jsx @@ -0,0 +1,60 @@ +/* + * 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 . + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./TextField.scss"; + +import classNames from 'classnames'; + +class TextField extends Component { + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const name = this.props.name || null; + const error = this.props.error || null; + const rows = this.props.rows || 3; + + const handler = (e) => { + if (this.props.onChange) { + if (e == null) { + this.props.onChange(""); + } else { + this.props.onChange(e.target.value); + } + } + }; + + return ( +
+ {name && } +