diff options
Diffstat (limited to 'web/src/js')
23 files changed, 1100 insertions, 608 deletions
diff --git a/web/src/js/actions.js b/web/src/js/actions.js index 9211403f..2c4183e4 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -1,37 +1,64 @@ var ActionTypes = { - //Settings - UPDATE_SETTINGS: "update_settings", + // Connection + CONNECTION_OPEN: "connection_open", + CONNECTION_CLOSE: "connection_close", + CONNECTION_ERROR: "connection_error", - //EventLog - ADD_EVENT: "add_event", + // Stores + SETTINGS_STORE: "settings", + EVENT_STORE: "events", + FLOW_STORE: "flows", +}; - //Flow - ADD_FLOW: "add_flow", - UPDATE_FLOW: "update_flow", +var StoreCmds = { + ADD: "add", + UPDATE: "update", + REMOVE: "remove", + RESET: "reset" +}; + +var ConnectionActions = { + open: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_OPEN + }); + }, + close: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_CLOSE + }); + }, + error: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CONNECTION_ERROR + }); + } }; var SettingsActions = { update: function (settings) { - settings = _.merge({}, SettingsStore.getAll(), settings); + //TODO: Update server. //Facebook Flux: We do an optimistic update on the client already. AppDispatcher.dispatchViewAction({ - type: ActionTypes.UPDATE_SETTINGS, - settings: settings + type: ActionTypes.SETTINGS_STORE, + cmd: StoreCmds.UPDATE, + data: settings }); } }; -var event_id = 0; +var EventLogActions_event_id = 0; var EventLogActions = { - add_event: function(message){ + add_event: function (message) { AppDispatcher.dispatchViewAction({ - type: ActionTypes.ADD_EVENT, + type: ActionTypes.EVENT_STORE, + cmd: StoreCmds.ADD, data: { message: message, level: "web", - id: "viewAction-"+event_id++ + id: "viewAction-" + EventLogActions_event_id++ } }); } diff --git a/web/src/js/app.js b/web/src/js/app.js index 736072dc..5146cb46 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -1,4 +1,7 @@ $(function () { - Connection.init(); - app = React.renderComponent(ProxyApp, document.body); + window.ws = new Connection("/updates"); + + ReactRouter.run(routes, function (Handler) { + React.render(<Handler/>, document.body); + }); });
\ No newline at end of file diff --git a/web/src/js/components/eventlog.jsx.js b/web/src/js/components/eventlog.jsx.js index 08a6dfb4..7ef369f8 100644 --- a/web/src/js/components/eventlog.jsx.js +++ b/web/src/js/components/eventlog.jsx.js @@ -1,10 +1,8 @@ -/** @jsx React.DOM */ - var LogMessage = React.createClass({ - render: function(){ + render: function () { var entry = this.props.entry; var indicator; - switch(entry.level){ + switch (entry.level) { case "web": indicator = <i className="fa fa-fw fa-html5"></i>; break; @@ -20,47 +18,79 @@ var LogMessage = React.createClass({ </div> ); }, - shouldComponentUpdate: function(){ + shouldComponentUpdate: function () { return false; // log entries are immutable. } }); var EventLogContents = React.createClass({ - mixins:[AutoScrollMixin], + mixins: [AutoScrollMixin, VirtualScrollMixin], getInitialState: function () { return { log: [] }; }, - componentDidMount: function () { - this.log = EventLogStore.getView(); - this.log.addListener("change", this.onEventLogChange); + componentWillMount: function () { + this.openView(this.props.eventStore); }, componentWillUnmount: function () { - this.log.removeListener("change", this.onEventLogChange); - this.log.close(); + this.closeView(); + }, + openView: function (store) { + var view = new StoreView(store, function (entry) { + return this.props.filter[entry.level]; + }.bind(this)); + this.setState({ + view: view + }); + + view.addListener("add recalculate", this.onEventLogChange); + }, + closeView: function () { + this.state.view.close(); }, onEventLogChange: function () { this.setState({ - log: this.log.getAll() + log: this.state.view.list }); }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.filter !== this.props.filter) { + this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. + this.state.view.recalculate(this.props.eventStore.list); + } + if (nextProps.eventStore !== this.props.eventStore) { + this.closeView(); + this.openView(nextProps.eventStore); + } + }, + getDefaultProps: function () { + return { + rowHeight: 45, + rowHeightMin: 15, + placeholderTagName: "div" + }; + }, + renderRow: function (elem) { + return <LogMessage key={elem.id} entry={elem}/>; + }, render: function () { - var messages = this.state.log.map(function(row) { - if(!this.props.filter[row.level]){ - return null; - } - return <LogMessage key={row.id} entry={row}/>; - }.bind(this)); - return <pre>{messages}</pre>; + var rows = this.renderRows(this.state.log); + + return <pre onScroll={this.onScroll}> + { this.getPlaceholderTop(this.state.log.length) } + {rows} + { this.getPlaceholderBottom(this.state.log.length) } + </pre>; } }); var ToggleFilter = React.createClass({ - toggle: function(){ + toggle: function (e) { + e.preventDefault(); return this.props.toggleLevel(this.props.name); }, - render: function(){ + render: function () { var className = "label "; if (this.props.active) { className += "label-primary"; @@ -75,11 +105,11 @@ var ToggleFilter = React.createClass({ {this.props.name} </a> ); - } + } }); var EventLog = React.createClass({ - getInitialState: function(){ + getInitialState: function () { return { filter: { "debug": false, @@ -93,11 +123,10 @@ var EventLog = React.createClass({ showEventLog: false }); }, - toggleLevel: function(level){ - var filter = this.state.filter; + toggleLevel: function (level) { + var filter = _.extend({}, this.state.filter); filter[level] = !filter[level]; this.setState({filter: filter}); - return false; }, render: function () { return ( @@ -112,7 +141,7 @@ var EventLog = React.createClass({ </div> </div> - <EventLogContents filter={this.state.filter}/> + <EventLogContents filter={this.state.filter} eventStore={this.props.eventStore}/> </div> ); } diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js index 3ba025a9..6d46cd2e 100644 --- a/web/src/js/components/flowdetail.jsx.js +++ b/web/src/js/components/flowdetail.jsx.js @@ -1,34 +1,32 @@ -/** @jsx React.DOM */ - var FlowDetailNav = React.createClass({ - render: function(){ + render: function () { - var items = this.props.tabs.map(function(e){ + var items = this.props.tabs.map(function (e) { var str = e.charAt(0).toUpperCase() + e.slice(1); var className = this.props.active === e ? "active" : ""; - var onClick = function(){ + var onClick = function (event) { this.props.selectTab(e); - return false; + event.preventDefault(); }.bind(this); return <a key={e} - href="#" - className={className} - onClick={onClick}>{str}</a>; + href="#" + className={className} + onClick={onClick}>{str}</a>; }.bind(this)); return ( <nav ref="head" className="nav-tabs nav-tabs-sm"> {items} </nav> ); - } + } }); var Headers = React.createClass({ - render: function(){ - var rows = this.props.message.headers.map(function(header, i){ + render: function () { + var rows = this.props.message.headers.map(function (header, i) { return ( <tr key={i}> - <td className="header-name">{header[0]+":"}</td> + <td className="header-name">{header[0] + ":"}</td> <td className="header-value">{header[1]}</td> </tr> ); @@ -44,16 +42,16 @@ var Headers = React.createClass({ }); var FlowDetailRequest = React.createClass({ - render: function(){ + render: function () { var flow = this.props.flow; var first_line = [ - flow.request.method, - RequestUtils.pretty_url(flow.request), - "HTTP/"+ flow.response.httpversion.join(".") - ].join(" "); + flow.request.method, + RequestUtils.pretty_url(flow.request), + "HTTP/" + flow.request.httpversion.join(".") + ].join(" "); var content = null; - if(flow.request.contentLength > 0){ - content = "Request Content Size: "+ formatSize(flow.request.contentLength); + if (flow.request.contentLength > 0) { + content = "Request Content Size: " + formatSize(flow.request.contentLength); } else { content = <div className="alert alert-info">No Content</div>; } @@ -72,16 +70,16 @@ var FlowDetailRequest = React.createClass({ }); var FlowDetailResponse = React.createClass({ - render: function(){ + render: function () { var flow = this.props.flow; var first_line = [ - "HTTP/"+ flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); + "HTTP/" + flow.response.httpversion.join("."), + flow.response.code, + flow.response.msg + ].join(" "); var content = null; - if(flow.response.contentLength > 0){ - content = "Response Content Size: "+ formatSize(flow.response.contentLength); + if (flow.response.contentLength > 0) { + content = "Response Content Size: " + formatSize(flow.response.contentLength); } else { content = <div className="alert alert-info">No Content</div>; } @@ -99,43 +97,67 @@ var FlowDetailResponse = React.createClass({ } }); +var FlowDetailError = React.createClass({ + render: function () { + var flow = this.props.flow; + return ( + <section> + <div className="alert alert-warning"> + {flow.error.msg} + <div><small>{ formatTimeStamp(flow.error.timestamp) }</small></div> + </div> + </section> + ); + } +}); + var TimeStamp = React.createClass({ - render: function() { + render: function () { - if(!this.props.t){ + if (!this.props.t) { //should be return null, but that triggers a React bug. return <tr></tr>; } - var ts = (new Date(this.props.t * 1000)).toISOString(); - ts = ts.replace("T", " ").replace("Z",""); + var ts = formatTimeStamp(this.props.t); var delta; - if(this.props.deltaTo){ - delta = formatTimeDelta(1000 * (this.props.t-this.props.deltaTo)); + if (this.props.deltaTo) { + delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); delta = <span className="text-muted">{"(" + delta + ")"}</span>; } else { delta = null; } - return <tr><td>{this.props.title + ":"}</td><td>{ts} {delta}</td></tr>; + return <tr> + <td>{this.props.title + ":"}</td> + <td>{ts} {delta}</td> + </tr>; } }); var ConnectionInfo = React.createClass({ - render: function() { + render: function () { var conn = this.props.conn; var address = conn.address.address.join(":"); var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug. - if(conn.sni){ - sni = <tr key="sni"><td><abbr title="TLS Server Name Indication">TLS SNI:</abbr></td><td>{conn.sni}</td></tr>; + if (conn.sni) { + sni = <tr key="sni"> + <td> + <abbr title="TLS Server Name Indication">TLS SNI:</abbr> + </td> + <td>{conn.sni}</td> + </tr>; } return ( <table className="connection-table"> <tbody> - <tr key="address"><td>Address:</td><td>{address}</td></tr> + <tr key="address"> + <td>Address:</td> + <td>{address}</td> + </tr> {sni} </tbody> </table> @@ -144,7 +166,7 @@ var ConnectionInfo = React.createClass({ }); var CertificateInfo = React.createClass({ - render: function(){ + render: function () { //TODO: We should fetch human-readable certificate representation // from the server var flow = this.props.flow; @@ -165,7 +187,7 @@ var CertificateInfo = React.createClass({ }); var Timing = React.createClass({ - render: function(){ + render: function () { var flow = this.props.flow; var sc = flow.server_conn; var cc = flow.client_conn; @@ -218,82 +240,112 @@ var Timing = React.createClass({ } //Add unique key for each row. - timestamps.forEach(function(e){ + timestamps.forEach(function (e) { e.key = e.title; }); timestamps = _.sortBy(timestamps, 't'); - var rows = timestamps.map(function(e){ - return TimeStamp(e); + var rows = timestamps.map(function (e) { + return <TimeStamp {...e}/>; }); return ( <div> - <h4>Timing</h4> - <table className="timing-table"> - <tbody> + <h4>Timing</h4> + <table className="timing-table"> + <tbody> {rows} - </tbody> - </table> + </tbody> + </table> </div> ); } }); var FlowDetailConnectionInfo = React.createClass({ - render: function(){ + render: function () { var flow = this.props.flow; var client_conn = flow.client_conn; var server_conn = flow.server_conn; return ( <section> - <h4>Client Connection</h4> - <ConnectionInfo conn={client_conn}/> + <h4>Client Connection</h4> + <ConnectionInfo conn={client_conn}/> - <h4>Server Connection</h4> - <ConnectionInfo conn={server_conn}/> + <h4>Server Connection</h4> + <ConnectionInfo conn={server_conn}/> - <CertificateInfo flow={flow}/> + <CertificateInfo flow={flow}/> - <Timing flow={flow}/> + <Timing flow={flow}/> </section> ); } }); -var tabs = { +var allTabs = { request: FlowDetailRequest, response: FlowDetailResponse, + error: FlowDetailError, details: FlowDetailConnectionInfo }; var FlowDetail = React.createClass({ - getDefaultProps: function(){ - return { - tabs: ["request","response", "details"] - }; + mixins: [StickyHeadMixin, ReactRouter.Navigation, ReactRouter.State], + getTabs: function (flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; }, - mixins: [StickyHeadMixin], - nextTab: function(i) { - var currentIndex = this.props.tabs.indexOf(this.props.active); + nextTab: function (i) { + var tabs = this.getTabs(this.props.flow); + var currentIndex = tabs.indexOf(this.getParams().detailTab); // JS modulo operator doesn't correct negative numbers, make sure that we are positive. - var nextIndex = (currentIndex + i + this.props.tabs.length) % this.props.tabs.length; - this.props.selectTab(this.props.tabs[nextIndex]); + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); }, - render: function(){ - var flow = JSON.stringify(this.props.flow, null, 2); - var Tab = tabs[this.props.active]; + selectTab: function (panel) { + this.replaceWith( + "flow", + { + flowId: this.getParams().flowId, + detailTab: panel + } + ); + }, + render: function () { + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getParams().detailTab; + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var Tab = allTabs[active]; return ( <div className="flow-detail" onScroll={this.adjustHead}> <FlowDetailNav ref="head" - tabs={this.props.tabs} - active={this.props.active} - selectTab={this.props.selectTab}/> - <Tab flow={this.props.flow}/> + tabs={tabs} + active={active} + selectTab={this.selectTab}/> + <Tab flow={flow}/> </div> - ); - } + ); + } });
\ No newline at end of file diff --git a/web/src/js/components/flowtable-columns.jsx.js b/web/src/js/components/flowtable-columns.jsx.js index b7db71b7..1aa256c4 100644 --- a/web/src/js/components/flowtable-columns.jsx.js +++ b/web/src/js/components/flowtable-columns.jsx.js @@ -1,17 +1,14 @@ -/** @jsx React.DOM */ - - var TLSColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="tls" className="col-tls"></th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; var ssl = (flow.request.scheme == "https"); var classes; - if(ssl){ + if (ssl) { classes = "col-tls col-tls-https"; } else { classes = "col-tls col-tls-http"; @@ -23,23 +20,23 @@ var TLSColumn = React.createClass({ var IconColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="icon" className="col-icon"></th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; var icon; - if(flow.response){ + if (flow.response) { var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - if(flow.response.code == 304) { + if (flow.response.code == 304) { icon = "resource-icon-not-modified"; - } else if(300 <= flow.response.code && flow.response.code < 400) { + } else if (300 <= flow.response.code && flow.response.code < 400) { icon = "resource-icon-redirect"; - } else if(contentType && contentType.indexOf("image") >= 0) { + } else if (contentType && contentType.indexOf("image") >= 0) { icon = "resource-icon-image"; } else if (contentType && contentType.indexOf("javascript") >= 0) { icon = "resource-icon-js"; @@ -49,23 +46,25 @@ var IconColumn = React.createClass({ icon = "resource-icon-document"; } } - if(!icon){ + if (!icon) { icon = "resource-icon-plain"; } icon += " resource-icon"; - return <td className="col-icon"><div className={icon}></div></td>; + return <td className="col-icon"> + <div className={icon}></div> + </td>; } }); var PathColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="path" className="col-path">Path</th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; return <td className="col-path">{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>; } @@ -74,11 +73,11 @@ var PathColumn = React.createClass({ var MethodColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="method" className="col-method">Method</th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; return <td className="col-method">{flow.request.method}</td>; } @@ -87,14 +86,14 @@ var MethodColumn = React.createClass({ var StatusColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="status" className="col-status">Status</th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; var status; - if(flow.response){ + if (flow.response) { status = flow.response.code; } else { status = null; @@ -106,15 +105,15 @@ var StatusColumn = React.createClass({ var SizeColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="size" className="col-size">Size</th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; var total = flow.request.contentLength; - if(flow.response){ + if (flow.response) { total += flow.response.contentLength || 0; } var size = formatSize(total); @@ -125,14 +124,14 @@ var SizeColumn = React.createClass({ var TimeColumn = React.createClass({ statics: { - renderTitle: function(){ + renderTitle: function () { return <th key="time" className="col-time">Time</th>; } }, - render: function(){ + render: function () { var flow = this.props.flow; var time; - if(flow.response){ + if (flow.response) { time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); } else { time = "..."; diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js index fc4d8fbc..4b72dd29 100644 --- a/web/src/js/components/flowtable.jsx.js +++ b/web/src/js/components/flowtable.jsx.js @@ -1,13 +1,11 @@ -/** @jsx React.DOM */ - var FlowRow = React.createClass({ - render: function(){ + render: function () { var flow = this.props.flow; - var columns = this.props.columns.map(function(column){ - return <column key={column.displayName} flow={flow}/>; + var columns = this.props.columns.map(function (Column) { + return <Column key={Column.displayName} flow={flow}/>; }.bind(this)); var className = ""; - if(this.props.selected){ + if (this.props.selected) { className += "selected"; } return ( @@ -15,80 +13,97 @@ var FlowRow = React.createClass({ {columns} </tr>); }, - shouldComponentUpdate: function(nextProps){ - var isEqual = ( - this.props.columns.length === nextProps.columns.length && - this.props.selected === nextProps.selected && - this.props.flow.response === nextProps.flow.response); - return !isEqual; + shouldComponentUpdate: function (nextProps) { + return true; + // Further optimization could be done here + // by calling forceUpdate on flow updates, selection changes and column changes. + //return ( + //(this.props.columns.length !== nextProps.columns.length) || + //(this.props.selected !== nextProps.selected) + //); } }); var FlowTableHead = React.createClass({ - render: function(){ - var columns = this.props.columns.map(function(column){ + render: function () { + var columns = this.props.columns.map(function (column) { return column.renderTitle(); }.bind(this)); - return <thead><tr>{columns}</tr></thead>; + return <thead> + <tr>{columns}</tr> + </thead>; } }); -var FlowTableBody = React.createClass({ - render: function(){ - var rows = this.props.flows.map(function(flow){ - var selected = (flow == this.props.selected); - return <FlowRow key={flow.id} - ref={flow.id} - flow={flow} - columns={this.props.columns} - selected={selected} - selectFlow={this.props.selectFlow} - />; - }.bind(this)); - return <tbody>{rows}</tbody>; - } -}); +var ROW_HEIGHT = 32; var FlowTable = React.createClass({ - mixins: [StickyHeadMixin, AutoScrollMixin], + mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin], getInitialState: function () { return { columns: all_columns }; }, - scrollIntoView: function(flow){ - // Now comes the fun part: Scroll the flow into the view. - var viewport = this.getDOMNode(); - var flowNode = this.refs.body.refs[flow.id].getDOMNode(); - var viewport_top = viewport.scrollTop; - var viewport_bottom = viewport_top + viewport.offsetHeight; - var flowNode_top = flowNode.offsetTop; - var flowNode_bottom = flowNode_top + flowNode.offsetHeight; - - // Account for pinned thead by pretending that the flowNode starts - // -thead_height pixel earlier. - flowNode_top -= this.refs.body.getDOMNode().offsetTop; - - if(flowNode_top < viewport_top){ - viewport.scrollTop = flowNode_top; - } else if(flowNode_bottom > viewport_bottom) { - viewport.scrollTop = flowNode_bottom - viewport.offsetHeight; + componentWillMount: function () { + if (this.props.view) { + this.props.view.addListener("add update remove recalculate", this.onChange); + } + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.view !== this.props.view) { + if (this.props.view) { + this.props.view.removeListener("add update remove recalculate"); + } + nextProps.view.addListener("add update remove recalculate", this.onChange); } }, + getDefaultProps: function () { + return { + rowHeight: ROW_HEIGHT + }; + }, + onScrollFlowTable: function () { + this.adjustHead(); + this.onScroll(); + }, + onChange: function () { + this.forceUpdate(); + }, + scrollIntoView: function (flow) { + this.scrollRowIntoView( + this.props.view.index(flow), + this.refs.body.getDOMNode().offsetTop + ); + }, + renderRow: function (flow) { + var selected = (flow === this.props.selected); + return <FlowRow key={flow.id} + ref={flow.id} + flow={flow} + columns={this.state.columns} + selected={selected} + selectFlow={this.props.selectFlow} + />; + }, render: function () { + //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); + var flows = this.props.view ? this.props.view.list : []; + + var rows = this.renderRows(flows); + return ( - <div className="flow-table" onScroll={this.adjustHead}> + <div className="flow-table" onScroll={this.onScrollFlowTable}> <table> <FlowTableHead ref="head" - columns={this.state.columns}/> - <FlowTableBody ref="body" - flows={this.props.flows} - selected={this.props.selected} - selectFlow={this.props.selectFlow} - columns={this.state.columns}/> + columns={this.state.columns}/> + <tbody ref="body"> + { this.getPlaceholderTop(flows.length) } + {rows} + { this.getPlaceholderBottom(flows.length) } + </tbody> </table> </div> - ); + ); } }); diff --git a/web/src/js/components/footer.jsx.js b/web/src/js/components/footer.jsx.js index 9bcbbc2a..73fadef2 100644 --- a/web/src/js/components/footer.jsx.js +++ b/web/src/js/components/footer.jsx.js @@ -1,5 +1,3 @@ -/** @jsx React.DOM */ - var Footer = React.createClass({ render: function () { var mode = this.props.settings.mode; @@ -7,6 +5,6 @@ var Footer = React.createClass({ <footer> {mode != "regular" ? <span className="label label-success">{mode} mode</span> : null} </footer> - ); + ); } }); diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js index 994bc759..a23afa9b 100644 --- a/web/src/js/components/header.jsx.js +++ b/web/src/js/components/header.jsx.js @@ -1,5 +1,3 @@ -/** @jsx React.DOM */ - var MainMenu = React.createClass({ statics: { title: "Traffic", @@ -10,14 +8,23 @@ var MainMenu = React.createClass({ showEventLog: !this.props.settings.showEventLog }); }, + clearFlows: function () { + $.post("/flows/clear"); + }, render: function () { return ( <div> <button className={"btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default")} onClick={this.toggleEventLog}> - <i className="fa fa-database"></i> Display Event Log + <i className="fa fa-database"></i> + Display Event Log + </button> + + <button className="btn btn-default" onClick={this.clearFlows}> + <i className="fa fa-eraser"></i> + Clear Flows </button> </div> - ); + ); } }); @@ -43,53 +50,125 @@ var ReportsMenu = React.createClass({ } }); +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(); + console.error("unimplemented: handleNewClick"); + }, + handleOpenClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleOpenClick"); + }, + handleSaveClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleSaveClick"); + }, + 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}> File </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> + </li> + <li> + <a href="#" onClick={this.handleSaveClick}> + <i className="fa fa-fw fa-save"></i> + Save + </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, ToolsMenu, ReportsMenu]; var Header = React.createClass({ + mixins: [ReactRouter.Navigation], getInitialState: function () { return { active: header_entries[0] }; }, - handleClick: function (active) { - ReactRouter.transitionTo(active.route); + handleClick: function (active, e) { + e.preventDefault(); + this.transitionTo(active.route); this.setState({active: active}); - return false; - }, - handleFileClick: function () { - console.log("File click"); }, render: function () { - var header = header_entries.map(function(entry, i){ + var header = header_entries.map(function (entry, i) { var classes = React.addons.classSet({ active: entry == this.state.active }); return ( - <a key={i} - href="#" - className={classes} - onClick={this.handleClick.bind(this, entry)} + <a key={i} + href="#" + className={classes} + onClick={this.handleClick.bind(this, entry)} > { entry.title} </a> - ); + ); }.bind(this)); - + return ( <header> <div className="title-bar"> mitmproxy { this.props.settings.version } </div> <nav className="nav-tabs nav-tabs-lg"> - <a href="#" className="special" onClick={this.handleFileClick}> File </a> + <FileMenu/> {header} </nav> <div className="menu"> <this.state.active settings={this.props.settings}/> </div> </header> - ); + ); } }); diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js index 795b8136..17a024ee 100644 --- a/web/src/js/components/mainview.jsx.js +++ b/web/src/js/components/mainview.jsx.js @@ -1,65 +1,86 @@ -/** @jsx React.DOM */ - var MainView = React.createClass({ - getInitialState: function() { + mixins: [ReactRouter.Navigation, ReactRouter.State], + getInitialState: function () { return { - flows: [], + flows: [] }; }, - componentDidMount: function () { - this.flowStore = FlowStore.getView(); - this.flowStore.addListener("change",this.onFlowChange); - }, - componentWillUnmount: function () { - this.flowStore.removeListener("change",this.onFlowChange); - this.flowStore.close(); + componentWillReceiveProps: function (nextProps) { + if (nextProps.flowStore !== this.props.flowStore) { + this.closeView(); + this.openView(nextProps.flowStore); + } }, - onFlowChange: function () { + openView: function (store) { + var view = new StoreView(store); this.setState({ - flows: this.flowStore.getAll() + view: view }); + + view.addListener("recalculate", this.onRecalculate); + view.addListener("add update remove", this.onUpdate); }, - selectDetailTab: function(panel) { - ReactRouter.replaceWith( - "flow", - { - flowId: this.props.params.flowId, - detailTab: panel - } - ); + onRecalculate: function(){ + this.forceUpdate(); + var selected = this.getSelected(); + if(selected){ + this.refs.flowTable.scrollIntoView(selected); + } + }, + onUpdate: function (flow) { + if (flow.id === this.getParams().flowId) { + this.forceUpdate(); + } + }, + closeView: function () { + this.state.view.close(); + }, + componentWillMount: function () { + this.openView(this.props.flowStore); + }, + componentWillUnmount: function () { + this.closeView(); }, - selectFlow: function(flow) { - if(flow){ - ReactRouter.replaceWith( - "flow", + selectFlow: function (flow) { + if (flow) { + this.replaceWith( + "flow", { flowId: flow.id, - detailTab: this.props.params.detailTab || "request" + detailTab: this.getParams().detailTab || "request" } ); this.refs.flowTable.scrollIntoView(flow); } else { - ReactRouter.replaceWith("flows"); + this.replaceWith("flows"); } }, - selectFlowRelative: function(i){ + selectFlowRelative: function (shift) { + var flows = this.state.view.list; var index; - if(!this.props.params.flowId){ - if(i > 0){ - index = this.state.flows.length-1; + if (!this.getParams().flowId) { + if (shift > 0) { + index = flows.length - 1; } else { index = 0; } } else { - index = _.findIndex(this.state.flows, function(f){ - return f.id === this.props.params.flowId; - }.bind(this)); - index = Math.min(Math.max(0, index+i), this.state.flows.length-1); + var currFlowId = this.getParams().flowId; + var i = flows.length; + while (i--) { + if (flows[i].id === currFlowId) { + index = i; + break; + } + } + index = Math.min( + Math.max(0, index + shift), + flows.length - 1); } - this.selectFlow(this.state.flows[index]); + this.selectFlow(flows[index]); }, - onKeyDown: function(e){ - switch(e.keyCode){ + onKeyDown: function (e) { + switch (e.keyCode) { case Key.K: case Key.UP: this.selectFlowRelative(-1); @@ -80,14 +101,14 @@ var MainView = React.createClass({ break; case Key.H: case Key.LEFT: - if(this.refs.flowDetails){ + if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; case Key.L: case Key.TAB: case Key.RIGHT: - if(this.refs.flowDetails){ + if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; @@ -95,19 +116,20 @@ var MainView = React.createClass({ console.debug("keydown", e.keyCode); return; } - return false; + e.preventDefault(); + }, + getSelected: function(){ + return this.props.flowStore.get(this.getParams().flowId); }, - render: function() { - var selected = _.find(this.state.flows, { id: this.props.params.flowId }); + render: function () { + var selected = this.getSelected(); var details; - if(selected){ - details = ( - <FlowDetail ref="flowDetails" - flow={selected} - selectTab={this.selectDetailTab} - active={this.props.params.detailTab}/> - ); + if (selected) { + details = [ + <Splitter key="splitter"/>, + <FlowDetail key="flowDetails" ref="flowDetails" flow={selected}/> + ]; } else { details = null; } @@ -115,10 +137,9 @@ var MainView = React.createClass({ return ( <div className="main-view" onKeyDown={this.onKeyDown} tabIndex="0"> <FlowTable ref="flowTable" - flows={this.state.flows} - selectFlow={this.selectFlow} - selected={selected} /> - { details ? <Splitter/> : null } + view={this.state.view} + selectFlow={this.selectFlow} + selected={selected} /> {details} </div> ); diff --git a/web/src/js/components/proxyapp.jsx.js b/web/src/js/components/proxyapp.jsx.js index ff6e8da1..20162ad1 100644 --- a/web/src/js/components/proxyapp.jsx.js +++ b/web/src/js/components/proxyapp.jsx.js @@ -1,5 +1,3 @@ -/** @jsx React.DOM */ - //TODO: Move out of here, just a stub. var Reports = React.createClass({ render: function () { @@ -10,45 +8,67 @@ var Reports = React.createClass({ var ProxyAppMain = React.createClass({ getInitialState: function () { - return { settings: SettingsStore.getAll() }; + var eventStore = new EventLogStore(); + var flowStore = new FlowStore(); + var settings = new SettingsStore(); + + // Default Settings before fetch + _.extend(settings.dict,{ + showEventLog: true + }); + return { + settings: settings, + flowStore: flowStore, + eventStore: eventStore + }; }, componentDidMount: function () { - SettingsStore.addListener("change", this.onSettingsChange); + this.state.settings.addListener("recalculate", this.onSettingsChange); }, componentWillUnmount: function () { - SettingsStore.removeListener("change", this.onSettingsChange); + this.state.settings.removeListener("recalculate", this.onSettingsChange); }, - onSettingsChange: function () { - this.setState({settings: SettingsStore.getAll()}); + onSettingsChange: function(){ + this.setState({ + settings: this.state.settings + }); }, render: function () { + + var eventlog; + if (this.state.settings.dict.showEventLog) { + eventlog = [ + <Splitter key="splitter" axis="y"/>, + <EventLog key="eventlog" eventStore={this.state.eventStore}/> + ]; + } else { + eventlog = null; + } + return ( <div id="container"> - <Header settings={this.state.settings}/> - <this.props.activeRouteHandler settings={this.state.settings}/> - {this.state.settings.showEventLog ? <Splitter axis="y"/> : null} - {this.state.settings.showEventLog ? <EventLog/> : null} - <Footer settings={this.state.settings}/> + <Header settings={this.state.settings.dict}/> + <RouteHandler settings={this.state.settings.dict} flowStore={this.state.flowStore}/> + {eventlog} + <Footer settings={this.state.settings.dict}/> </div> - ); + ); } }); -var Routes = ReactRouter.Routes; var Route = ReactRouter.Route; +var RouteHandler = ReactRouter.RouteHandler; var Redirect = ReactRouter.Redirect; var DefaultRoute = ReactRouter.DefaultRoute; var NotFoundRoute = ReactRouter.NotFoundRoute; -var ProxyApp = ( - <Routes location="hash"> - <Route path="/" handler={ProxyAppMain}> - <Route name="flows" path="flows" handler={MainView}/> - <Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/> - <Route name="reports" handler={Reports}/> - <Redirect path="/" to="flows" /> - </Route> - </Routes> - );
\ No newline at end of file +var routes = ( + <Route path="/" handler={ProxyAppMain}> + <Route name="flows" path="flows" handler={MainView}/> + <Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/> + <Route name="reports" handler={Reports}/> + <Redirect path="/" to="flows" /> + </Route> +);
\ No newline at end of file diff --git a/web/src/js/components/utils.jsx.js b/web/src/js/components/utils.jsx.js index 91cb8458..81ba6b4d 100644 --- a/web/src/js/components/utils.jsx.js +++ b/web/src/js/components/utils.jsx.js @@ -1,5 +1,3 @@ -/** @jsx React.DOM */ - //React utils. For other utilities, see ../utils.js var Splitter = React.createClass({ @@ -8,85 +6,93 @@ var Splitter = React.createClass({ axis: "x" }; }, - getInitialState: function(){ + getInitialState: function () { return { applied: false, startX: false, startY: false }; }, - onMouseDown: function(e){ + onMouseDown: function (e) { this.setState({ startX: e.pageX, startY: e.pageY }); - window.addEventListener("mousemove",this.onMouseMove); - window.addEventListener("mouseup",this.onMouseUp); + window.addEventListener("mousemove", this.onMouseMove); + window.addEventListener("mouseup", this.onMouseUp); // Occasionally, only a dragEnd event is triggered, but no mouseUp. - window.addEventListener("dragend",this.onDragEnd); + window.addEventListener("dragend", this.onDragEnd); }, - onDragEnd: function(){ - this.getDOMNode().style.transform=""; - window.removeEventListener("dragend",this.onDragEnd); - window.removeEventListener("mouseup",this.onMouseUp); - window.removeEventListener("mousemove",this.onMouseMove); + onDragEnd: function () { + this.getDOMNode().style.transform = ""; + window.removeEventListener("dragend", this.onDragEnd); + window.removeEventListener("mouseup", this.onMouseUp); + window.removeEventListener("mousemove", this.onMouseMove); }, - onMouseUp: function(e){ + onMouseUp: function (e) { this.onDragEnd(); var node = this.getDOMNode(); var prev = node.previousElementSibling; var next = node.nextElementSibling; - var dX = e.pageX-this.state.startX; - var dY = e.pageY-this.state.startY; + var dX = e.pageX - this.state.startX; + var dY = e.pageY - this.state.startY; var flexBasis; - if(this.props.axis === "x"){ + if (this.props.axis === "x") { flexBasis = prev.offsetWidth + dX; } else { flexBasis = prev.offsetHeight + dY; } - prev.style.flex = "0 0 "+Math.max(0, flexBasis)+"px"; + prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px"; next.style.flex = "1 1 auto"; this.setState({ applied: true }); + this.onResize(); }, - onMouseMove: function(e){ + onMouseMove: function (e) { var dX = 0, dY = 0; - if(this.props.axis === "x"){ - dX = e.pageX-this.state.startX; + if (this.props.axis === "x") { + dX = e.pageX - this.state.startX; } else { - dY = e.pageY-this.state.startY; + dY = e.pageY - this.state.startY; } - this.getDOMNode().style.transform = "translate("+dX+"px,"+dY+"px)"; + this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; + }, + onResize: function () { + // Trigger a global resize event. This notifies components that employ virtual scrolling + // that their viewport may have changed. + window.setTimeout(function () { + window.dispatchEvent(new CustomEvent("resize")); + }, 1); }, - reset: function(willUnmount) { + reset: function (willUnmount) { if (!this.state.applied) { return; } var node = this.getDOMNode(); var prev = node.previousElementSibling; var next = node.nextElementSibling; - + prev.style.flex = ""; next.style.flex = ""; - if(!willUnmount){ + if (!willUnmount) { this.setState({ applied: false }); } - + this.onResize(); }, - componentWillUnmount: function(){ + componentWillUnmount: function () { this.reset(true); }, - render: function(){ + render: function () { var className = "splitter"; - if(this.props.axis === "x"){ + if (this.props.axis === "x") { className += " splitter-x"; } else { className += " splitter-y"; @@ -97,4 +103,21 @@ var Splitter = React.createClass({ </div> ); } +}); + +function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; +} +var xsrf = $.param({_xsrf: getCookie("_xsrf")}); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function (options) { + if (options.type === "post" && options.url[0] === "/") { + if (options.data) { + options.data += ("&" + xsrf); + } else { + options.data = xsrf; + } + } });
\ No newline at end of file diff --git a/web/src/js/components/virtualscroll.jsx.js b/web/src/js/components/virtualscroll.jsx.js new file mode 100644 index 00000000..4f946cb4 --- /dev/null +++ b/web/src/js/components/virtualscroll.jsx.js @@ -0,0 +1,81 @@ +var VirtualScrollMixin = { + getInitialState: function () { + return { + start: 0, + stop: 0 + }; + }, + componentWillMount: function () { + if (!this.props.rowHeight) { + console.warn("VirtualScrollMixin: No rowHeight specified", this); + } + }, + getPlaceholderTop: function (total) { + var Tag = this.props.placeholderTagName || "tr"; + // When a large trunk of elements is removed from the button, start may be far off the viewport. + // To make this issue less severe, limit the top placeholder to the total number of rows. + var style = { + height: Math.min(this.state.start, total) * this.props.rowHeight + }; + var spacer = <Tag key="placeholder-top" style={style}></Tag>; + + if (this.state.start % 2 === 1) { + // fix even/odd rows + return [spacer, <Tag key="placeholder-top-2"></Tag>]; + } else { + return spacer; + } + }, + getPlaceholderBottom: function (total) { + var Tag = this.props.placeholderTagName || "tr"; + var style = { + height: Math.max(0, total - this.state.stop) * this.props.rowHeight + }; + return <Tag key="placeholder-bottom" style={style}></Tag>; + }, + componentDidMount: function () { + this.onScroll(); + window.addEventListener('resize', this.onScroll); + }, + componentWillUnmount: function(){ + window.removeEventListener('resize', this.onScroll); + }, + onScroll: function () { + var viewport = this.getDOMNode(); + var top = viewport.scrollTop; + var height = viewport.offsetHeight; + var start = Math.floor(top / this.props.rowHeight); + var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight)); + + this.setState({ + start: start, + stop: stop + }); + }, + renderRows: function (elems) { + var rows = []; + var max = Math.min(elems.length, this.state.stop); + + for (var i = this.state.start; i < max; i++) { + var elem = elems[i]; + rows.push(this.renderRow(elem)); + } + return rows; + }, + scrollRowIntoView: function (index, head_height) { + + var row_top = (index * this.props.rowHeight) + head_height; + var row_bottom = row_top + this.props.rowHeight; + + var viewport = this.getDOMNode(); + var viewport_top = viewport.scrollTop; + var viewport_bottom = viewport_top + viewport.offsetHeight; + + // Account for pinned thead + if (row_top - head_height < viewport_top) { + viewport.scrollTop = row_top - head_height; + } else if (row_bottom > viewport_bottom) { + viewport.scrollTop = row_bottom - viewport.offsetHeight; + } + }, +};
\ No newline at end of file diff --git a/web/src/js/connection.js b/web/src/js/connection.js index 3edbfc20..6ca353b3 100644 --- a/web/src/js/connection.js +++ b/web/src/js/connection.js @@ -1,33 +1,24 @@ -function _Connection(url) { - this.url = url; -} -_Connection.prototype.init = function () { - this.openWebSocketConnection(); -}; -_Connection.prototype.openWebSocketConnection = function () { - this.ws = new WebSocket(this.url.replace("http", "ws")); - var ws = this.ws; +function Connection(url) { - ws.onopen = this.onopen.bind(this); - ws.onmessage = this.onmessage.bind(this); - ws.onerror = this.onerror.bind(this); - ws.onclose = this.onclose.bind(this); -}; -_Connection.prototype.onopen = function (open) { - console.debug("onopen", this, arguments); -}; -_Connection.prototype.onmessage = function (message) { - //AppDispatcher.dispatchServerAction(...); - var m = JSON.parse(message.data); - AppDispatcher.dispatchServerAction(m); -}; -_Connection.prototype.onerror = function (error) { - EventLogActions.add_event("WebSocket Connection Error."); - console.debug("onerror", this, arguments); -}; -_Connection.prototype.onclose = function (close) { - EventLogActions.add_event("WebSocket Connection closed."); - console.debug("onclose", this, arguments); -}; + if (url[0] === "/") { + url = location.origin.replace("http", "ws") + url; + } -var Connection = new _Connection(location.origin + "/updates"); + var ws = new WebSocket(url); + ws.onopen = function () { + ConnectionActions.open(); + }; + ws.onmessage = function (message) { + var m = JSON.parse(message.data); + AppDispatcher.dispatchServerAction(m); + }; + ws.onerror = function () { + ConnectionActions.error(); + EventLogActions.add_event("WebSocket connection error."); + }; + ws.onclose = function () { + ConnectionActions.close(); + EventLogActions.add_event("WebSocket connection closed."); + }; + return ws; +}
\ No newline at end of file diff --git a/web/src/js/dispatcher.js b/web/src/js/dispatcher.js index 4fe23447..860ade9f 100644 --- a/web/src/js/dispatcher.js +++ b/web/src/js/dispatcher.js @@ -11,14 +11,14 @@ Dispatcher.prototype.register = function (callback) { this.callbacks.push(callback); }; Dispatcher.prototype.unregister = function (callback) { - var index = this.callbacks.indexOf(f); + var index = this.callbacks.indexOf(callback); if (index >= 0) { - this.callbacks.splice(this.callbacks.indexOf(f), 1); + this.callbacks.splice(index, 1); } }; Dispatcher.prototype.dispatch = function (payload) { console.debug("dispatch", payload); - for(var i = 0; i < this.callbacks.length; i++){ + for (var i = 0; i < this.callbacks.length; i++) { this.callbacks[i](payload); } }; diff --git a/web/src/js/filt/filt.pegjs b/web/src/js/filt/filt.pegjs new file mode 100644 index 00000000..06e2ecb3 --- /dev/null +++ b/web/src/js/filt/filt.pegjs @@ -0,0 +1,93 @@ +// PEG.js filter rules - see http://pegjs.majda.cz/online + +/* Explain Filter */ +{ + var or = function(first, second) { + return first + " or " + second; + }; + var and = function(first, second) { + return first + " and " + second; + }; + var not = function(expr) { + return "not " + expr; + }; + var binding = function(expr) { + return "(" + expr + ")"; + } + var assetFilter = "is asset"; + var trueFilter = true; + var falseFilter = false; + var bodyFilter = function(s) { + return "body ~= '" + s + "'"; + } + var urlFilter = function(s) { + return "url ~= '" + s + "'"; + } +} + +start "filter expression" + = __ orExpr:OrExpr __ { return orExpr; } + +ws "whitespace" = [ \t\n\r] +cc "control character" = [|&!()~"] +__ "optional whitespace" = ws* + +OrExpr + = first:AndExpr __ "|" __ second:OrExpr + { return or(first, second); } + / AndExpr + +AndExpr + = first:NotExpr __ "&" __ second:AndExpr + { return and(first, second); } + / first:NotExpr ws+ second:AndExpr + { return and(first, second); } + / NotExpr + +NotExpr + = "!" __ expr:NotExpr + { return not(expr); } + / BindingExpr + +BindingExpr + = "(" __ expr:OrExpr __ ")" + { return binding(orExpr); } + / Expr + +Expr + = NullaryExpr + / UnaryExpr + +NullaryExpr + = BooleanLiteral + / "~a" { return assetFilter; }; + +BooleanLiteral + = "true" { return trueFilter; } + / "false" { return falseFilter; } + +UnaryExpr + = "~b" ws+ s:StringLiteral { return bodyFilter(s); } + / s:StringLiteral { return urlFilter(s); } + +StringLiteral "string" + = '"' chars:DoubleStringChar* '"' { return chars.join(""); } + / "'" chars:SingleStringChar* "'" { return chars.join(""); } + / !cc chars:UnquotedStringChar+ { return chars.join(""); } + +DoubleStringChar + = !["\\] char:. { return char; } + / "\\" char:EscapeSequence { return char; } + +SingleStringChar + = !['\\] char:. { return char; } + / "\\" char:EscapeSequence { return char; } + +UnquotedStringChar + = !ws char:. { return char; } + +EscapeSequence + = ['"\\] + / "n" { return "\n"; } + / "r" { return "\r"; } + / "t" { return "\t"; }
\ No newline at end of file diff --git a/web/src/js/store/settingstore.js b/web/src/js/store/settingstore.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/web/src/js/store/settingstore.js diff --git a/web/src/js/store/store.js b/web/src/js/store/store.js new file mode 100644 index 00000000..0f94e496 --- /dev/null +++ b/web/src/js/store/store.js @@ -0,0 +1,164 @@ +function ListStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(ListStore.prototype, EventEmitter.prototype, { + add: function (elem) { + if (elem.id in this._pos_map) { + return; + } + this._pos_map[elem.id] = this.list.length; + this.list.push(elem); + this.emit("add", elem); + }, + update: function (elem) { + if (!(elem.id in this._pos_map)) { + return; + } + this.list[this._pos_map[elem.id]] = elem; + this.emit("update", elem); + }, + remove: function (elem_id) { + if (!(elem.id in this._pos_map)) { + return; + } + this.list.splice(this._pos_map[elem_id], 1); + this._build_map(); + this.emit("remove", elem_id); + }, + reset: function (elems) { + this.list = elems || []; + this._build_map(); + this.emit("recalculate", this.list); + }, + _build_map: function () { + this._pos_map = {}; + for (var i = 0; i < this.list.length; i++) { + var elem = this.list[i]; + this._pos_map[elem.id] = i; + } + }, + get: function (elem_id) { + return this.list[this._pos_map[elem_id]]; + }, + index: function (elem_id) { + return this._pos_map[elem_id]; + } +}); + + +function DictStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(DictStore.prototype, EventEmitter.prototype, { + update: function (dict) { + _.merge(this.dict, dict); + this.emit("recalculate", this.dict); + }, + reset: function (dict) { + this.dict = dict || {}; + this.emit("recalculate", this.dict); + } +}); + +function LiveStoreMixin(type) { + this.type = type; + + this._updates_before_fetch = undefined; + this._fetchxhr = false; + + this.handle = this.handle.bind(this); + AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } +} +_.extend(LiveStoreMixin.prototype, { + handle: function (event) { + if (event.type === ActionTypes.CONNECTION_OPEN) { + return this.fetch(); + } + if (event.type === this.type) { + if (event.cmd === StoreCmds.RESET) { + this.fetch(); + } else if (this._updates_before_fetch) { + console.log("defer update", event); + this._updates_before_fetch.push(event); + } else { + this[event.cmd](event.data); + } + } + }, + close: function () { + AppDispatcher.unregister(this.handle); + }, + fetch: function (data) { + console.log("fetch " + this.type); + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._updates_before_fetch = []; // (JS: empty array is true) + if (data) { + this.handle_fetch(data); + } else { + this._fetchxhr = $.getJSON("/" + this.type) + .done(function (message) { + this.handle_fetch(message.data); + }.bind(this)) + .fail(function () { + EventLogActions.add_event("Could not fetch " + this.type); + }.bind(this)); + } + }, + handle_fetch: function (data) { + this._fetchxhr = false; + console.log(this.type + " fetched.", this._updates_before_fetch); + this.reset(data); + var updates = this._updates_before_fetch; + this._updates_before_fetch = false; + for (var i = 0; i < updates.length; i++) { + this.handle(updates[i]); + } + }, +}); + +function LiveListStore(type) { + ListStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype); + +function LiveDictStore(type) { + DictStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype); + + +function FlowStore() { + return new LiveListStore(ActionTypes.FLOW_STORE); +} + +function SettingsStore() { + return new LiveDictStore(ActionTypes.SETTINGS_STORE); +} + +function EventLogStore() { + LiveListStore.call(this, ActionTypes.EVENT_STORE); +} +_.extend(EventLogStore.prototype, LiveListStore.prototype, { + fetch: function(){ + LiveListStore.prototype.fetch.apply(this, arguments); + + // Make sure to display updates even if fetching all events failed. + // This way, we can send "fetch failed" log messages to the log. + if(this._fetchxhr){ + this._fetchxhr.fail(function(){ + this.handle_fetch(null); + }.bind(this)); + } + } +});
\ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js new file mode 100644 index 00000000..56bc4dbd --- /dev/null +++ b/web/src/js/store/view.js @@ -0,0 +1,99 @@ +function SortByStoreOrder(elem) { + return this.store.index(elem.id); +} + +var default_sort = SortByStoreOrder; +var default_filt = function(elem){ + return true; +}; + +function StoreView(store, filt, sortfun) { + EventEmitter.call(this); + filt = filt || default_filt; + sortfun = sortfun || default_sort; + + this.store = store; + + this.add = this.add.bind(this); + this.update = this.update.bind(this); + this.remove = this.remove.bind(this); + this.recalculate = this.recalculate.bind(this); + this.store.addListener("add", this.add); + this.store.addListener("update", this.update); + this.store.addListener("remove", this.remove); + this.store.addListener("recalculate", this.recalculate); + + this.recalculate(this.store.list, filt, sortfun); +} + +_.extend(StoreView.prototype, EventEmitter.prototype, { + close: function () { + this.store.removeListener("add", this.add); + this.store.removeListener("update", this.update); + this.store.removeListener("remove", this.remove); + this.store.removeListener("recalculate", this.recalculate); + }, + recalculate: function (elems, filt, sortfun) { + if (filt) { + this.filt = filt; + } + if (sortfun) { + this.sortfun = sortfun.bind(this); + } + + this.list = elems.filter(this.filt); + this.list.sort(function (a, b) { + return this.sortfun(a) - this.sortfun(b); + }.bind(this)); + this.emit("recalculate"); + }, + index: function (elem) { + return _.sortedIndex(this.list, elem, this.sortfun); + }, + add: function (elem) { + if (this.filt(elem)) { + var idx = this.index(elem); + if (idx === this.list.length) { //happens often, .push is way faster. + this.list.push(elem); + } else { + this.list.splice(idx, 0, elem); + } + this.emit("add", elem, idx); + } + }, + update: function (elem) { + var idx; + var i = this.list.length; + // Search from the back, we usually update the latest entries. + while (i--) { + if (this.list[i].id === elem.id) { + idx = i; + break; + } + } + + if (idx === -1) { //not contained in list + this.add(elem); + } else if (!this.filt(elem)) { + this.remove(elem.id); + } else { + if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed + this.remove(this.list[idx]); + this.add(elem); + } else { + this.list[idx] = elem; + this.emit("update", elem, idx); + } + } + }, + remove: function (elem_id) { + var idx = this.list.length; + while (idx--) { + if (this.list[idx].id === elem_id) { + this.list.splice(idx, 1); + this.emit("remove", elem_id, idx); + break; + } + } + } +});
\ No newline at end of file diff --git a/web/src/js/stores/base.js b/web/src/js/stores/base.js deleted file mode 100644 index 952fa847..00000000 --- a/web/src/js/stores/base.js +++ /dev/null @@ -1,25 +0,0 @@ -function EventEmitter() { - this.listeners = {}; -} -EventEmitter.prototype.emit = function (event) { - if (!(event in this.listeners)) { - return; - } - var args = Array.prototype.slice.call(arguments, 1); - this.listeners[event].forEach(function (listener) { - listener.apply(this, args); - }.bind(this)); -}; -EventEmitter.prototype.addListener = function (event, f) { - this.listeners[event] = this.listeners[event] || []; - this.listeners[event].push(f); -}; -EventEmitter.prototype.removeListener = function (event, f) { - if (!(event in this.listeners)) { - return false; - } - var index = this.listeners[event].indexOf(f); - if (index >= 0) { - this.listeners[event].splice(index, 1); - } -}; diff --git a/web/src/js/stores/eventlogstore.js b/web/src/js/stores/eventlogstore.js deleted file mode 100644 index e356959a..00000000 --- a/web/src/js/stores/eventlogstore.js +++ /dev/null @@ -1,99 +0,0 @@ -// -// We have an EventLogView and an EventLogStore: -// The basic architecture is that one can request views on the event log -// from the store, which returns a view object and then deals with getting the data required for the view. -// The view object is accessed by React components and distributes updates etc. -// -// See also: components/EventLog.react.js -function EventLogView(store, live) { - EventEmitter.call(this); - this._store = store; - this.live = live; - this.log = []; - - this.add = this.add.bind(this); - - if (live) { - this._store.addListener(ActionTypes.ADD_EVENT, this.add); - } -} -_.extend(EventLogView.prototype, EventEmitter.prototype, { - close: function () { - this._store.removeListener(ActionTypes.ADD_EVENT, this.add); - }, - getAll: function () { - return this.log; - }, - add: function (entry) { - this.log.push(entry); - if(this.log.length > 200){ - this.log.shift(); - } - this.emit("change"); - }, - add_bulk: function (messages) { - var log = messages; - var last_id = log[log.length - 1].id; - var to_add = _.filter(this.log, function (entry) { - return entry.id > last_id; - }); - this.log = log.concat(to_add); - this.emit("change"); - } -}); - - -function _EventLogStore() { - EventEmitter.call(this); -} -_.extend(_EventLogStore.prototype, EventEmitter.prototype, { - getView: function (since) { - var view = new EventLogView(this, !since); - return view; - /* - //TODO: Really do bulk retrieval of last messages. - window.setTimeout(function () { - view.add_bulk([ - { - id: 1, - message: "Hello World" - }, - { - id: 2, - message: "I was already transmitted as an event." - } - ]); - }, 100); - - var id = 2; - view.add({ - id: id++, - message: "I was already transmitted as an event." - }); - view.add({ - id: id++, - message: "I was only transmitted as an event before the bulk was added.." - }); - window.setInterval(function () { - view.add({ - id: id++, - message: "." - }); - }, 1000); - return view; - */ - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.ADD_EVENT: - this.emit(ActionTypes.ADD_EVENT, action.data); - break; - default: - return; - } - } -}); - - -var EventLogStore = new _EventLogStore(); -AppDispatcher.register(EventLogStore.handle.bind(EventLogStore));
\ No newline at end of file diff --git a/web/src/js/stores/flowstore.js b/web/src/js/stores/flowstore.js deleted file mode 100644 index 7c0bddbd..00000000 --- a/web/src/js/stores/flowstore.js +++ /dev/null @@ -1,91 +0,0 @@ -function FlowView(store, live) { - EventEmitter.call(this); - this._store = store; - this.live = live; - this.flows = []; - - this.add = this.add.bind(this); - this.update = this.update.bind(this); - - if (live) { - this._store.addListener(ActionTypes.ADD_FLOW, this.add); - this._store.addListener(ActionTypes.UPDATE_FLOW, this.update); - } -} - -_.extend(FlowView.prototype, EventEmitter.prototype, { - close: function () { - this._store.removeListener(ActionTypes.ADD_FLOW, this.add); - this._store.removeListener(ActionTypes.UPDATE_FLOW, this.update); - }, - getAll: function () { - return this.flows; - }, - add: function (flow) { - return this.update(flow); - }, - add_bulk: function (flows) { - //Treat all previously received updates as newer than the bulk update. - //If they weren't newer, we're about to receive an update for them very soon. - var updates = this.flows; - this.flows = flows; - updates.forEach(function(flow){ - this._update(flow); - }.bind(this)); - this.emit("change"); - }, - _update: function(flow){ - var idx = _.findIndex(this.flows, function(f){ - return flow.id === f.id; - }); - - if(idx < 0){ - this.flows.push(flow); - //if(this.flows.length > 100){ - // this.flows.shift(); - //} - } else { - this.flows[idx] = flow; - } - }, - update: function(flow){ - this._update(flow); - this.emit("change"); - }, -}); - - -function _FlowStore() { - EventEmitter.call(this); -} -_.extend(_FlowStore.prototype, EventEmitter.prototype, { - getView: function (since) { - var view = new FlowView(this, !since); - - $.getJSON("/static/flows.json", function(flows){ - flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows)); - var id = 1; - flows.forEach(function(flow){ - flow.id = "uuid-" + id++; - }); - view.add_bulk(flows); - - }); - - return view; - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.ADD_FLOW: - case ActionTypes.UPDATE_FLOW: - this.emit(action.type, action.data); - break; - default: - return; - } - } -}); - - -var FlowStore = new _FlowStore(); -AppDispatcher.register(FlowStore.handle.bind(FlowStore)); diff --git a/web/src/js/stores/settingstore.js b/web/src/js/stores/settingstore.js deleted file mode 100644 index 7eef9b8f..00000000 --- a/web/src/js/stores/settingstore.js +++ /dev/null @@ -1,28 +0,0 @@ -function _SettingsStore() { - EventEmitter.call(this); - - //FIXME: What do we do if we haven't requested anything from the server yet? - this.settings = { - version: "0.12", - showEventLog: true, - mode: "transparent", - }; -} -_.extend(_SettingsStore.prototype, EventEmitter.prototype, { - getAll: function () { - return this.settings; - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.UPDATE_SETTINGS: - this.settings = action.settings; - this.emit("change"); - break; - default: - return; - } - } -}); - -var SettingsStore = new _SettingsStore(); -AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); diff --git a/web/src/js/utils.js b/web/src/js/utils.js index fa15db8c..8ae7aa54 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -12,6 +12,7 @@ var AutoScrollMixin = { }, }; + var StickyHeadMixin = { adjustHead: function () { // Abusing CSS transforms to set the element @@ -21,6 +22,7 @@ var StickyHeadMixin = { } }; + var Key = { UP: 38, DOWN: 40, @@ -38,17 +40,19 @@ var Key = { L: 76 }; + var formatSize = function (bytes) { var size = bytes; var prefix = ["B", "KB", "MB", "GB", "TB"]; - var i=0; - while (Math.abs(size) >= 1024 && i < prefix.length-1) { + var i = 0; + while (Math.abs(size) >= 1024 && i < prefix.length - 1) { i++; size = size / 1024; } return (Math.floor(size * 100) / 100.0).toFixed(2) + prefix[i]; }; + var formatTimeDelta = function (milliseconds) { var time = milliseconds; var prefix = ["ms", "s", "min", "h"]; @@ -59,4 +63,41 @@ var formatTimeDelta = function (milliseconds) { i++; } return Math.round(time) + prefix[i]; +}; + + +var formatTimeStamp = function (seconds) { + var ts = (new Date(seconds * 1000)).toISOString(); + return ts.replace("T", " ").replace("Z", ""); +}; + + +function EventEmitter() { + this.listeners = {}; +} +EventEmitter.prototype.emit = function (event) { + if (!(event in this.listeners)) { + return; + } + var args = Array.prototype.slice.call(arguments, 1); + this.listeners[event].forEach(function (listener) { + listener.apply(this, args); + }.bind(this)); +}; +EventEmitter.prototype.addListener = function (events, f) { + events.split(" ").forEach(function (event) { + this.listeners[event] = this.listeners[event] || []; + this.listeners[event].push(f); + }.bind(this)); +}; +EventEmitter.prototype.removeListener = function (events, f) { + if (!(events in this.listeners)) { + return false; + } + events.split(" ").forEach(function (event) { + var index = this.listeners[event].indexOf(f); + if (index >= 0) { + this.listeners[event].splice(index, 1); + } + }.bind(this)); };
\ No newline at end of file |