diff options
Diffstat (limited to 'frontend/src/components')
21 files changed, 265 insertions, 119 deletions
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js index bf959c5..0f700db 100644 --- a/frontend/src/components/App.js +++ b/frontend/src/components/App.js @@ -31,7 +31,8 @@ class App extends Component { if (payload.event === "connected") { this.setState({ connected: true, - configured: payload.message["is_configured"] + configured: payload.message["is_configured"], + version: payload.message["version"] }); } }); @@ -50,7 +51,7 @@ class App extends Component { <> <Notifications/> {this.state.connected ? - (this.state.configured ? <MainPage/> : + (this.state.configured ? <MainPage version={this.state.version}/> : <ConfigurationPage onConfigured={() => this.setState({configured: true})}/>) : <ServiceUnavailablePage/> } diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 1017a42..ad681a2 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -30,49 +30,84 @@ class Notifications extends Component { }; componentDidMount() { - dispatcher.register("notifications", notification => { + dispatcher.register("notifications", 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); + default: + return; + } + }; + + pushNotification = (notification) => { + const notifications = this.state.notifications; + notifications.push(notification); + this.setState({notifications}); + setTimeout(() => { const notifications = this.state.notifications; - notifications.push(notification); + notification.open = true; this.setState({notifications}); - setTimeout(() => { - const notifications = this.state.notifications; - notification.open = true; - this.setState({notifications}); - }, 100); + }, 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 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); + 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}); - }; - }); - } + notification.onClick = () => { + clearTimeout(hideHandle); + clearTimeout(removeHandle); + const notifications = _.without(this.state.notifications, notification); + this.setState({notifications}); + }; + }; render() { return ( <div className="notifications"> <div className="notifications-list"> { - this.state.closedNotifications.concat(this.state.notifications).map(n => - <div className={classNames("notification", {"notification-closed": n.closed}, - {"notification-open": n.open})} onClick={n.onClick}> - <h3 className="notification-title">{n.event}</h3> - <span className="notification-description">{JSON.stringify(n.message)}</span> - </div> - ) + 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 <div className={classNames(notificationClassnames)} onClick={n.onClick}> + <h3 className="notification-title">{n.title}</h3> + <pre className="notification-description">{n.description}</pre> + </div>; + }) } </div> </div> diff --git a/frontend/src/components/Notifications.scss b/frontend/src/components/Notifications.scss index 324d0bb..98d228e 100644 --- a/frontend/src/components/Notifications.scss +++ b/frontend/src/components/Notifications.scss @@ -7,18 +7,15 @@ left: 30px; .notification { - overflow: hidden; width: 250px; margin: 10px 0; padding: 10px; + cursor: pointer; transition: all 1s ease; transform: translateX(-300px); - white-space: nowrap; - text-overflow: ellipsis; color: $color-green-light; border-left: 5px solid $color-green-dark; background-color: $color-green; - cursor: pointer; .notification-title { font-size: 0.9em; @@ -27,6 +24,11 @@ .notification-description { font-size: 0.8em; + overflow: hidden; + margin: 10px 0; + white-space: nowrap; + text-overflow: ellipsis; + color: $color-primary-4; } &.notification-open { @@ -37,5 +39,11 @@ transform: translateY(-50px); opacity: 0; } + + &.notification-blue { + color: $color-blue-light; + border-left: 5px solid $color-blue-dark; + background-color: $color-blue; + } } } diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js index 7be42e0..615203f 100644 --- a/frontend/src/components/Timeline.js +++ b/frontend/src/components/Timeline.js @@ -35,6 +35,7 @@ import log from "../log"; import dispatcher from "../dispatcher"; const minutes = 60 * 1000; +const classNames = require('classnames'); class Timeline extends Component { @@ -70,6 +71,11 @@ class Timeline extends Component { this.loadServices().then(() => log.debug("Services reloaded after notification update")); } }); + + dispatcher.register("pulse_timeline", payload => { + this.setState({pulseTimeline: true}); + setTimeout(() => this.setState({pulseTimeline: false}), payload.duration); + }); } componentDidUpdate(prevProps, prevState, snapshot) { @@ -183,7 +189,7 @@ class Timeline extends Component { return ( <footer className="footer"> - <div className="time-line"> + <div className={classNames("time-line", {"pulse-timeline": this.state.pulseTimeline})}> <Resizable> <ChartContainer timeRange={this.state.timeRange} enableDragZoom={false} paddingTop={5} minDuration={60000} diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss index eeb9d50..db8d9c8 100644 --- a/frontend/src/components/Timeline.scss +++ b/frontend/src/components/Timeline.scss @@ -13,6 +13,10 @@ top: 5px; right: 10px; } + + &.pulse-timeline { + animation: pulse 2s infinite; + } } svg text { diff --git a/frontend/src/components/dialogs/Filters.js b/frontend/src/components/dialogs/Filters.js index 35c11df..dfd554b 100644 --- a/frontend/src/components/dialogs/Filters.js +++ b/frontend/src/components/dialogs/Filters.js @@ -81,7 +81,7 @@ class Filters extends Component { </thead> <tbody> {this.generateRows(["service_port", "client_address", "min_duration", - "min_bytes", "started_after", "closed_after", "marked"])} + "min_bytes"])} </tbody> </Table> </Col> @@ -95,14 +95,11 @@ class Filters extends Component { </thead> <tbody> {this.generateRows(["matched_rules", "client_port", "max_duration", - "max_bytes", "started_before", "closed_before", "hidden"])} + "max_bytes", "marked"])} </tbody> </Table> </Col> - </Row> - - </Container> </Modal.Body> <Modal.Footer className="dialog-footer"> diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js index ffcceae..193339c 100644 --- a/frontend/src/components/fields/ButtonField.js +++ b/frontend/src/components/fields/ButtonField.js @@ -55,8 +55,8 @@ class ButtonField extends Component { } return ( - <div className={classNames( "field", "button-field", {"field-small": this.props.small})}> - <button type="button" className={classNames(classNames(buttonClassnames))} + <div className={classNames("field", "button-field", {"field-small": this.props.small})}> + <button type="button" className={classNames(buttonClassnames)} onClick={handler} style={buttonStyle}>{this.props.name}</button> </div> ); diff --git a/frontend/src/components/fields/TextField.scss b/frontend/src/components/fields/TextField.scss index c2d6ef5..5fde9e6 100644 --- a/frontend/src/components/fields/TextField.scss +++ b/frontend/src/components/fields/TextField.scss @@ -51,4 +51,8 @@ padding: 5px 10px; color: $color-secondary-0; } + + &:hover::-webkit-scrollbar-thumb { + background: $color-secondary-2 !important; + } } diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js index cde3cfb..9fb3b18 100644 --- a/frontend/src/components/filters/FiltersDefinitions.js +++ b/frontend/src/components/filters/FiltersDefinitions.js @@ -22,8 +22,7 @@ import RulesConnectionsFilter from "./RulesConnectionsFilter"; import BooleanConnectionsFilter from "./BooleanConnectionsFilter"; export const filtersNames = ["service_port", "matched_rules", "client_address", "client_port", - "min_duration", "max_duration", "min_bytes", "max_bytes", "started_after", - "started_before", "closed_after", "closed_before", "marked", "hidden"]; + "min_duration", "max_duration", "min_bytes", "max_bytes", "marked"]; export const filtersDefinitions = { service_port: <StringConnectionsFilter filterName="service_port" @@ -66,34 +65,9 @@ export const filtersDefinitions = { replaceFunc={cleanNumber} key="max_bytes_filter" width={200}/>, - // started_after: <StringConnectionsFilter filterName="started_after" - // defaultFilterValue="00:00:00" - // validateFunc={validate24HourTime} - // encodeFunc={timeToTimestamp} - // decodeFunc={timestampToTime} - // key="started_after_filter" - // width={230} />, - // started_before: <StringConnectionsFilter filterName="started_before" - // defaultFilterValue="00:00:00" - // validateFunc={validate24HourTime} - // encodeFunc={timeToTimestamp} - // decodeFunc={timestampToTime} - // key="started_before_filter" - // width={230} />, - // closed_after: <StringConnectionsFilter filterName="closed_after" - // defaultFilterValue="00:00:00" - // validateFunc={validate24HourTime} - // encodeFunc={timeToTimestamp} - // decodeFunc={timestampToTime} - // key="closed_after_filter" - // width={230} />, - // closed_before: <StringConnectionsFilter filterName="closed_before" - // defaultFilterValue="00:00:00" - // validateFunc={validate24HourTime} - // encodeFunc={timeToTimestamp} - // decodeFunc={timestampToTime} - // key="closed_before_filter" - // width={230} />, - marked: <BooleanConnectionsFilter filterName={"marked"}/>, - // hidden: <BooleanConnectionsFilter filterName={"hidden"} /> + contains_string: <StringConnectionsFilter filterName="contains_string" + defaultFilterValue="" + key="contains_string_filter" + width={320}/>, + marked: <BooleanConnectionsFilter filterName={"marked"}/> }; diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js index 5e2beba..e0e942a 100644 --- a/frontend/src/components/objects/Connection.js +++ b/frontend/src/components/objects/Connection.js @@ -17,11 +17,12 @@ import React, {Component} from 'react'; import './Connection.scss'; -import {Form, OverlayTrigger, Popover} from "react-bootstrap"; +import {Form} from "react-bootstrap"; import backend from "../../backend"; import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; import ButtonField from "../fields/ButtonField"; import LinkPopover from "./LinkPopover"; +import TextField from "../fields/TextField"; const classNames = require('classnames'); @@ -81,14 +82,6 @@ class Connection extends Component { <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span> </div>; - const popoverFor = function (name, content) { - return <Popover id={`popover-${name}-${conn.id}`} className="connection-popover"> - <Popover.Content> - {content} - </Popover.Content> - </Popover>; - }; - 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}/>} @@ -97,7 +90,7 @@ class Connection extends Component { const copyPopoverContent = <div> {this.state.copiedMessage ? <span><strong>Copied!</strong></span> : <span>Click to <strong>copy</strong> the connection id</span>} - <Form.Control as="textarea" readOnly={true} rows={1} defaultValue={conn.id} ref={this.copyTextarea}/> + <TextField readonly rows={1} value={conn.id} textRef={this.copyTextarea}/> </div>; return ( @@ -119,22 +112,16 @@ class Connection extends Component { <td className="clickable" onClick={this.props.onSelected}>{durationBetween(startedAt, closedAt)}</td> <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["client_bytes"])}</td> <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn["server_bytes"])}</td> - <td> - <OverlayTrigger trigger={["focus", "hover"]} placement="right" - overlay={popoverFor("hide", <span>Mark this connection</span>)}> - <span className={"connection-icon" + (conn.marked ? " icon-enabled" : "")} - onClick={() => this.handleAction("mark")}>!!</span> - </OverlayTrigger> - <OverlayTrigger trigger={["focus", "hover"]} placement="right" - overlay={popoverFor("comment", commentPopoverContent)}> - <span className={"connection-icon" + (conn.comment ? " icon-enabled" : "")} - onClick={() => this.handleAction("comment")}>@</span> - </OverlayTrigger> - <OverlayTrigger trigger={["focus", "hover"]} placement="right" - overlay={popoverFor("copy", copyPopoverContent)}> - <span className="connection-icon" - onClick={() => this.handleAction("copy")}>#</span> - </OverlayTrigger> + <td className="connection-actions"> + <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.marked})} + 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>} + content={commentPopoverContent} placement="right"/> + <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.hidden})} + onClick={() => this.handleAction("copy")}>#</span>} + content={copyPopoverContent} placement="right"/> </td> </tr> ); diff --git a/frontend/src/components/objects/Connection.scss b/frontend/src/components/objects/Connection.scss index 3b9f479..bf66272 100644 --- a/frontend/src/components/objects/Connection.scss +++ b/frontend/src/components/objects/Connection.scss @@ -46,6 +46,10 @@ .link-popover { font-weight: 400; } + + .connection-actions .link-popover { + text-decoration: none; + } } .connection-popover { diff --git a/frontend/src/components/objects/LinkPopover.scss b/frontend/src/components/objects/LinkPopover.scss index 725224c..c81f8bb 100644 --- a/frontend/src/components/objects/LinkPopover.scss +++ b/frontend/src/components/objects/LinkPopover.scss @@ -5,3 +5,8 @@ cursor: pointer; text-decoration: underline; } + +.popover { + font-family: "Fira Code", monospace; + font-size: 0.75em; +} diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js index 7376091..4632bbd 100644 --- a/frontend/src/components/pages/MainPage.js +++ b/frontend/src/components/pages/MainPage.js @@ -57,7 +57,7 @@ class MainPage extends Component { <Route path="/services" children={<ServicesPane/>}/> <Route exact path="/connections/:id" children={<StreamsPane connection={this.state.selectedConnection}/>}/> - <Route children={<MainPane/>}/> + <Route children={<MainPane version={this.props.version}/>}/> </Switch> </div> diff --git a/frontend/src/components/pages/MainPage.scss b/frontend/src/components/pages/MainPage.scss index 3b1a689..4ca54c0 100644 --- a/frontend/src/components/pages/MainPage.scss +++ b/frontend/src/components/pages/MainPage.scss @@ -13,6 +13,7 @@ } .details-pane { + position: relative; flex: 1 1; margin-left: 7.5px; } diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index 038ef8f..1f79ab8 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -27,6 +27,8 @@ import log from "../../log"; import ButtonField from "../fields/ButtonField"; import dispatcher from "../../dispatcher"; +const classNames = require('classnames'); + class ConnectionsPane extends Component { state = { @@ -81,6 +83,11 @@ class ConnectionsPane extends Component { this.loadServices().then(() => log.debug("Services reloaded after notification update")); } }); + + dispatcher.register("pulse_connections_view", payload => { + this.setState({pulseConnectionsView: true}); + setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration); + }); } connectionSelected = (c, doRedirect = true) => { @@ -246,7 +253,7 @@ class ConnectionsPane extends Component { return ( <div className="connections-container"> {this.state.showMoreRecentButton && <div className="most-recent-button"> - <ButtonField name="most_recent" variant="green" onClick={() => { + <ButtonField name="most_recent" variant="green" bordered onClick={() => { this.disableScrollHandler = true; this.connectionsListRef.current.scrollTop = 0; this.loadConnections({limit: this.queryLimit}) @@ -257,7 +264,8 @@ class ConnectionsPane extends Component { }}/> </div>} - <div className="connections" onScroll={this.handleScroll} ref={this.connectionsListRef}> + <div className={classNames("connections", {"connections-pulse": this.state.pulseConnectionsView})} + onScroll={this.handleScroll} ref={this.connectionsListRef}> <Table borderless size="sm"> <thead> <tr> diff --git a/frontend/src/components/panels/ConnectionsPane.scss b/frontend/src/components/panels/ConnectionsPane.scss index 06f5827..59fe372 100644 --- a/frontend/src/components/panels/ConnectionsPane.scss +++ b/frontend/src/components/panels/ConnectionsPane.scss @@ -33,6 +33,9 @@ z-index: 20; top: 45px; left: calc(50% - 50px); - background-color: red; + } + + .connections-pulse { + animation: pulse 2s infinite; } } diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js index 74c859c..8aa8ad8 100644 --- a/frontend/src/components/panels/MainPane.js +++ b/frontend/src/components/panels/MainPane.js @@ -17,17 +17,91 @@ import React, {Component} from 'react'; import './common.scss'; -import './ServicesPane.scss'; +import './MainPane.scss'; +import Typed from "typed.js"; +import dispatcher from "../../dispatcher"; +import RulesPane from "./RulesPane"; +import StreamsPane from "./StreamsPane"; +import PcapsPane from "./PcapsPane"; +import ServicesPane from "./ServicesPane"; class MainPane extends Component { state = {}; + componentDidMount() { + const nl = "^600\n^400"; + const options = { + strings: [ + `welcome to caronte!^1000 the current version is ${this.props.version}` + nl + + "caronte is a network analyzer,^300 it is able to read pcaps and extract connections", // 0 + "the left panel lists all connections that have already been closed" + nl + + "scrolling up the list will load the most recent connections,^300 downward the oldest ones", // 1 + "by selecting a connection you can view its content,^300 which will be shown in the right panel" + nl + + "you can choose the display format,^300 or decide to download the connection content", // 2 + "below there is the timeline,^300 which shows the number of connections per minute per service" + nl + + "you can use the sliding window to move the time range of the connections to be displayed", // 3 + "there are also additional metrics,^300 selectable from the drop-down menu", // 4 + "at the top are the filters,^300 which can be used to select only certain types of connections" + nl + + "you can choose which filters to display in the top bar from the filters window", // 5 + "in the pcaps panel it is possible to analyze new pcaps,^300 or to see the pcaps already analyzed" + nl + + "you can load pcaps from your browser,^300 or process pcaps already present on the filesystem", // 6 + "in the rules panel you can see the rules already created,^300 or create new ones" + nl + + "the rules inserted will be used only to label new connections, not those already analyzed" + nl + + "a connection is tagged if it meets all the requirements specified by the rule", // 7 + "in the services panel you can assign new services or edit existing ones" + nl + + "each service is associated with a port number,^300 and will be shown in the connection list", // 8 + "from the configuration panel you can change the settings of the frontend application", // 9 + "that's all! and have fun!" + nl + "created by @eciavatta" // 10 + ], + typeSpeed: 40, + cursorChar: "_", + backSpeed: 5, + smartBackspace: false, + backDelay: 1500, + preStringTyped: (arrayPos) => { + switch (arrayPos) { + case 1: + return dispatcher.dispatch("pulse_connections_view", {duration: 12000}); + case 2: + return this.setState({backgroundPane: <StreamsPane/>}); + case 3: + this.setState({backgroundPane: null}); + return dispatcher.dispatch("pulse_timeline", {duration: 12000}); + case 6: + return this.setState({backgroundPane: <PcapsPane/>}); + case 7: + return this.setState({backgroundPane: <RulesPane/>}); + case 8: + return this.setState({backgroundPane: <ServicesPane/>}); + case 10: + return this.setState({backgroundPane: null}); + default: + return; + } + }, + }; + this.typed = new Typed(this.el, options); + } + + componentWillUnmount() { + this.typed.destroy(); + } + render() { return ( - <div className="pane-container main-pane"> - <div className="pane-section"> - MainPane + <div className="pane-container"> + <div className="main-pane"> + <div className="pane-section"> + <div className="tutorial"> + <span style={{whiteSpace: 'pre'}} ref={(el) => { + this.el = el; + }}/> + </div> + </div> + </div> + <div className="background-pane"> + {this.state.backgroundPane} </div> </div> ); diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss index c8460f2..8f99b3c 100644 --- a/frontend/src/components/panels/MainPane.scss +++ b/frontend/src/components/panels/MainPane.scss @@ -1,5 +1,30 @@ @import "../../colors"; -.main-pane { +.pane-container { + background-color: $color-primary-0; + .main-pane { + position: absolute; + z-index: 50; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: transparent; + + .tutorial { + flex-basis: 100%; + padding: 5px 10px; + text-align: center; + background-color: $color-primary-2; + } + } + + .background-pane { + height: 100%; + opacity: 0.4; + } } diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js index c8bd121..bd1964e 100644 --- a/frontend/src/components/panels/StreamsPane.js +++ b/frontend/src/components/panels/StreamsPane.js @@ -208,8 +208,8 @@ class StreamsPane extends Component { ); return ( - <div className="connection-content"> - <div className="connection-content-header container-fluid"> + <div className="pane-container stream-pane"> + <div className="stream-pane-header container-fluid"> <Row> <div className="header-info col"> <span><strong>flow</strong>: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]}</span> @@ -235,7 +235,6 @@ class StreamsPane extends Component { </div> ); } - } diff --git a/frontend/src/components/panels/StreamsPane.scss b/frontend/src/components/panels/StreamsPane.scss index d5510cf..1f641f3 100644 --- a/frontend/src/components/panels/StreamsPane.scss +++ b/frontend/src/components/panels/StreamsPane.scss @@ -1,7 +1,6 @@ @import "../../colors"; -.connection-content { - height: 100%; +.stream-pane { background-color: $color-primary-0; pre { @@ -15,6 +14,10 @@ margin: 0; padding: 0; } + + &:hover::-webkit-scrollbar-thumb { + background: $color-secondary-2; + } } .connection-message { @@ -87,7 +90,7 @@ } } - .connection-content-header { + .stream-pane-header { height: 33px; padding: 0; background-color: $color-primary-3; diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss index 1468f35..335e65b 100644 --- a/frontend/src/components/panels/common.scss +++ b/frontend/src/components/panels/common.scss @@ -32,6 +32,10 @@ margin-left: 10px; color: $color-secondary-0; } + + &:hover::-webkit-scrollbar-thumb { + background: $color-secondary-2; + } } table { @@ -96,4 +100,8 @@ margin-left: 5px; } } + + &:hover::-webkit-scrollbar-thumb { + background: $color-secondary-2; + } } |