mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-30 03:14:22 +00:00
Merge branch 'master' into toxfiddle
This commit is contained in:
commit
227d762cac
File diff suppressed because one or more lines are too long
@ -11,7 +11,7 @@ var conf = {
|
|||||||
// Package these as well as the dependencies
|
// Package these as well as the dependencies
|
||||||
vendor_includes: [
|
vendor_includes: [
|
||||||
],
|
],
|
||||||
app: 'src/js/app.js',
|
app: 'src/js/app',
|
||||||
eslint: ["src/js/**/*.js", "!src/js/filt/filt.js"]
|
eslint: ["src/js/**/*.js", "!src/js/filt/filt.js"]
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
@ -142,6 +142,7 @@ function app_stream(dev) {
|
|||||||
var bundler = browserify({
|
var bundler = browserify({
|
||||||
entries: [conf.js.app],
|
entries: [conf.js.app],
|
||||||
debug: true,
|
debug: true,
|
||||||
|
extensions: ['.jsx'],
|
||||||
cache: {}, // required for watchify
|
cache: {}, // required for watchify
|
||||||
packageCache: {} // required for watchify
|
packageCache: {} // required for watchify
|
||||||
});
|
});
|
||||||
|
41
web/src/js/components/EventLog.jsx
Normal file
41
web/src/js/components/EventLog.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog'
|
||||||
|
import { ToggleButton } from './common'
|
||||||
|
import EventList from './EventLog/EventList'
|
||||||
|
|
||||||
|
EventLog.propTypes = {
|
||||||
|
filters: PropTypes.object.isRequired,
|
||||||
|
events: PropTypes.array.isRequired,
|
||||||
|
onToggleFilter: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventLog({ filters, events, onToggleFilter, onClose }) {
|
||||||
|
return (
|
||||||
|
<div className="eventlog">
|
||||||
|
<div>
|
||||||
|
Eventlog
|
||||||
|
<div className="pull-right">
|
||||||
|
{['debug', 'info', 'web'].map(type => (
|
||||||
|
<ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
|
||||||
|
))}
|
||||||
|
<i onClick={onClose} className="fa fa-close"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EventList events={events} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state => ({
|
||||||
|
filters: state.eventLog.filter,
|
||||||
|
events: state.eventLog.filteredEvents,
|
||||||
|
}),
|
||||||
|
dispatch => bindActionCreators({
|
||||||
|
onClose: toggleEventLogVisibility,
|
||||||
|
onToggleFilter: toggleEventLogFilter,
|
||||||
|
}, dispatch)
|
||||||
|
)(EventLog)
|
90
web/src/js/components/EventLog/EventList.jsx
Normal file
90
web/src/js/components/EventLog/EventList.jsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import shallowEqual from 'shallowequal'
|
||||||
|
import AutoScroll from '../helpers/AutoScroll'
|
||||||
|
import { calcVScroll } from '../helpers/VirtualScroll'
|
||||||
|
|
||||||
|
class EventLogList extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
events: PropTypes.array.isRequired,
|
||||||
|
rowHeight: PropTypes.number,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
rowHeight: 18,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.heights = {}
|
||||||
|
this.state = { vScroll: calcVScroll() }
|
||||||
|
|
||||||
|
this.onViewportUpdate = this.onViewportUpdate.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', this.onViewportUpdate)
|
||||||
|
this.onViewportUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.onViewportUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.onViewportUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewportUpdate() {
|
||||||
|
const viewport = ReactDOM.findDOMNode(this)
|
||||||
|
|
||||||
|
const vScroll = calcVScroll({
|
||||||
|
itemCount: this.props.events.length,
|
||||||
|
rowHeight: this.props.rowHeight,
|
||||||
|
viewportTop: viewport.scrollTop,
|
||||||
|
viewportHeight: viewport.offsetHeight,
|
||||||
|
itemHeights: this.props.events.map(entry => this.heights[entry.id]),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shallowEqual(this.state.vScroll, vScroll)) {
|
||||||
|
this.setState({vScroll})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeight(id, node) {
|
||||||
|
if (node && !this.heights[id]) {
|
||||||
|
const height = node.offsetHeight
|
||||||
|
if (this.heights[id] !== height) {
|
||||||
|
this.heights[id] = height
|
||||||
|
this.onViewportUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { vScroll } = this.state
|
||||||
|
const { events } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre onScroll={this.onViewportUpdate}>
|
||||||
|
<div style={{ height: vScroll.paddingTop }}></div>
|
||||||
|
{events.slice(vScroll.start, vScroll.end).map(event => (
|
||||||
|
<div key={event.id} ref={node => this.setHeight(event.id, node)}>
|
||||||
|
<LogIcon event={event}/>
|
||||||
|
{event.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ height: vScroll.paddingBottom }}></div>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogIcon({ event }) {
|
||||||
|
const icon = { web: 'html5', debug: 'bug' }[event.level] || 'info'
|
||||||
|
return <i className={`fa fa-fw fa-${icon}`}></i>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoScroll(EventLogList)
|
120
web/src/js/components/FlowTable.jsx
Normal file
120
web/src/js/components/FlowTable.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import shallowEqual from 'shallowequal'
|
||||||
|
import AutoScroll from './helpers/AutoScroll'
|
||||||
|
import { calcVScroll } from './helpers/VirtualScroll'
|
||||||
|
import FlowTableHead from './FlowTable/FlowTableHead'
|
||||||
|
import FlowRow from './FlowTable/FlowRow'
|
||||||
|
import Filt from "../filt/filt"
|
||||||
|
|
||||||
|
class FlowTable extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
flows: PropTypes.array.isRequired,
|
||||||
|
rowHeight: PropTypes.number,
|
||||||
|
highlight: PropTypes.string,
|
||||||
|
selected: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
rowHeight: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
|
||||||
|
this.state = { vScroll: calcVScroll() }
|
||||||
|
this.onViewportUpdate = this.onViewportUpdate.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
window.addEventListener('resize', this.onViewportUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.onViewportUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.onViewportUpdate()
|
||||||
|
|
||||||
|
if (!this.shouldScrollIntoView) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shouldScrollIntoView = false
|
||||||
|
|
||||||
|
const { rowHeight, flows, selected } = this.props
|
||||||
|
const viewport = ReactDOM.findDOMNode(this)
|
||||||
|
const head = ReactDOM.findDOMNode(this.refs.head)
|
||||||
|
|
||||||
|
const headHeight = head ? head.offsetHeight : 0
|
||||||
|
|
||||||
|
const rowTop = (flows.indexOf(selected) * rowHeight) + headHeight
|
||||||
|
const rowBottom = rowTop + rowHeight
|
||||||
|
|
||||||
|
const viewportTop = viewport.scrollTop
|
||||||
|
const viewportHeight = viewport.offsetHeight
|
||||||
|
|
||||||
|
// Account for pinned thead
|
||||||
|
if (rowTop - headHeight < viewportTop) {
|
||||||
|
viewport.scrollTop = rowTop - headHeight
|
||||||
|
} else if (rowBottom > viewportTop + viewportHeight) {
|
||||||
|
viewport.scrollTop = rowBottom - viewportHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.selected && nextProps.selected !== this.props.selected) {
|
||||||
|
this.shouldScrollIntoView = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewportUpdate() {
|
||||||
|
const viewport = ReactDOM.findDOMNode(this)
|
||||||
|
const viewportTop = viewport.scrollTop
|
||||||
|
|
||||||
|
const vScroll = calcVScroll({
|
||||||
|
viewportTop,
|
||||||
|
viewportHeight: viewport.offsetHeight,
|
||||||
|
itemCount: this.props.flows.length,
|
||||||
|
rowHeight: this.props.rowHeight,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.state.viewportTop !== viewportTop || !shallowEqual(this.state.vScroll, vScroll)) {
|
||||||
|
this.setState({ vScroll, viewportTop })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { vScroll, viewportTop } = this.state
|
||||||
|
const { flows, selected, highlight } = this.props
|
||||||
|
const isHighlighted = highlight ? Filt.parse(highlight) : () => false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flow-table" onScroll={this.onViewportUpdate}>
|
||||||
|
<table>
|
||||||
|
<thead ref="head" style={{ transform: `translateY(${viewportTop}px)` }}>
|
||||||
|
<FlowTableHead />
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style={{ height: vScroll.paddingTop }}></tr>
|
||||||
|
{flows.slice(vScroll.start, vScroll.end).map(flow => (
|
||||||
|
<FlowRow
|
||||||
|
key={flow.id}
|
||||||
|
flow={flow}
|
||||||
|
selected={flow === selected}
|
||||||
|
highlighted={isHighlighted(flow)}
|
||||||
|
onSelect={this.props.onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<tr style={{ height: vScroll.paddingBottom }}></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoScroll(FlowTable)
|
137
web/src/js/components/FlowTable/FlowColumns.jsx
Normal file
137
web/src/js/components/FlowTable/FlowColumns.jsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { RequestUtils, ResponseUtils } from '../../flow/utils.js'
|
||||||
|
import { formatSize, formatTimeDelta } from '../../utils.js'
|
||||||
|
|
||||||
|
export function TLSColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}></td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TLSColumn.sortKeyFun = flow => flow.request.scheme
|
||||||
|
TLSColumn.headerClass = 'col-tls'
|
||||||
|
TLSColumn.headerName = ''
|
||||||
|
|
||||||
|
export function IconColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-icon">
|
||||||
|
<div className={classnames('resource-icon', IconColumn.getIcon(flow))}></div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconColumn.headerClass = 'col-icon'
|
||||||
|
IconColumn.headerName = ''
|
||||||
|
|
||||||
|
IconColumn.getIcon = flow => {
|
||||||
|
if (!flow.response) {
|
||||||
|
return 'resource-icon-plain'
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||||
|
|
||||||
|
// @todo We should assign a type to the flow somewhere else.
|
||||||
|
if (flow.response.status_code === 304) {
|
||||||
|
return 'resource-icon-not-modified'
|
||||||
|
}
|
||||||
|
if (300 <= flow.response.status_code && flow.response.status_code < 400) {
|
||||||
|
return 'resource-icon-redirect'
|
||||||
|
}
|
||||||
|
if (contentType.indexOf('image') >= 0) {
|
||||||
|
return 'resource-icon-image'
|
||||||
|
}
|
||||||
|
if (contentType.indexOf('javascript') >= 0) {
|
||||||
|
return 'resource-icon-js'
|
||||||
|
}
|
||||||
|
if (contentType.indexOf('css') >= 0) {
|
||||||
|
return 'resource-icon-css'
|
||||||
|
}
|
||||||
|
if (contentType.indexOf('html') >= 0) {
|
||||||
|
return 'resource-icon-document'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'resource-icon-plain'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PathColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-path">
|
||||||
|
{flow.request.is_replay && (
|
||||||
|
<i className="fa fa-fw fa-repeat pull-right"></i>
|
||||||
|
)}
|
||||||
|
{flow.intercepted && (
|
||||||
|
<i className="fa fa-fw fa-pause pull-right"></i>
|
||||||
|
)}
|
||||||
|
{RequestUtils.pretty_url(flow.request)}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PathColumn.sortKeyFun = flow => RequestUtils.pretty_url(flow.request)
|
||||||
|
PathColumn.headerClass = 'col-path'
|
||||||
|
PathColumn.headerName = 'Path'
|
||||||
|
|
||||||
|
export function MethodColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-method">{flow.request.method}</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodColumn.sortKeyFun = flow => flow.request.method
|
||||||
|
MethodColumn.headerClass = 'col-method'
|
||||||
|
MethodColumn.headerName = 'Method'
|
||||||
|
|
||||||
|
export function StatusColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-status">{flow.response && flow.response.status_code}</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusColumn.sortKeyFun = flow => flow.response && flow.response.status_code
|
||||||
|
StatusColumn.headerClass = 'col-status'
|
||||||
|
StatusColumn.headerName = 'Status'
|
||||||
|
|
||||||
|
export function SizeColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-size">{formatSize(SizeColumn.getTotalSize(flow))}</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeColumn.sortKeyFun = flow => {
|
||||||
|
let total = flow.request.contentLength
|
||||||
|
if (flow.response) {
|
||||||
|
total += flow.response.contentLength || 0
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeColumn.getTotalSize = SizeColumn.sortKeyFun
|
||||||
|
SizeColumn.headerClass = 'col-size'
|
||||||
|
SizeColumn.headerName = 'Size'
|
||||||
|
|
||||||
|
export function TimeColumn({ flow }) {
|
||||||
|
return (
|
||||||
|
<td className="col-time">
|
||||||
|
{flow.response ? (
|
||||||
|
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
||||||
|
) : (
|
||||||
|
'...'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeColumn.sortKeyFun = flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start
|
||||||
|
TimeColumn.headerClass = 'col-time'
|
||||||
|
TimeColumn.headerName = 'Time'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
TLSColumn,
|
||||||
|
IconColumn,
|
||||||
|
PathColumn,
|
||||||
|
MethodColumn,
|
||||||
|
StatusColumn,
|
||||||
|
SizeColumn,
|
||||||
|
TimeColumn,
|
||||||
|
]
|
28
web/src/js/components/FlowTable/FlowRow.jsx
Normal file
28
web/src/js/components/FlowTable/FlowRow.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import columns from './FlowColumns'
|
||||||
|
|
||||||
|
FlowRow.propTypes = {
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
flow: PropTypes.object.isRequired,
|
||||||
|
highlighted: PropTypes.bool,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FlowRow({ flow, selected, highlighted, onSelect }) {
|
||||||
|
const className = classnames({
|
||||||
|
'selected': selected,
|
||||||
|
'highlighted': highlighted,
|
||||||
|
'intercepted': flow.intercepted,
|
||||||
|
'has-request': flow.request,
|
||||||
|
'has-response': flow.response,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={className} onClick={() => onSelect(flow)}>
|
||||||
|
{columns.map(Column => (
|
||||||
|
<Column key={Column.name} flow={flow}/>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
43
web/src/js/components/FlowTable/FlowTableHead.jsx
Normal file
43
web/src/js/components/FlowTable/FlowTableHead.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import columns from './FlowColumns'
|
||||||
|
|
||||||
|
import { setSort } from "../../ducks/flows"
|
||||||
|
|
||||||
|
FlowTableHead.propTypes = {
|
||||||
|
onSort: PropTypes.func.isRequired,
|
||||||
|
sortDesc: React.PropTypes.bool.isRequired,
|
||||||
|
sortColumn: React.PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlowTableHead({ sortColumn, sortDesc, onSort }) {
|
||||||
|
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
{columns.map(Column => (
|
||||||
|
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
||||||
|
key={Column.name}
|
||||||
|
onClick={() => onClick(Column)}>
|
||||||
|
{Column.headerName}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
|
||||||
|
function onClick(Column) {
|
||||||
|
onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state => ({
|
||||||
|
sortDesc: state.flows.sort.sortDesc,
|
||||||
|
sortColumn: state.flows.sort.sortColumn,
|
||||||
|
}),
|
||||||
|
dispatch => bindActionCreators({
|
||||||
|
onSort: setSort,
|
||||||
|
}, dispatch)
|
||||||
|
)(FlowTableHead)
|
@ -1,50 +1,47 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import {formatSize} from "../utils.js"
|
import { formatSize } from '../utils.js'
|
||||||
import {SettingsState} from "./common.js";
|
import { SettingsState } from './common.js'
|
||||||
|
|
||||||
Footer.propTypes = {
|
Footer.propTypes = {
|
||||||
settings: React.PropTypes.object.isRequired,
|
settings: React.PropTypes.object.isRequired,
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function Footer({ settings }) {
|
export default function Footer({ settings }) {
|
||||||
const {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream} = settings;
|
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer>
|
||||||
{mode && mode != "regular" && (
|
{settings.mode && settings.mode != "regular" && (
|
||||||
<span className="label label-success">{mode} mode</span>
|
<span className="label label-success">{settings.mode} mode</span>
|
||||||
)}
|
)}
|
||||||
{intercept && (
|
{settings.intercept && (
|
||||||
<span className="label label-success">Intercept: {intercept}</span>
|
<span className="label label-success">Intercept: {settings.intercept}</span>
|
||||||
)}
|
)}
|
||||||
{showhost && (
|
{settings.showhost && (
|
||||||
<span className="label label-success">showhost</span>
|
<span className="label label-success">showhost</span>
|
||||||
)}
|
)}
|
||||||
{no_upstream_cert && (
|
{settings.no_upstream_cert && (
|
||||||
<span className="label label-success">no-upstream-cert</span>
|
<span className="label label-success">no-upstream-cert</span>
|
||||||
)}
|
)}
|
||||||
{rawtcp && (
|
{settings.rawtcp && (
|
||||||
<span className="label label-success">raw-tcp</span>
|
<span className="label label-success">raw-tcp</span>
|
||||||
)}
|
)}
|
||||||
{!http2 && (
|
{!settings.http2 && (
|
||||||
<span className="label label-success">no-http2</span>
|
<span className="label label-success">no-http2</span>
|
||||||
)}
|
)}
|
||||||
{anticache && (
|
{settings.anticache && (
|
||||||
<span className="label label-success">anticache</span>
|
<span className="label label-success">anticache</span>
|
||||||
)}
|
)}
|
||||||
{anticomp && (
|
{settings.anticomp && (
|
||||||
<span className="label label-success">anticomp</span>
|
<span className="label label-success">anticomp</span>
|
||||||
)}
|
)}
|
||||||
{stickyauth && (
|
{settings.stickyauth && (
|
||||||
<span className="label label-success">stickyauth: {stickyauth}</span>
|
<span className="label label-success">stickyauth: {settings.stickyauth}</span>
|
||||||
)}
|
)}
|
||||||
{stickycookie && (
|
{settings.stickycookie && (
|
||||||
<span className="label label-success">stickycookie: {stickycookie}</span>
|
<span className="label label-success">stickycookie: {settings.stickycookie}</span>
|
||||||
)}
|
)}
|
||||||
{stream && (
|
{settings.stream && (
|
||||||
<span className="label label-success">stream: {formatSize(stream)}</span>
|
<span className="label label-success">stream: {formatSize(settings.stream)}</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
)
|
||||||
}
|
}
|
56
web/src/js/components/Header.js
Normal file
56
web/src/js/components/Header.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { toggleEventLogVisibility } from '../ducks/eventLog'
|
||||||
|
import MainMenu from './Header/MainMenu'
|
||||||
|
import ViewMenu from './Header/ViewMenu'
|
||||||
|
import OptionMenu from './Header/OptionMenu'
|
||||||
|
import FileMenu from './Header/FileMenu'
|
||||||
|
|
||||||
|
export default class Header extends Component {
|
||||||
|
|
||||||
|
static entries = [MainMenu, ViewMenu, OptionMenu]
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
this.state = { active: Header.entries[0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(active, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.props.updateLocation(active.route)
|
||||||
|
this.setState({ active })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { active: Active } = this.state
|
||||||
|
const { settings, updateLocation, query } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<nav className="nav-tabs nav-tabs-lg">
|
||||||
|
<FileMenu/>
|
||||||
|
{Header.entries.map(Entry => (
|
||||||
|
<a key={Entry.title}
|
||||||
|
href="#"
|
||||||
|
className={classnames({ active: Entry === Active })}
|
||||||
|
onClick={e => this.handleClick(Entry, e)}>
|
||||||
|
{Entry.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="menu">
|
||||||
|
<Active
|
||||||
|
ref="active"
|
||||||
|
settings={settings}
|
||||||
|
updateLocation={updateLocation}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
100
web/src/js/components/Header/FileMenu.jsx
Normal file
100
web/src/js/components/Header/FileMenu.jsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { FlowActions } from '../../actions.js'
|
||||||
|
|
||||||
|
export default class FileMenu extends Component {
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
this.state = { show: false }
|
||||||
|
|
||||||
|
this.close = this.close.bind(this)
|
||||||
|
this.onFileClick = this.onFileClick.bind(this)
|
||||||
|
this.onNewClick = this.onNewClick.bind(this)
|
||||||
|
this.onOpenClick = this.onOpenClick.bind(this)
|
||||||
|
this.onOpenFile = this.onOpenFile.bind(this)
|
||||||
|
this.onSaveClick = this.onSaveClick.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.setState({ show: false })
|
||||||
|
document.removeEventListener('click', this.close)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileClick(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (this.state.show) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', this.close)
|
||||||
|
this.setState({ show: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewClick(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (confirm('Delete all flows?')) {
|
||||||
|
FlowActions.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenClick(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.fileInput.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenFile(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
FlowActions.upload(e.target.files[0])
|
||||||
|
this.fileInput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveClick(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
FlowActions.download()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={classnames('dropdown pull-left', { open: this.state.show })}>
|
||||||
|
<a href="#" className="special" onClick={this.onFileClick}>mitmproxy</a>
|
||||||
|
<ul className="dropdown-menu" role="menu">
|
||||||
|
<li>
|
||||||
|
<a href="#" onClick={this.onNewClick}>
|
||||||
|
<i className="fa fa-fw fa-file"></i>
|
||||||
|
New
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" onClick={this.onOpenClick}>
|
||||||
|
<i className="fa fa-fw fa-folder-open"></i>
|
||||||
|
Open...
|
||||||
|
</a>
|
||||||
|
<input
|
||||||
|
ref={ref => this.fileInput = ref}
|
||||||
|
className="hidden"
|
||||||
|
type="file"
|
||||||
|
onChange={this.onOpenFile}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" onClick={this.onSaveClick}>
|
||||||
|
<i className="fa fa-fw fa-floppy-o"></i>
|
||||||
|
Save...
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" className="divider"></li>
|
||||||
|
<li>
|
||||||
|
<a href="http://mitm.it/" target="_blank">
|
||||||
|
<i className="fa fa-fw fa-external-link"></i>
|
||||||
|
Install Certificates...
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
56
web/src/js/components/Header/FilterDocs.jsx
Normal file
56
web/src/js/components/Header/FilterDocs.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
export default class FilterDocs extends Component {
|
||||||
|
|
||||||
|
// @todo move to redux
|
||||||
|
|
||||||
|
static xhr = null
|
||||||
|
static doc = null
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
this.state = { doc: FilterDocs.doc }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
if (!FilterDocs.xhr) {
|
||||||
|
FilterDocs.xhr = $.getJSON('/filter-help')
|
||||||
|
FilterDocs.xhr.fail(() => {
|
||||||
|
FilterDocs.xhr = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!this.state.doc) {
|
||||||
|
FilterDocs.xhr.done(doc => {
|
||||||
|
FilterDocs.doc = doc
|
||||||
|
this.setState({ doc })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { doc } = this.state
|
||||||
|
return !doc ? (
|
||||||
|
<i className="fa fa-spinner fa-spin"></i>
|
||||||
|
) : (
|
||||||
|
<table className="table table-condensed">
|
||||||
|
<tbody>
|
||||||
|
{doc.commands.map(cmd => (
|
||||||
|
<tr key={cmd[1]}>
|
||||||
|
<td>{cmd[0].replace(' ', '\u00a0')}</td>
|
||||||
|
<td>{cmd[1]}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
<tr key="docs-link">
|
||||||
|
<td colSpan="2">
|
||||||
|
<a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
|
||||||
|
target="_blank">
|
||||||
|
<i className="fa fa-external-link"></i>
|
||||||
|
  mitmproxy docs</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
133
web/src/js/components/Header/FilterInput.jsx
Normal file
133
web/src/js/components/Header/FilterInput.jsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, { PropTypes, Component } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import { Key } from '../../utils.js'
|
||||||
|
import Filt from '../../filt/filt'
|
||||||
|
import FilterDocs from './FilterDocs'
|
||||||
|
|
||||||
|
export default class FilterInput extends Component {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
returnFocus: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
|
||||||
|
// Consider both focus and mouseover for showing/hiding the tooltip,
|
||||||
|
// because onBlur of the input is triggered before the click on the tooltip
|
||||||
|
// finalized, hiding the tooltip just as the user clicks on it.
|
||||||
|
this.state = { value: this.props.value, focus: false, mousefocus: false }
|
||||||
|
|
||||||
|
this.onChange = this.onChange.bind(this)
|
||||||
|
this.onFocus = this.onFocus.bind(this)
|
||||||
|
this.onBlur = this.onBlur.bind(this)
|
||||||
|
this.onKeyDown = this.onKeyDown.bind(this)
|
||||||
|
this.onMouseEnter = this.onMouseEnter.bind(this)
|
||||||
|
this.onMouseLeave = this.onMouseLeave.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({ value: nextProps.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(filt) {
|
||||||
|
try {
|
||||||
|
const str = filt == null ? this.state.value : filt
|
||||||
|
if (str) {
|
||||||
|
Filt.parse(str)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDesc() {
|
||||||
|
if (!this.state.value) {
|
||||||
|
return <FilterDocs/>
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Filt.parse(this.state.value).desc
|
||||||
|
} catch (e) {
|
||||||
|
return '' + e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(e) {
|
||||||
|
const value = e.target.value
|
||||||
|
this.setState({ value })
|
||||||
|
|
||||||
|
// Only propagate valid filters upwards.
|
||||||
|
if (this.isValid(value)) {
|
||||||
|
this.props.onChange(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus() {
|
||||||
|
this.setState({ focus: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur() {
|
||||||
|
this.setState({ focus: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter() {
|
||||||
|
this.setState({ mousefocus: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave() {
|
||||||
|
this.setState({ mousefocus: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
|
||||||
|
this.blur()
|
||||||
|
// If closed using ESC/ENTER, hide the tooltip.
|
||||||
|
this.setState({mousefocus: false})
|
||||||
|
}
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
ReactDOM.findDOMNode(this.refs.input).blur()
|
||||||
|
this.context.returnFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
select() {
|
||||||
|
ReactDOM.findDOMNode(this.refs.input).select()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { type, color, placeholder } = this.props
|
||||||
|
const { value, focus, mousefocus } = this.state
|
||||||
|
return (
|
||||||
|
<div className={classnames('filter-input input-group', { 'has-error': !this.isValid() })}>
|
||||||
|
<span className="input-group-addon">
|
||||||
|
<i className={'fa fa-fw fa-' + type} style={{ color }}></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref="input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="form-control"
|
||||||
|
value={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
/>
|
||||||
|
{(focus || mousefocus) && (
|
||||||
|
<div className="popover bottom"
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
onMouseLeave={this.onMouseLeave}>
|
||||||
|
<div className="arrow"></div>
|
||||||
|
<div className="popover-content">
|
||||||
|
{this.getDesc()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
73
web/src/js/components/Header/MainMenu.jsx
Normal file
73
web/src/js/components/Header/MainMenu.jsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
import { SettingsActions } from "../../actions.js"
|
||||||
|
import FilterInput from './FilterInput'
|
||||||
|
import { Query } from '../../actions.js'
|
||||||
|
|
||||||
|
export default class MainMenu extends Component {
|
||||||
|
|
||||||
|
static title = 'Start'
|
||||||
|
static route = 'flows'
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
settings: React.PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
this.onSearchChange = this.onSearchChange.bind(this)
|
||||||
|
this.onHighlightChange = this.onHighlightChange.bind(this)
|
||||||
|
this.onInterceptChange = this.onInterceptChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChange(val) {
|
||||||
|
this.props.updateLocation(undefined, { [Query.SEARCH]: val })
|
||||||
|
}
|
||||||
|
|
||||||
|
onHighlightChange(val) {
|
||||||
|
this.props.updateLocation(undefined, { [Query.HIGHLIGHT]: val })
|
||||||
|
}
|
||||||
|
|
||||||
|
onInterceptChange(val) {
|
||||||
|
SettingsActions.update({ intercept: val })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { query, settings } = this.props
|
||||||
|
|
||||||
|
const search = query[Query.SEARCH] || ''
|
||||||
|
const highlight = query[Query.HIGHLIGHT] || ''
|
||||||
|
const intercept = settings.intercept || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="menu-row">
|
||||||
|
<FilterInput
|
||||||
|
ref="search"
|
||||||
|
placeholder="Search"
|
||||||
|
type="search"
|
||||||
|
color="black"
|
||||||
|
value={search}
|
||||||
|
onChange={this.onSearchChange}
|
||||||
|
/>
|
||||||
|
<FilterInput
|
||||||
|
ref="highlight"
|
||||||
|
placeholder="Highlight"
|
||||||
|
type="tag"
|
||||||
|
color="hsl(48, 100%, 50%)"
|
||||||
|
value={highlight}
|
||||||
|
onChange={this.onHighlightChange}
|
||||||
|
/>
|
||||||
|
<FilterInput
|
||||||
|
ref="intercept"
|
||||||
|
placeholder="Intercept"
|
||||||
|
type="pause"
|
||||||
|
color="hsl(208, 56%, 53%)"
|
||||||
|
value={intercept}
|
||||||
|
onChange={this.onInterceptChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
60
web/src/js/components/Header/OptionMenu.jsx
Normal file
60
web/src/js/components/Header/OptionMenu.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { ToggleInputButton, ToggleButton } from '../common.js'
|
||||||
|
import { SettingsActions } from '../../actions.js'
|
||||||
|
|
||||||
|
OptionMenu.title = "Options"
|
||||||
|
|
||||||
|
OptionMenu.propTypes = {
|
||||||
|
settings: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OptionMenu({ settings }) {
|
||||||
|
// @todo use settings.map
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="menu-row">
|
||||||
|
<ToggleButton text="showhost"
|
||||||
|
checked={settings.showhost}
|
||||||
|
onToggle={() => SettingsActions.update({ showhost: !settings.showhost })}
|
||||||
|
/>
|
||||||
|
<ToggleButton text="no_upstream_cert"
|
||||||
|
checked={settings.no_upstream_cert}
|
||||||
|
onToggle={() => SettingsActions.update({ no_upstream_cert: !settings.no_upstream_cert })}
|
||||||
|
/>
|
||||||
|
<ToggleButton text="rawtcp"
|
||||||
|
checked={settings.rawtcp}
|
||||||
|
onToggle={() => SettingsActions.update({ rawtcp: !settings.rawtcp })}
|
||||||
|
/>
|
||||||
|
<ToggleButton text="http2"
|
||||||
|
checked={settings.http2}
|
||||||
|
onToggle={() => SettingsActions.update({ http2: !settings.http2 })}
|
||||||
|
/>
|
||||||
|
<ToggleButton text="anticache"
|
||||||
|
checked={settings.anticache}
|
||||||
|
onToggle={() => SettingsActions.update({ anticache: !settings.anticache })}
|
||||||
|
/>
|
||||||
|
<ToggleButton text="anticomp"
|
||||||
|
checked={settings.anticomp}
|
||||||
|
onToggle={() => SettingsActions.update({ anticomp: !settings.anticomp })}
|
||||||
|
/>
|
||||||
|
<ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
|
||||||
|
checked={!!settings.stickyauth}
|
||||||
|
txt={settings.stickyauth || ''}
|
||||||
|
onToggleChanged={txt => SettingsActions.update({ stickyauth: !settings.stickyauth ? txt : null })}
|
||||||
|
/>
|
||||||
|
<ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
|
||||||
|
checked={!!settings.stickycookie}
|
||||||
|
txt={settings.stickycookie || ''}
|
||||||
|
onToggleChanged={txt => SettingsActions.update({ stickycookie: !settings.stickycookie ? txt : null })}
|
||||||
|
/>
|
||||||
|
<ToggleInputButton name="stream" placeholder="stream..."
|
||||||
|
checked={!!settings.stream}
|
||||||
|
txt={settings.stream || ''}
|
||||||
|
inputType="number"
|
||||||
|
onToggleChanged={txt => SettingsActions.update({ stream: !settings.stream ? txt : null })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="clearfix"/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
33
web/src/js/components/Header/ViewMenu.jsx
Normal file
33
web/src/js/components/Header/ViewMenu.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { ToggleButton } from '../common.js'
|
||||||
|
import { toggleEventLogVisibility } from '../../ducks/eventLog'
|
||||||
|
|
||||||
|
ViewMenu.title = 'View'
|
||||||
|
ViewMenu.route = 'flows'
|
||||||
|
|
||||||
|
ViewMenu.propTypes = {
|
||||||
|
visible: PropTypes.bool.isRequired,
|
||||||
|
onToggle: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewMenu({ visible, onToggle }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="menu-row">
|
||||||
|
<ToggleButton text="Show Event Log" checked={visible} onToggle={onToggle} />
|
||||||
|
</div>
|
||||||
|
<div className="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
state => ({
|
||||||
|
visible: state.eventLog.visible,
|
||||||
|
}),
|
||||||
|
dispatch => bindActionCreators({
|
||||||
|
onToggle: toggleEventLogVisibility,
|
||||||
|
}, dispatch)
|
||||||
|
)(ViewMenu)
|
@ -1,16 +1,22 @@
|
|||||||
import React, { Component } from "react"
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
|
||||||
import { FlowActions } from "../actions.js"
|
|
||||||
import { Query } from "../actions.js"
|
|
||||||
import { Key } from "../utils.js"
|
|
||||||
import { Splitter } from "./common.js"
|
|
||||||
import FlowTable from "./flowtable.js"
|
|
||||||
import FlowView from "./flowview/index.js"
|
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { selectFlow, setFilter, setHighlight } from "../ducks/flows"
|
import { bindActionCreators } from 'redux'
|
||||||
|
|
||||||
|
import { FlowActions } from '../actions.js'
|
||||||
|
import { Query } from '../actions.js'
|
||||||
|
import { Key } from '../utils.js'
|
||||||
|
import { Splitter } from './common.js'
|
||||||
|
import FlowTable from './FlowTable'
|
||||||
|
import FlowView from './flowview/index.js'
|
||||||
|
import { selectFlow, setFilter, setHighlight } from '../ducks/flows'
|
||||||
|
|
||||||
class MainView extends Component {
|
class MainView extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
highlight: PropTypes.string,
|
||||||
|
sort: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo move to actions
|
* @todo move to actions
|
||||||
* @todo replace with mapStateToProps
|
* @todo replace with mapStateToProps
|
||||||
@ -33,9 +39,9 @@ class MainView extends Component {
|
|||||||
*/
|
*/
|
||||||
selectFlow(flow) {
|
selectFlow(flow) {
|
||||||
if (flow) {
|
if (flow) {
|
||||||
this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || "request"}`)
|
this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || 'request'}`)
|
||||||
} else {
|
} else {
|
||||||
this.props.updateLocation("/flows")
|
this.props.updateLocation('/flows')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,20 +149,22 @@ class MainView extends Component {
|
|||||||
case Key.SHIFT:
|
case Key.SHIFT:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
console.debug("keydown", e.keyCode)
|
console.debug('keydown', e.keyCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { selectedFlow } = this.props
|
const { flows, selectedFlow, highlight, sort } = this.props
|
||||||
return (
|
return (
|
||||||
<div className="main-view">
|
<div className="main-view">
|
||||||
<FlowTable
|
<FlowTable
|
||||||
ref="flowTable"
|
ref="flowTable"
|
||||||
selectFlow={flow => this.selectFlow(flow)}
|
flows={flows}
|
||||||
selected={selectedFlow}
|
selected={selectedFlow}
|
||||||
|
highlight={highlight}
|
||||||
|
onSelect={flow => this.selectFlow(flow)}
|
||||||
/>
|
/>
|
||||||
{selectedFlow && [
|
{selectedFlow && [
|
||||||
<Splitter key="splitter"/>,
|
<Splitter key="splitter"/>,
|
||||||
@ -178,14 +186,15 @@ export default connect(
|
|||||||
state => ({
|
state => ({
|
||||||
flows: state.flows.view,
|
flows: state.flows.view,
|
||||||
filter: state.flows.filter,
|
filter: state.flows.filter,
|
||||||
|
sort: state.flows.sort,
|
||||||
highlight: state.flows.highlight,
|
highlight: state.flows.highlight,
|
||||||
selectedFlow: state.flows.all.byId[state.flows.selected[0]]
|
selectedFlow: state.flows.all.byId[state.flows.selected[0]]
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => bindActionCreators({
|
||||||
selectFlow: flowId => dispatch(selectFlow(flowId)),
|
selectFlow,
|
||||||
setFilter: filter => dispatch(setFilter(filter)),
|
setFilter,
|
||||||
setHighlight: highlight => dispatch(setHighlight(highlight))
|
setHighlight,
|
||||||
}),
|
}, dispatch),
|
||||||
undefined,
|
undefined,
|
||||||
{ withRef: true }
|
{ withRef: true }
|
||||||
)(MainView)
|
)(MainView)
|
@ -4,9 +4,9 @@ import _ from "lodash"
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
import { Splitter } from "./common.js"
|
import { Splitter } from "./common.js"
|
||||||
import { Header, MainMenu } from "./header.js"
|
import Header from "./Header"
|
||||||
import EventLog from "./eventlog.js"
|
import EventLog from "./EventLog"
|
||||||
import Footer from "./footer.js"
|
import Footer from "./Footer"
|
||||||
import { SettingsStore } from "../store/store.js"
|
import { SettingsStore } from "../store/store.js"
|
||||||
import { Key } from "../utils.js"
|
import { Key } from "../utils.js"
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ class ProxyAppMain extends Component {
|
|||||||
|
|
||||||
this.state = { settings: this.settingsStore.dict }
|
this.state = { settings: this.settingsStore.dict }
|
||||||
|
|
||||||
|
this.focus = this.focus.bind(this)
|
||||||
this.onKeyDown = this.onKeyDown.bind(this)
|
this.onKeyDown = this.onKeyDown.bind(this)
|
||||||
this.updateLocation = this.updateLocation.bind(this)
|
this.updateLocation = this.updateLocation.bind(this)
|
||||||
this.onSettingsChange = this.onSettingsChange.bind(this)
|
this.onSettingsChange = this.onSettingsChange.bind(this)
|
||||||
@ -45,7 +46,7 @@ class ProxyAppMain extends Component {
|
|||||||
}
|
}
|
||||||
const query = this.props.location.query
|
const query = this.props.location.query
|
||||||
for (const key of Object.keys(queryUpdate || {})) {
|
for (const key of Object.keys(queryUpdate || {})) {
|
||||||
query[i] = queryUpdate[i] || undefined
|
query[key] = queryUpdate[key] || undefined
|
||||||
}
|
}
|
||||||
this.context.router.replace({ pathname, query })
|
this.context.router.replace({ pathname, query })
|
||||||
}
|
}
|
||||||
@ -133,7 +134,7 @@ class ProxyAppMain extends Component {
|
|||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
const headerComponent = this.refs.header
|
const headerComponent = this.refs.header
|
||||||
headerComponent.setState({active: MainMenu}, function () {
|
headerComponent.setState({ active: Header.entries.MainMenu }, () => {
|
||||||
headerComponent.refs.active.refs[name].select()
|
headerComponent.refs.active.refs[name].select()
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,153 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import ReactDOM from "react-dom"
|
|
||||||
import {connect} from 'react-redux'
|
|
||||||
import shallowEqual from "shallowequal"
|
|
||||||
import {toggleEventLogFilter, toggleEventLogVisibility} from "../ducks/eventLog"
|
|
||||||
import AutoScroll from "./helpers/AutoScroll";
|
|
||||||
import {calcVScroll} from "./helpers/VirtualScroll"
|
|
||||||
import {ToggleButton} from "./common";
|
|
||||||
|
|
||||||
function LogIcon({event}) {
|
|
||||||
let icon = {web: "html5", debug: "bug"}[event.level] || "info";
|
|
||||||
return <i className={`fa fa-fw fa-${icon}`}></i>
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogEntry({event, registerHeight}) {
|
|
||||||
return <div ref={registerHeight}>
|
|
||||||
<LogIcon event={event}/>
|
|
||||||
{event.message}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EventLogContents extends React.Component {
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
rowHeight: 18,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.heights = {};
|
|
||||||
this.state = {vScroll: calcVScroll()};
|
|
||||||
|
|
||||||
this.onViewportUpdate = this.onViewportUpdate.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener("resize", this.onViewportUpdate);
|
|
||||||
this.onViewportUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener("resize", this.onViewportUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.onViewportUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewportUpdate() {
|
|
||||||
const viewport = ReactDOM.findDOMNode(this);
|
|
||||||
|
|
||||||
const vScroll = calcVScroll({
|
|
||||||
itemCount: this.props.events.length,
|
|
||||||
rowHeight: this.props.rowHeight,
|
|
||||||
viewportTop: viewport.scrollTop,
|
|
||||||
viewportHeight: viewport.offsetHeight,
|
|
||||||
itemHeights: this.props.events.map(entry => this.heights[entry.id]),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shallowEqual(this.state.vScroll, vScroll)) {
|
|
||||||
this.setState({vScroll});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeight(id, node) {
|
|
||||||
if (node && !this.heights[id]) {
|
|
||||||
const height = node.offsetHeight;
|
|
||||||
if (this.heights[id] !== height) {
|
|
||||||
this.heights[id] = height;
|
|
||||||
this.onViewportUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const vScroll = this.state.vScroll;
|
|
||||||
const events = this.props.events
|
|
||||||
.slice(vScroll.start, vScroll.end)
|
|
||||||
.map(event =>
|
|
||||||
<LogEntry
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
registerHeight={(node) => this.setHeight(event.id, node)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pre onScroll={this.onViewportUpdate}>
|
|
||||||
<div style={{ height: vScroll.paddingTop }}></div>
|
|
||||||
{events}
|
|
||||||
<div style={{ height: vScroll.paddingBottom }}></div>
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventLogContents = AutoScroll(EventLogContents);
|
|
||||||
|
|
||||||
|
|
||||||
const EventLogContentsContainer = connect(
|
|
||||||
state => ({
|
|
||||||
events: state.eventLog.filteredEvents
|
|
||||||
})
|
|
||||||
)(EventLogContents);
|
|
||||||
|
|
||||||
|
|
||||||
export const ToggleEventLog = connect(
|
|
||||||
state => ({
|
|
||||||
checked: state.eventLog.visible
|
|
||||||
}),
|
|
||||||
dispatch => ({
|
|
||||||
onToggle: () => dispatch(toggleEventLogVisibility())
|
|
||||||
})
|
|
||||||
)(ToggleButton);
|
|
||||||
|
|
||||||
|
|
||||||
const ToggleFilter = connect(
|
|
||||||
(state, ownProps) => ({
|
|
||||||
checked: state.eventLog.filter[ownProps.text]
|
|
||||||
}),
|
|
||||||
(dispatch, ownProps) => ({
|
|
||||||
onToggle: () => dispatch(toggleEventLogFilter(ownProps.text))
|
|
||||||
})
|
|
||||||
)(ToggleButton);
|
|
||||||
|
|
||||||
|
|
||||||
const EventLog = ({close}) =>
|
|
||||||
<div className="eventlog">
|
|
||||||
<div>
|
|
||||||
Eventlog
|
|
||||||
<div className="pull-right">
|
|
||||||
<ToggleFilter text="debug"/>
|
|
||||||
<ToggleFilter text="info"/>
|
|
||||||
<ToggleFilter text="web"/>
|
|
||||||
<i onClick={close} className="fa fa-close"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<EventLogContentsContainer/>
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
EventLog.propTypes = {
|
|
||||||
close: React.PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventLogContainer = connect(
|
|
||||||
undefined,
|
|
||||||
dispatch => ({
|
|
||||||
close: () => dispatch(toggleEventLogVisibility())
|
|
||||||
})
|
|
||||||
)(EventLog);
|
|
||||||
|
|
||||||
export default EventLogContainer;
|
|
@ -1,131 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import {RequestUtils, ResponseUtils} from "../flow/utils.js"
|
|
||||||
import {formatSize, formatTimeDelta} from "../utils.js"
|
|
||||||
|
|
||||||
|
|
||||||
export function TLSColumn({flow}) {
|
|
||||||
let ssl = (flow.request.scheme === "https")
|
|
||||||
let classes
|
|
||||||
if (ssl) {
|
|
||||||
classes = "col-tls col-tls-https"
|
|
||||||
} else {
|
|
||||||
classes = "col-tls col-tls-http"
|
|
||||||
}
|
|
||||||
return <td className={classes}></td>
|
|
||||||
}
|
|
||||||
TLSColumn.Title = ({className = "", ...props}) => <th {...props} className={"col-tls " + className }></th>
|
|
||||||
TLSColumn.sortKeyFun = flow => flow.request.scheme
|
|
||||||
|
|
||||||
|
|
||||||
export function IconColumn({flow}) {
|
|
||||||
let icon
|
|
||||||
if (flow.response) {
|
|
||||||
var contentType = ResponseUtils.getContentType(flow.response)
|
|
||||||
|
|
||||||
//TODO: We should assign a type to the flow somewhere else.
|
|
||||||
if (flow.response.status_code === 304) {
|
|
||||||
icon = "resource-icon-not-modified"
|
|
||||||
} else if (300 <= flow.response.status_code && flow.response.status_code < 400) {
|
|
||||||
icon = "resource-icon-redirect"
|
|
||||||
} else if (contentType && contentType.indexOf("image") >= 0) {
|
|
||||||
icon = "resource-icon-image"
|
|
||||||
} else if (contentType && contentType.indexOf("javascript") >= 0) {
|
|
||||||
icon = "resource-icon-js"
|
|
||||||
} else if (contentType && contentType.indexOf("css") >= 0) {
|
|
||||||
icon = "resource-icon-css"
|
|
||||||
} else if (contentType && contentType.indexOf("html") >= 0) {
|
|
||||||
icon = "resource-icon-document"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!icon) {
|
|
||||||
icon = "resource-icon-plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
icon += " resource-icon"
|
|
||||||
return <td className="col-icon">
|
|
||||||
<div className={icon}></div>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
IconColumn.Title = ({className = "", ...props}) => <th {...props} className={"col-icon " + className }></th>
|
|
||||||
|
|
||||||
|
|
||||||
export function PathColumn({flow}) {
|
|
||||||
return <td className="col-path">
|
|
||||||
{flow.request.is_replay ? <i className="fa fa-fw fa-repeat pull-right"></i> : null}
|
|
||||||
{flow.intercepted ? <i className="fa fa-fw fa-pause pull-right"></i> : null}
|
|
||||||
{ RequestUtils.pretty_url(flow.request) }
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
PathColumn.Title = ({className = "", ...props}) =>
|
|
||||||
<th {...props} className={"col-path " + className }>Path</th>
|
|
||||||
PathColumn.sortKeyFun = flow => RequestUtils.pretty_url(flow.request)
|
|
||||||
|
|
||||||
|
|
||||||
export function MethodColumn({flow}) {
|
|
||||||
return <td className="col-method">{flow.request.method}</td>
|
|
||||||
}
|
|
||||||
MethodColumn.Title = ({className = "", ...props}) =>
|
|
||||||
<th {...props} className={"col-method " + className }>Method</th>
|
|
||||||
MethodColumn.sortKeyFun = flow => flow.request.method
|
|
||||||
|
|
||||||
|
|
||||||
export function StatusColumn({flow}) {
|
|
||||||
let status
|
|
||||||
if (flow.response) {
|
|
||||||
status = flow.response.status_code
|
|
||||||
} else {
|
|
||||||
status = null
|
|
||||||
}
|
|
||||||
return <td className="col-status">{status}</td>
|
|
||||||
|
|
||||||
}
|
|
||||||
StatusColumn.Title = ({className = "", ...props}) =>
|
|
||||||
<th {...props} className={"col-status " + className }>Status</th>
|
|
||||||
StatusColumn.sortKeyFun = flow => flow.response ? flow.response.status_code : undefined
|
|
||||||
|
|
||||||
|
|
||||||
export function SizeColumn({flow}) {
|
|
||||||
let total = flow.request.contentLength
|
|
||||||
if (flow.response) {
|
|
||||||
total += flow.response.contentLength || 0
|
|
||||||
}
|
|
||||||
let size = formatSize(total)
|
|
||||||
return <td className="col-size">{size}</td>
|
|
||||||
|
|
||||||
}
|
|
||||||
SizeColumn.Title = ({className = "", ...props}) =>
|
|
||||||
<th {...props} className={"col-size " + className }>Size</th>
|
|
||||||
SizeColumn.sortKeyFun = flow => {
|
|
||||||
let total = flow.request.contentLength
|
|
||||||
if (flow.response) {
|
|
||||||
total += flow.response.contentLength || 0
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function TimeColumn({flow}) {
|
|
||||||
let time
|
|
||||||
if (flow.response) {
|
|
||||||
time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
|
||||||
} else {
|
|
||||||
time = "..."
|
|
||||||
}
|
|
||||||
return <td className="col-time">{time}</td>
|
|
||||||
}
|
|
||||||
TimeColumn.Title = ({className = "", ...props}) =>
|
|
||||||
<th {...props} className={"col-time " + className }>Time</th>
|
|
||||||
TimeColumn.sortKeyFun = flow => flow.response.timestamp_end - flow.request.timestamp_start
|
|
||||||
|
|
||||||
|
|
||||||
var all_columns = [
|
|
||||||
TLSColumn,
|
|
||||||
IconColumn,
|
|
||||||
PathColumn,
|
|
||||||
MethodColumn,
|
|
||||||
StatusColumn,
|
|
||||||
SizeColumn,
|
|
||||||
TimeColumn
|
|
||||||
]
|
|
||||||
|
|
||||||
export default all_columns
|
|
@ -1,201 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import {connect} from 'react-redux'
|
|
||||||
import classNames from "classnames";
|
|
||||||
import _ from "lodash";
|
|
||||||
import shallowEqual from "shallowequal";
|
|
||||||
import AutoScroll from "./helpers/AutoScroll";
|
|
||||||
import {calcVScroll} from "./helpers/VirtualScroll";
|
|
||||||
import flowtable_columns from "./flowtable-columns.js";
|
|
||||||
import Filt from "../filt/filt";
|
|
||||||
import {setSort} from "../ducks/flows";
|
|
||||||
|
|
||||||
|
|
||||||
FlowRow.propTypes = {
|
|
||||||
selectFlow: React.PropTypes.func.isRequired,
|
|
||||||
columns: React.PropTypes.array.isRequired,
|
|
||||||
flow: React.PropTypes.object.isRequired,
|
|
||||||
highlight: React.PropTypes.string,
|
|
||||||
selected: React.PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
function FlowRow({flow, selected, highlight, columns, selectFlow}) {
|
|
||||||
|
|
||||||
const className = classNames({
|
|
||||||
"selected": selected,
|
|
||||||
"highlighted": highlight && parseFilter(highlight)(flow),
|
|
||||||
"intercepted": flow.intercepted,
|
|
||||||
"has-request": flow.request,
|
|
||||||
"has-response": flow.response,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={className} onClick={() => selectFlow(flow)}>
|
|
||||||
{columns.map(Column => (
|
|
||||||
<Column key={Column.name} flow={flow}/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const FlowRowContainer = connect(
|
|
||||||
(state, ownProps) => ({
|
|
||||||
flow: state.flows.all.byId[ownProps.flowId],
|
|
||||||
highlight: state.flows.highlight,
|
|
||||||
selected: state.flows.selected.indexOf(ownProps.flowId) >= 0
|
|
||||||
})
|
|
||||||
)(FlowRow)
|
|
||||||
|
|
||||||
function FlowTableHead({setSort, columns, sort}) {
|
|
||||||
const sortColumn = sort.sortColumn;
|
|
||||||
const sortType = sort.sortDesc ? "sort-desc" : "sort-asc";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
{columns.map(Column => (
|
|
||||||
<Column.Title
|
|
||||||
key={Column.name}
|
|
||||||
onClick={() => setSort({sortColumn: Column.name, sortDesc: Column.name != sort.sortColumn ? false : !sort.sortDesc})}
|
|
||||||
className={sortColumn === Column.name ? sortType : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowTableHead.propTypes = {
|
|
||||||
setSort: React.PropTypes.func.isRequired,
|
|
||||||
sort: React.PropTypes.object.isRequired,
|
|
||||||
columns: React.PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlowTableHeadContainer = connect(
|
|
||||||
state => ({
|
|
||||||
sort: state.flows.sort
|
|
||||||
}),
|
|
||||||
dispatch => ({
|
|
||||||
setSort: (sort) => dispatch(setSort(sort)),
|
|
||||||
})
|
|
||||||
)(FlowTableHead)
|
|
||||||
|
|
||||||
class FlowTable extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
rowHeight: React.PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
rowHeight: 32,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {vScroll: calcVScroll()};
|
|
||||||
|
|
||||||
this.onViewportUpdate = this.onViewportUpdate.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
window.addEventListener("resize", this.onViewportUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener("resize", this.onViewportUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.selected && nextProps.selected !== this.props.selected) {
|
|
||||||
window.setTimeout(() => this.scrollIntoView(nextProps.selected), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.onViewportUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewportUpdate() {
|
|
||||||
const viewport = ReactDOM.findDOMNode(this);
|
|
||||||
const viewportTop = viewport.scrollTop;
|
|
||||||
|
|
||||||
const vScroll = calcVScroll({
|
|
||||||
viewportTop,
|
|
||||||
viewportHeight: viewport.offsetHeight,
|
|
||||||
itemCount: this.props.flows.length,
|
|
||||||
rowHeight: this.props.rowHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shallowEqual(this.state.vScroll, vScroll) ||
|
|
||||||
this.state.viewportTop !== viewportTop) {
|
|
||||||
this.setState({vScroll, viewportTop});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollIntoView(flow) {
|
|
||||||
const viewport = ReactDOM.findDOMNode(this);
|
|
||||||
const index = this.props.flows.indexOf(flow);
|
|
||||||
const rowHeight = this.props.rowHeight;
|
|
||||||
const head = ReactDOM.findDOMNode(this.refs.head);
|
|
||||||
|
|
||||||
const headHeight = head ? head.offsetHeight : 0;
|
|
||||||
|
|
||||||
const rowTop = (index * rowHeight) + headHeight;
|
|
||||||
const rowBottom = rowTop + rowHeight;
|
|
||||||
|
|
||||||
const viewportTop = viewport.scrollTop;
|
|
||||||
const viewportHeight = viewport.offsetHeight;
|
|
||||||
|
|
||||||
// Account for pinned thead
|
|
||||||
if (rowTop - headHeight < viewportTop) {
|
|
||||||
viewport.scrollTop = rowTop - headHeight;
|
|
||||||
} else if (rowBottom > viewportTop + viewportHeight) {
|
|
||||||
viewport.scrollTop = rowBottom - viewportHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const vScroll = this.state.vScroll;
|
|
||||||
const flows = this.props.flows.slice(vScroll.start, vScroll.end);
|
|
||||||
|
|
||||||
const transform = `translate(0,${this.state.viewportTop}px)`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flow-table" onScroll={this.onViewportUpdate}>
|
|
||||||
<table>
|
|
||||||
<thead ref="head" style={{ transform }}>
|
|
||||||
<FlowTableHeadContainer
|
|
||||||
columns={flowtable_columns}
|
|
||||||
setSortKeyFun={this.props.setSortKeyFun}
|
|
||||||
setSort={this.props.setSort}
|
|
||||||
/>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr style={{ height: vScroll.paddingTop }}></tr>
|
|
||||||
{flows.map(flow => (
|
|
||||||
<FlowRowContainer
|
|
||||||
key={flow.id}
|
|
||||||
flowId={flow.id}
|
|
||||||
columns={flowtable_columns}
|
|
||||||
selectFlow={this.props.selectFlow}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<tr style={{ height: vScroll.paddingBottom }}></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowTable = AutoScroll(FlowTable)
|
|
||||||
|
|
||||||
|
|
||||||
const parseFilter = _.memoize(Filt.parse)
|
|
||||||
|
|
||||||
const FlowTableContainer = connect(
|
|
||||||
state => ({
|
|
||||||
flows: state.flows.view
|
|
||||||
})
|
|
||||||
)(FlowTable)
|
|
||||||
|
|
||||||
export default FlowTableContainer;
|
|
@ -1,453 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import $ from "jquery";
|
|
||||||
import {connect} from 'react-redux'
|
|
||||||
|
|
||||||
import Filt from "../filt/filt.js";
|
|
||||||
import {Key} from "../utils.js";
|
|
||||||
import {ToggleInputButton, ToggleButton} from "./common.js";
|
|
||||||
import {SettingsActions, FlowActions} from "../actions.js";
|
|
||||||
import {Query} from "../actions.js";
|
|
||||||
import {SettingsState} from "./common.js";
|
|
||||||
import {ToggleEventLog} from "./eventlog"
|
|
||||||
|
|
||||||
var FilterDocs = React.createClass({
|
|
||||||
statics: {
|
|
||||||
xhr: false,
|
|
||||||
doc: false
|
|
||||||
},
|
|
||||||
componentWillMount: function () {
|
|
||||||
if (!FilterDocs.doc) {
|
|
||||||
FilterDocs.xhr = $.getJSON("/filter-help").done(function (doc) {
|
|
||||||
FilterDocs.doc = doc;
|
|
||||||
FilterDocs.xhr = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (FilterDocs.xhr) {
|
|
||||||
FilterDocs.xhr.done(function () {
|
|
||||||
this.forceUpdate();
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
if (!FilterDocs.doc) {
|
|
||||||
return <i className="fa fa-spinner fa-spin"></i>;
|
|
||||||
} else {
|
|
||||||
var commands = FilterDocs.doc.commands.map(function (c) {
|
|
||||||
return <tr key={c[1]}>
|
|
||||||
<td>{c[0].replace(" ", '\u00a0')}</td>
|
|
||||||
<td>{c[1]}</td>
|
|
||||||
</tr>;
|
|
||||||
});
|
|
||||||
commands.push(<tr key="docs-link">
|
|
||||||
<td colSpan="2">
|
|
||||||
<a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
|
|
||||||
target="_blank">
|
|
||||||
<i className="fa fa-external-link"></i>
|
|
||||||
mitmproxy docs</a>
|
|
||||||
</td>
|
|
||||||
</tr>);
|
|
||||||
return <table className="table table-condensed">
|
|
||||||
<tbody>{commands}</tbody>
|
|
||||||
</table>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var FilterInput = React.createClass({
|
|
||||||
contextTypes: {
|
|
||||||
returnFocus: React.PropTypes.func
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
// Consider both focus and mouseover for showing/hiding the tooltip,
|
|
||||||
// because onBlur of the input is triggered before the click on the tooltip
|
|
||||||
// finalized, hiding the tooltip just as the user clicks on it.
|
|
||||||
return {
|
|
||||||
value: this.props.value,
|
|
||||||
focus: false,
|
|
||||||
mousefocus: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
|
||||||
this.setState({value: nextProps.value});
|
|
||||||
},
|
|
||||||
onChange: function (e) {
|
|
||||||
var nextValue = e.target.value;
|
|
||||||
this.setState({
|
|
||||||
value: nextValue
|
|
||||||
});
|
|
||||||
// Only propagate valid filters upwards.
|
|
||||||
if (this.isValid(nextValue)) {
|
|
||||||
this.props.onChange(nextValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isValid: function (filt) {
|
|
||||||
try {
|
|
||||||
var str = filt || this.state.value;
|
|
||||||
if(str){
|
|
||||||
Filt.parse(filt || this.state.value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getDesc: function () {
|
|
||||||
if(this.state.value) {
|
|
||||||
try {
|
|
||||||
return Filt.parse(this.state.value).desc;
|
|
||||||
} catch (e) {
|
|
||||||
return "" + e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <FilterDocs/>;
|
|
||||||
},
|
|
||||||
onFocus: function () {
|
|
||||||
this.setState({focus: true});
|
|
||||||
},
|
|
||||||
onBlur: function () {
|
|
||||||
this.setState({focus: false});
|
|
||||||
},
|
|
||||||
onMouseEnter: function () {
|
|
||||||
this.setState({mousefocus: true});
|
|
||||||
},
|
|
||||||
onMouseLeave: function () {
|
|
||||||
this.setState({mousefocus: false});
|
|
||||||
},
|
|
||||||
onKeyDown: function (e) {
|
|
||||||
if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
|
|
||||||
this.blur();
|
|
||||||
// If closed using ESC/ENTER, hide the tooltip.
|
|
||||||
this.setState({mousefocus: false});
|
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
blur: function () {
|
|
||||||
ReactDOM.findDOMNode(this.refs.input).blur();
|
|
||||||
this.context.returnFocus();
|
|
||||||
},
|
|
||||||
select: function () {
|
|
||||||
ReactDOM.findDOMNode(this.refs.input).select();
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var isValid = this.isValid();
|
|
||||||
var icon = "fa fa-fw fa-" + this.props.type;
|
|
||||||
var groupClassName = "filter-input input-group" + (isValid ? "" : " has-error");
|
|
||||||
|
|
||||||
var popover;
|
|
||||||
if (this.state.focus || this.state.mousefocus) {
|
|
||||||
popover = (
|
|
||||||
<div className="popover bottom" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
|
||||||
<div className="arrow"></div>
|
|
||||||
<div className="popover-content">
|
|
||||||
{this.getDesc()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={groupClassName}>
|
|
||||||
<span className="input-group-addon">
|
|
||||||
<i className={icon} style={{color: this.props.color}}></i>
|
|
||||||
</span>
|
|
||||||
<input type="text" placeholder={this.props.placeholder} className="form-control"
|
|
||||||
ref="input"
|
|
||||||
onChange={this.onChange}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onKeyDown={this.onKeyDown}
|
|
||||||
value={this.state.value}/>
|
|
||||||
{popover}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export var MainMenu = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
settings: React.PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
statics: {
|
|
||||||
title: "Start",
|
|
||||||
route: "flows"
|
|
||||||
},
|
|
||||||
onSearchChange: function (val) {
|
|
||||||
var d = {};
|
|
||||||
d[Query.SEARCH] = val;
|
|
||||||
this.props.updateLocation(undefined, d);
|
|
||||||
},
|
|
||||||
onHighlightChange: function (val) {
|
|
||||||
var d = {};
|
|
||||||
d[Query.HIGHLIGHT] = val;
|
|
||||||
this.props.updateLocation(undefined, d);
|
|
||||||
},
|
|
||||||
onInterceptChange: function (val) {
|
|
||||||
SettingsActions.update({intercept: val});
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var search = this.props.query[Query.SEARCH] || "";
|
|
||||||
var highlight = this.props.query[Query.HIGHLIGHT] || "";
|
|
||||||
var intercept = this.props.settings.intercept || "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="menu-row">
|
|
||||||
<FilterInput
|
|
||||||
ref="search"
|
|
||||||
placeholder="Search"
|
|
||||||
type="search"
|
|
||||||
color="black"
|
|
||||||
value={search}
|
|
||||||
onChange={this.onSearchChange} />
|
|
||||||
<FilterInput
|
|
||||||
ref="highlight"
|
|
||||||
placeholder="Highlight"
|
|
||||||
type="tag"
|
|
||||||
color="hsl(48, 100%, 50%)"
|
|
||||||
value={highlight}
|
|
||||||
onChange={this.onHighlightChange}/>
|
|
||||||
<FilterInput
|
|
||||||
ref="intercept"
|
|
||||||
placeholder="Intercept"
|
|
||||||
type="pause"
|
|
||||||
color="hsl(208, 56%, 53%)"
|
|
||||||
value={intercept}
|
|
||||||
onChange={this.onInterceptChange}/>
|
|
||||||
</div>
|
|
||||||
<div className="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var ViewMenu = React.createClass({
|
|
||||||
statics: {
|
|
||||||
title: "View",
|
|
||||||
route: "flows"
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="menu-row">
|
|
||||||
<ToggleEventLog text="Show Event Log"/>
|
|
||||||
</div>
|
|
||||||
<div className="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OptionMenu = (props) => {
|
|
||||||
const {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickycookie, stickyauth, stream} = props.settings;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="menu-row">
|
|
||||||
<ToggleButton text="showhost"
|
|
||||||
checked={showhost}
|
|
||||||
onToggle={() => SettingsActions.update({showhost: !showhost})}
|
|
||||||
/>
|
|
||||||
<ToggleButton text="no_upstream_cert"
|
|
||||||
checked={no_upstream_cert}
|
|
||||||
onToggle={() => SettingsActions.update({no_upstream_cert: !no_upstream_cert})}
|
|
||||||
/>
|
|
||||||
<ToggleButton text="rawtcp"
|
|
||||||
checked={rawtcp}
|
|
||||||
onToggle={() => SettingsActions.update({rawtcp: !rawtcp})}
|
|
||||||
/>
|
|
||||||
<ToggleButton text="http2"
|
|
||||||
checked={http2}
|
|
||||||
onToggle={() => SettingsActions.update({http2: !http2})}
|
|
||||||
/>
|
|
||||||
<ToggleButton text="anticache"
|
|
||||||
checked={anticache}
|
|
||||||
onToggle={() => SettingsActions.update({anticache: !anticache})}
|
|
||||||
/>
|
|
||||||
<ToggleButton text="anticomp"
|
|
||||||
checked={anticomp}
|
|
||||||
onToggle={() => SettingsActions.update({anticomp: !anticomp})}
|
|
||||||
/>
|
|
||||||
<ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
|
|
||||||
checked={Boolean(stickyauth)}
|
|
||||||
txt={stickyauth || ""}
|
|
||||||
onToggleChanged={txt => SettingsActions.update({stickyauth: (!stickyauth ? txt : null)})}
|
|
||||||
/>
|
|
||||||
<ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
|
|
||||||
checked={Boolean(stickycookie)}
|
|
||||||
txt={stickycookie || ""}
|
|
||||||
onToggleChanged={txt => SettingsActions.update({stickycookie: (!stickycookie ? txt : null)})}
|
|
||||||
/>
|
|
||||||
<ToggleInputButton name="stream" placeholder="stream..."
|
|
||||||
checked={Boolean(stream)}
|
|
||||||
txt={stream || ""}
|
|
||||||
inputType = "number"
|
|
||||||
onToggleChanged={txt => SettingsActions.update({stream: (!stream ? txt : null)})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="clearfix"/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
OptionMenu.title = "Options";
|
|
||||||
|
|
||||||
OptionMenu.propTypes = {
|
|
||||||
settings: React.PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
var ReportsMenu = React.createClass({
|
|
||||||
statics: {
|
|
||||||
title: "Visualization",
|
|
||||||
route: "reports"
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return <div>Reports Menu</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FileMenu = React.createClass({
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
showFileMenu: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
handleFileClick: function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!this.state.showFileMenu) {
|
|
||||||
var close = function () {
|
|
||||||
this.setState({showFileMenu: false});
|
|
||||||
document.removeEventListener("click", close);
|
|
||||||
}.bind(this);
|
|
||||||
document.addEventListener("click", close);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
showFileMenu: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleNewClick: function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (confirm("Delete all flows?")) {
|
|
||||||
FlowActions.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleOpenClick: function (e) {
|
|
||||||
this.fileInput.click();
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
handleOpenFile: function (e) {
|
|
||||||
if (e.target.files.length > 0) {
|
|
||||||
FlowActions.upload(e.target.files[0]);
|
|
||||||
this.fileInput.value = "";
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
handleSaveClick: function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
FlowActions.download();
|
|
||||||
},
|
|
||||||
handleShutdownClick: function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
console.error("unimplemented: handleShutdownClick");
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={fileMenuClass}>
|
|
||||||
<a href="#" className="special" onClick={this.handleFileClick}> mitmproxy </a>
|
|
||||||
<ul className="dropdown-menu" role="menu">
|
|
||||||
<li>
|
|
||||||
<a href="#" onClick={this.handleNewClick}>
|
|
||||||
<i className="fa fa-fw fa-file"></i>
|
|
||||||
New
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" onClick={this.handleOpenClick}>
|
|
||||||
<i className="fa fa-fw fa-folder-open"></i>
|
|
||||||
Open...
|
|
||||||
</a>
|
|
||||||
<input ref={(ref) => this.fileInput = ref} className="hidden" type="file" onChange={this.handleOpenFile}/>
|
|
||||||
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" onClick={this.handleSaveClick}>
|
|
||||||
<i className="fa fa-fw fa-floppy-o"></i>
|
|
||||||
Save...
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li role="presentation" className="divider"></li>
|
|
||||||
<li>
|
|
||||||
<a href="http://mitm.it/" target="_blank">
|
|
||||||
<i className="fa fa-fw fa-external-link"></i>
|
|
||||||
Install Certificates...
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/*
|
|
||||||
<li role="presentation" className="divider"></li>
|
|
||||||
<li>
|
|
||||||
<a href="#" onClick={this.handleShutdownClick}>
|
|
||||||
<i className="fa fa-fw fa-plug"></i>
|
|
||||||
Shutdown
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
*/}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var header_entries = [MainMenu, ViewMenu, OptionMenu /*, ReportsMenu */];
|
|
||||||
|
|
||||||
|
|
||||||
export var Header = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
settings: React.PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
active: header_entries[0]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
handleClick: function (active, e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.updateLocation(active.route);
|
|
||||||
this.setState({active: active});
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
var header = header_entries.map(function (entry, i) {
|
|
||||||
var className;
|
|
||||||
if (entry === this.state.active) {
|
|
||||||
className = "active";
|
|
||||||
} else {
|
|
||||||
className = "";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<a key={i}
|
|
||||||
href="#"
|
|
||||||
className={className}
|
|
||||||
onClick={this.handleClick.bind(this, entry)}>
|
|
||||||
{entry.title}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<nav className="nav-tabs nav-tabs-lg">
|
|
||||||
<FileMenu/>
|
|
||||||
{header}
|
|
||||||
</nav>
|
|
||||||
<div className="menu">
|
|
||||||
<this.state.active
|
|
||||||
settings={this.props.settings}
|
|
||||||
updateLocation={this.props.updateLocation}
|
|
||||||
query={this.props.query}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -42,7 +42,7 @@ var Prompt = React.createClass({
|
|||||||
var opts = [];
|
var opts = [];
|
||||||
|
|
||||||
var keyTaken = function (k) {
|
var keyTaken = function (k) {
|
||||||
return _.includes(_.pluck(opts, "key"), k);
|
return _.includes(_.map(opts, "key"), k);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var i = 0; i < this.props.options.length; i++) {
|
for (var i = 0; i < this.props.options.length; i++) {
|
||||||
|
@ -2,7 +2,7 @@ import makeList from "./utils/list"
|
|||||||
import Filt from "../filt/filt"
|
import Filt from "../filt/filt"
|
||||||
import {updateViewFilter, updateViewList, updateViewSort} from "./utils/view"
|
import {updateViewFilter, updateViewList, updateViewSort} from "./utils/view"
|
||||||
import {reverseString} from "../utils.js";
|
import {reverseString} from "../utils.js";
|
||||||
import * as flow_table_columns from "../components/flowtable-columns.js";
|
import * as columns from "../components/FlowTable/FlowColumns";
|
||||||
|
|
||||||
export const UPDATE_FLOWS = "UPDATE_FLOWS"
|
export const UPDATE_FLOWS = "UPDATE_FLOWS"
|
||||||
export const SET_FILTER = "SET_FLOW_FILTER"
|
export const SET_FILTER = "SET_FLOW_FILTER"
|
||||||
@ -32,7 +32,7 @@ function makeFilterFn(filter) {
|
|||||||
|
|
||||||
|
|
||||||
function makeSortFn(sort){
|
function makeSortFn(sort){
|
||||||
let column = flow_table_columns[sort.sortColumn];
|
let column = columns[sort.sortColumn];
|
||||||
if (!column) return;
|
if (!column) return;
|
||||||
|
|
||||||
let sortKeyFun = column.sortKeyFun;
|
let sortKeyFun = column.sortKeyFun;
|
||||||
|
Loading…
Reference in New Issue
Block a user