diff --git a/web/package.json b/web/package.json index 6b3e9a391..7b4d64a55 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,6 @@ "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", diff --git a/web/src/css/app.less b/web/src/css/app.less index 1f246b192..7fbf65044 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -19,3 +19,4 @@ html { @import (less) "codemirror.less"; @import (less) "contentview.less"; @import (less) "modal.less"; +@import (less) "dropdown.less"; diff --git a/web/src/css/dropdown.less b/web/src/css/dropdown.less index ba8442df3..e8b7d696e 100644 --- a/web/src/css/dropdown.less +++ b/web/src/css/dropdown.less @@ -1,4 +1,3 @@ -hr.divider { - margin-top: 5px; - margin-bottom: 5px; +.dropdown-menu > li > a { + padding: 3px 10px; } diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index 7e6d63d46..a002c4a77 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -17,14 +17,14 @@ table-layout: fixed; } - thead { + thead tr { background-color: #F2F2F2; + border-bottom: solid #bebebe 1px; line-height: 23px; } th { font-weight: normal; - box-shadow: 0 1px 0 #a6a6a6; position: relative !important; padding-left: 1px; .user-select(none); @@ -155,12 +155,15 @@ .col-quickactions { width: 0; direction: rtl; + * { + direction: ltr; + } overflow: hidden; background-color: inherit; font-size: 20px; } - tr:hover .col-quickactions { + tr:hover .col-quickactions, .col-quickactions.hover { overflow: visible; } @@ -170,72 +173,24 @@ display: inline-flex; align-items: center; - div { + > a { margin-right: 2px; height: 32px; width: 32px; border-radius: 16px; + text-align: center; - &:hover { - background-color: rgba(0, 0, 0, 5%); + &:hover, &.open { + background-color: rgba(0, 0, 0, 5%); } } } .col-quickactions .fa-ellipsis-h { - transform: translate(-3px, 3px); + transform: translate(0, 3px); } .col-quickactions .fa-play { - transform: translate(-1px, 2px); + 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/css/tabs.less b/web/src/css/tabs.less index 43f7264ee..2c2573100 100644 --- a/web/src/css/tabs.less +++ b/web/src/css/tabs.less @@ -4,7 +4,7 @@ border-bottom: solid @separator-color 1px; - a { + > a { display: inline-block; border: solid transparent 1px; text-decoration: none; @@ -30,20 +30,20 @@ } .nav-tabs-lg { - a { + > a { padding: 3px 14px; margin: 0 2px -1px; } } .nav-tabs-sm { - a { + > a { padding: 0px 7px; margin: 2px 2px -1px; } - a.nav-action { + > a.nav-action { float: right; padding: 0; margin: 1px 0 0px; } -} \ No newline at end of file +} diff --git a/web/src/js/__tests__/components/common/DropdownSpec.js b/web/src/js/__tests__/components/common/DropdownSpec.js index 218b3590f..fbd1579aa 100644 --- a/web/src/js/__tests__/components/common/DropdownSpec.js +++ b/web/src/js/__tests__/components/common/DropdownSpec.js @@ -3,27 +3,18 @@ import renderer from 'react-test-renderer' import Dropdown, { Divider } from '../../../components/common/Dropdown' describe('Dropdown Component', () => { - let dropup = renderer.create( - 1 - - 2 - ), - dropdown = renderer.create( + let dropdown = renderer.create( 1 2 ) it('should render correctly', () => { - let tree = dropup.toJSON() - expect(tree).toMatchSnapshot() - - tree = dropdown.toJSON() + let tree = dropdown.toJSON() expect(tree).toMatchSnapshot() }) it('should handle open/close action', () => { - document.body.addEventListener('click', ()=>{}) - let tree = dropup.toJSON(), + let tree = dropdown.toJSON(), e = { preventDefault: jest.fn(), stopPropagation: jest.fn() } tree.children[0].props.onClick(e) expect(tree).toMatchSnapshot() @@ -31,6 +22,9 @@ describe('Dropdown Component', () => { // click action when the state is open tree.children[0].props.onClick(e) + // open again + tree.children[0].props.onClick(e) + // close document.body.click() expect(tree).toMatchSnapshot() diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 69a7024d3..263540abd 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -1,8 +1,8 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { setContentView } from '../../ducks/ui/flow'; -import Dropdown from '../common/Dropdown' +import {connect} from 'react-redux' +import {setContentView} from '../../ducks/ui/flow'; +import Dropdown, {MenuItem} from '../common/Dropdown' ViewSelector.propTypes = { @@ -11,22 +11,24 @@ ViewSelector.propTypes = { setContentView: PropTypes.func.isRequired } -export function ViewSelector ({contentViews, activeView, setContentView}){ - let inner = View: {activeView.toLowerCase()} +export function ViewSelector({contentViews, activeView, setContentView}) { + let inner = View: {activeView.toLowerCase()} return ( - + {contentViews.map(name => - {e.preventDefault(); setContentView(name)}}> + setContentView(name)}> {name.toLowerCase().replace('_', ' ')} - - ) - } + + )} ) } -export default connect ( +export default connect( state => ({ contentViews: state.settings.contentViews || [], activeView: state.ui.flow.contentView, diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx index c26d9b326..7c46a4575 100644 --- a/web/src/js/components/FlowTable/FlowColumns.jsx +++ b/web/src/js/components/FlowTable/FlowColumns.jsx @@ -1,10 +1,10 @@ -import React from 'react' +import React, {useEffect, useState} from 'react' import {useDispatch} from 'react-redux' 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"; +import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown"; export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"] @@ -167,22 +167,29 @@ TimeStampColumn.headerName = 'TimeStamp' export function QuickActionsColumn({flow, selected}) { const dispatch = useDispatch() - function resume(e) { - dispatch(flowActions.resume(flow)) - e.preventDefault() - e.stopPropagation() + let [open, setOpen] = useState(false) + + let intercept = null; + if (flow.intercepted) { + intercept = dispatch(flowActions.resume(flow))}> + + ; } return ( - + e.stopPropagation()}>
- {selected ? -
- : null} - {flow.intercepted - ?
- : null} + {intercept} + } className="quickaction" onOpen={setOpen} options={{placement: "bottom-end"}}> + alert("Foo!")}>Foo + + Qux + Quux + + Baz +
+ ) } diff --git a/web/src/js/components/FlowTable/HoverMenu.jsx b/web/src/js/components/FlowTable/HoverMenu.jsx deleted file mode 100644 index f3e531041..000000000 --- a/web/src/js/components/FlowTable/HoverMenu.jsx +++ /dev/null @@ -1,89 +0,0 @@ -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' - -let SubMenu = ({flow, intercept, updateSettings}) => { - const onClick = useCallback((e, example) => { - e.preventDefault(); - if (intercept && intercept.includes(example)) { - return - } - if (!intercept) { - intercept = example - } else { - intercept = `${intercept} | ${example}` - } - 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() { - const { flow } = this.props - if (!flow) { - return null - } - return ( - }> - { - e.preventDefault() - }}> - -  Copy as Curl - - { - e.preventDefault() - }}> - -  Download HAR - - - ) - } -} - -export default connect( - state => ({ - flow: state.flows.byId[state.flows.selected[0]], - }), - null -)(HoverMenu) \ No newline at end of file diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx index f0790c390..6a6a3fbdd 100644 --- a/web/src/js/components/Header/FileMenu.jsx +++ b/web/src/js/components/Header/FileMenu.jsx @@ -1,10 +1,9 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { connect } from 'react-redux' +import {connect} from 'react-redux' import FileChooser from '../common/FileChooser' -import Dropdown, {Divider} from '../common/Dropdown' +import Dropdown, {Divider, MenuItem} from '../common/Dropdown' import * as flowsActions from '../../ducks/flows' -import * as modalActions from '../../ducks/ui/modal' import HideInStatic from "../common/HideInStatic"; FileMenu.propTypes = { @@ -19,29 +18,36 @@ FileMenu.onNewClick = (e, clearFlows) => { clearFlows() } -export function FileMenu ({clearFlows, loadFlows, saveFlows}) { - return ( - - FileMenu.onNewClick(e, clearFlows)}> - +export function FileMenu({clearFlows, loadFlows, saveFlows}) { + return ( + + FileMenu.onNewClick(e, clearFlows)}> +  Clear All - - loadFlows(file)} - /> - { e.preventDefault(); saveFlows();}}> - + +
  • + loadFlows(file)} + /> +
  • + { + e.preventDefault(); + saveFlows(); + }}> +  Save... -
    + - - - -  Install Certificates... - + +
  • + + +  Install Certificates... + +
  • ) diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx deleted file mode 100644 index f339bcea0..000000000 --- a/web/src/js/components/common/Dropdown.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' - -export const Divider = () =>
    - -export default class Dropdown extends Component { - - static propTypes = { - dropup: PropTypes.bool, - className: PropTypes.string, - btnClass: PropTypes.string.isRequired - } - - 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 {dropup, className, btnClass, text, icon, children, submenu} = this.props - return ( -
    - - {icon ? : null} - {text} - -
      - {children.map((item, i) =>
    • {item}
    • )} - {submenu} -
    -
    - ) - } -} diff --git a/web/src/js/components/common/Dropdown.tsx b/web/src/js/components/common/Dropdown.tsx new file mode 100644 index 000000000..32964bfe9 --- /dev/null +++ b/web/src/js/components/common/Dropdown.tsx @@ -0,0 +1,114 @@ +import React, {useEffect, useState} from 'react' +import {usePopper} from "react-popper"; +import * as PopperJS from "@popperjs/core"; +import classnames from "classnames"; + +export const Divider = () =>
  • ; + + +type MenuItemProps = { + onClick: () => void, + children: React.ReactNode +} + +export function MenuItem({onClick, children, ...attrs}: MenuItemProps) { + + const click = (e) => { + e.preventDefault(); + onClick(); + } + + return
  • + + {children} + +
  • +} + +type SubMenuProps = { + title: string, + children: React.ReactNode, +} + +export function SubMenu({title, children}: SubMenuProps) { + const [open, setOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const {styles, attributes} = usePopper(referenceElement, popperElement, {placement: "right-start"}); + + let submenu = null; + if (open) { + submenu =
      {children}
    ; + } + + return ( +
  • setOpen(true)} + onMouseLeave={() => setOpen(false)} + > +