[web] separate flowtable to multiple files

This commit is contained in:
Jason 2016-06-09 17:46:14 +08:00
parent 52754f40c2
commit f306cfa8b6
13 changed files with 407 additions and 394 deletions

File diff suppressed because one or more lines are too long

View File

@ -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: {
@ -27,4 +27,4 @@ var conf = {
peg: ["src/js/filt/filt.peg"] peg: ["src/js/filt/filt.peg"]
}; };
module.exports = conf; module.exports = conf;

View File

@ -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
}); });

View 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)

View 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,
]

View 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>
)
}

View 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)

View File

@ -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)

View File

@ -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 })
} }

View File

@ -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

View File

@ -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;

View File

@ -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;
@ -108,4 +108,4 @@ export function selectFlow(flowId) {
} }
export {updateList as updateFlows, fetchList as fetchFlows} export {updateList as updateFlows, fetchList as fetchFlows}