diff --git a/web/package.json b/web/package.json
index 65c2f1cc7..6b3e9a391 100644
--- a/web/package.json
+++ b/web/package.json
@@ -7,14 +7,17 @@
"start": "gulp"
},
"dependencies": {
+ "@popperjs/core": "^2.9.2",
"bootstrap": "^3.3.7",
"classnames": "^2.3.1",
"lodash": "^4.17.21",
"mock-xmlhttprequest": "^1.1.0",
+ "popper.js": "^1.16.1",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-codemirror": "^1.0.0",
"react-dom": "^17.0.2",
+ "react-popper": "^2.2.5",
"react-redux": "^7.2.4",
"react-test-renderer": "^17.0.2",
"redux": "^4.1.0",
diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less
index 9f7dbc40a..7e6d63d46 100644
--- a/web/src/css/flowtable.less
+++ b/web/src/css/flowtable.less
@@ -190,4 +190,52 @@
transform: translate(-1px, 2px);
}
+ .dropdown-submenu {
+ position:relative;
+ }
+
+ .dropdown-submenu>.dropdown-menu {
+ top:0;
+ left:100%;
+ margin-top:-6px;
+ margin-left:-1px;
+ -webkit-border-radius:0 6px 6px 6px;
+ -moz-border-radius:0 6px 6px 6px;
+ border-radius:0 6px 6px 6px;
+ }
+
+ .dropdown-submenu:hover>.dropdown-menu {
+ display:block;
+ }
+
+ .dropdown-submenu>a:after {
+ display:block;
+ content:" ";
+ float:right;
+ width:0;
+ height:0;
+ border-color:transparent;
+ border-style:solid;
+ border-width:5px 0 5px 5px;
+ border-left-color:#cccccc;
+ margin-top:5px;
+ margin-right:-10px;
+ }
+
+ .dropdown-submenu:hover>a:after {
+ border-left-color:#ffffff;
+ }
+
+ .dropdown-submenu.pull-left {
+ float:none;
+ }
+
+ .dropdown-submenu.pull-left>.dropdown-menu {
+ left:-100%;
+ margin-left:10px;
+ -webkit-border-radius:6px 0 6px 6px;
+ -moz-border-radius:6px 0 6px 6px;
+ border-radius:6px 0 6px 6px;
+ }
+
}
diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
index dd444abfd..860253bc4 100644
--- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
+++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
@@ -102,15 +102,7 @@ exports[`Flowcolumns Components should render QuickActionsColumn 1`] = `
-
+
|
`;
diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.js.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.js.snap
index 8edcbd2a2..a5966feb0 100644
--- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.js.snap
+++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.js.snap
@@ -51,72 +51,7 @@ exports[`FlowRow Component should render correctly 1`] = `
-
+
|
-
`;
diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx
index 9896ec591..c26d9b326 100644
--- a/web/src/js/components/FlowTable/FlowColumns.jsx
+++ b/web/src/js/components/FlowTable/FlowColumns.jsx
@@ -4,6 +4,7 @@ import classnames from 'classnames'
import {RequestUtils, ResponseUtils} from '../../flow/utils.js'
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js'
import * as flowActions from "../../ducks/flows";
+import HoverMenu from "./HoverMenu";
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
@@ -163,7 +164,7 @@ export function TimeStampColumn({flow}) {
TimeStampColumn.headerClass = 'col-timestamp'
TimeStampColumn.headerName = 'TimeStamp'
-export function QuickActionsColumn({flow}) {
+export function QuickActionsColumn({flow, selected}) {
const dispatch = useDispatch()
function resume(e) {
@@ -175,7 +176,9 @@ export function QuickActionsColumn({flow}) {
return (
-
+ {selected ?
+
+ : null}
{flow.intercepted
?
: null}
diff --git a/web/src/js/components/FlowTable/FlowRow.jsx b/web/src/js/components/FlowTable/FlowRow.jsx
index c3973d515..7c2b14ba2 100644
--- a/web/src/js/components/FlowTable/FlowRow.jsx
+++ b/web/src/js/components/FlowTable/FlowRow.jsx
@@ -4,7 +4,6 @@ import classnames from 'classnames'
import {defaultColumnNames} from './FlowColumns'
import { pure } from '../../utils'
import {getDisplayColumns} from './FlowTableHead'
-import HoverMenu from './HoverMenu'
import { connect } from 'react-redux'
FlowRow.propTypes = {
@@ -28,9 +27,8 @@ function FlowRow({ flow, selected, highlighted, onSelect, displayColumnNames })
return (
onSelect(flow.id)}>
{displayColumns.map(Column => (
-
+
))}
-
)
}
diff --git a/web/src/js/components/FlowTable/HoverMenu.jsx b/web/src/js/components/FlowTable/HoverMenu.jsx
index 8016c9471..f3e531041 100644
--- a/web/src/js/components/FlowTable/HoverMenu.jsx
+++ b/web/src/js/components/FlowTable/HoverMenu.jsx
@@ -1,16 +1,12 @@
-import React, { Component } from 'react'
+import React, { Component, useCallback } from 'react'
import { connect } from 'react-redux'
import { update as updateSettings } from "../../ducks/settings"
import Dropdown from '../common/Dropdown'
+import DropdownSubMenu from '../common/DropdownSubMenu'
-class HoverMenu extends Component {
- constructor(props, context) {
- super(props, context)
- }
-
- onClick(e, example) {
+let SubMenu = ({flow, intercept, updateSettings}) => {
+ const onClick = useCallback((e, example) => {
e.preventDefault();
- let intercept = this.props.intercept
if (intercept && intercept.includes(example)) {
return
}
@@ -19,7 +15,46 @@ class HoverMenu extends Component {
} else {
intercept = `${intercept} | ${example}`
}
- this.props.updateSettings({ intercept })
+ updateSettings({ intercept })
+ }, [intercept])
+
+ return (
+
+ {
+ onClick(e, flow.request.host)
+ }}>
+
+ Intercept {flow.request.host}
+
+ { flow.request.path != "/" ? {
+ onClick(e, flow.request.host + flow.request.path)
+ }}>
+
+ Intercept {flow.request.host + flow.request.path}
+ : null }
+ {
+ onClick(e, `~m POST & ${flow.request.host}`)
+ }}>
+
+ Intercept all POST requests from this host
+
+
+ )
+}
+
+SubMenu = connect(
+ state => ({
+ flow: state.flows.byId[state.flows.selected[0]],
+ intercept: state.settings.intercept,
+ }),
+ {
+ updateSettings,
+ }
+)(SubMenu)
+
+class HoverMenu extends Component {
+ constructor(props, context) {
+ super(props, context)
}
render() {
@@ -28,24 +63,18 @@ class HoverMenu extends Component {
return null
}
return (
-
+ }>
{
- this.onClick(e, flow.request.host)
+ e.preventDefault()
}}>
- Intercept {flow.request.host}
+ Copy as Curl
- { flow.request.path != "/" ? {
- this.onClick(e, flow.request.host + flow.request.path)
- }}>
-
- Intercept {flow.request.host + flow.request.path}
- : null }
{
- this.onClick(e, `~m POST & ${flow.request.host}`)
+ e.preventDefault()
}}>
- Intercept all POST requests from this host
+ Download HAR
)
@@ -55,9 +84,6 @@ class HoverMenu extends Component {
export default connect(
state => ({
flow: state.flows.byId[state.flows.selected[0]],
- intercept: state.settings.intercept,
}),
- {
- updateSettings,
- }
+ null
)(HoverMenu)
\ No newline at end of file
diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx
index 4999b420b..f339bcea0 100644
--- a/web/src/js/components/common/Dropdown.jsx
+++ b/web/src/js/components/common/Dropdown.jsx
@@ -39,15 +39,17 @@ export default class Dropdown extends Component {
}
render() {
- const {dropup, className, btnClass, text, children} = this.props
+ const {dropup, className, btnClass, text, icon, children, submenu} = this.props
return (
)
diff --git a/web/src/js/components/common/DropdownSubMenu.jsx b/web/src/js/components/common/DropdownSubMenu.jsx
new file mode 100644
index 000000000..c1c7656de
--- /dev/null
+++ b/web/src/js/components/common/DropdownSubMenu.jsx
@@ -0,0 +1,73 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import { Manager, Reference, Popper } from 'react-popper';
+import classnames from 'classnames'
+
+export const Divider = () =>
+
+export default class DropdownSubMenu extends Component {
+
+ static propTypes = {
+ dropup: PropTypes.bool,
+ className: PropTypes.string,
+ }
+
+ static defaultProps = {
+ dropup: false
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = {open: false}
+ this.close = this.close.bind(this)
+ this.open = this.open.bind(this)
+ }
+
+ close() {
+ this.setState({open: false})
+ document.removeEventListener('click', this.close)
+ }
+
+ open(e) {
+ e.preventDefault()
+ if (this.state.open) {
+ return
+ }
+ e.stopPropagation();
+ this.setState({open: !this.state.open})
+ document.addEventListener('click', this.close)
+ }
+
+ render() {
+ const {text, children} = this.props
+
+ return (
+
+
+
+ {({ ref }) => (
+ {text}
+ )}
+
+
+ {({ ref, style, placement, arrowProps }) => (
+
+ {children.map((item, i) => - {item}
)}
+
+ )}
+
+
+
+ )
+ }
+}
|