UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

1,259 lines (1,202 loc) 43 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint react:true, es6:true*/ import React from 'react'; import $ from 'jquery'; import moment from 'moment'; import classnames from 'classnames'; import {withRouter} from 'react-router-dom'; import {Waypoint} from 'react-waypoint'; import codemirror from 'codemirror/lib/codemirror'; import 'codemirror/lib/codemirror.css'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/htmlmixed/htmlmixed'; import Pure_component from '/www/util/pub/pure_component.js'; import zutil from '../../../util/util.js'; import setdb from '../../../util/setdb.js'; import {bytes_format, get_troubleshoot} from '../util.js'; import {Toolbar_button} from '../chrome_widgets.js'; import Tooltip from '../common/tooltip.js'; import {trigger_types, action_types} from '../../../util/rules_util.js'; import {Copy_btn} from '../common.js'; import './viewer.less'; export class Preview extends Pure_component { panes = [ {id: 'headers', width: 65, comp: Pane_headers}, {id: 'preview', width: 63, comp: Pane_preview}, {id: 'response', width: 72, comp: Pane_response}, {id: 'timing', width: 57, comp: Pane_timing}, {id: 'rules', width: 50, comp: Pane_rules}, {id: 'troubleshooting', width: 110, comp: Pane_troubleshoot}, ]; state = {cur_pane: 0}; select_pane = id=>{ this.setState({cur_pane: id}); }; componentDidMount(){ this.setdb_on('har_viewer.set_pane', pane=>{ if (pane===undefined) return; this.setState({cur_pane: pane}); }); } render(){ if (!this.props.cur_preview) return null; const Pane_content = this.panes[this.state.cur_pane].comp; const req = this.props.cur_preview; return <div style={this.props.style} className="har_preview chrome"> <div className="tabbed_pane_header"> <div className="left_pane"> <div onClick={this.props.close} className="close_btn_wrapper"> <div className="small_icon close_btn"/> <div className="medium_icon close_btn_h"/> </div> </div> <div className="right_panes"> {this.panes.map((p, idx)=> <Pane key={p.id} width={p.width} id={p.id} idx={idx} on_click={this.select_pane} active={this.state.cur_pane==idx} /> )} <Pane_slider panes={this.panes} cur_pane={this.state.cur_pane}/> </div> </div> <div className="tabbed_pane_content"> <Pane_content key={req.uuid} req={req}/> </div> </div>; } } const Pane = ({id, idx, width, on_click, active})=> <div onClick={()=>on_click(idx)} style={{width}} className={classnames('pane', id, {active})}> <span>{id}</span> </div>; const Pane_slider = ({panes, cur_pane})=>{ const slider_class = classnames('pane_slider'); const offset = panes.slice(0, cur_pane).reduce((acc, e)=>acc+e.width, 0); const slider_style = { width: panes[cur_pane].width, transform: `translateX(${offset+24}px)`, }; return <div className={slider_class} style={slider_style}/>; }; class Pane_headers extends Pure_component { get_curl = ()=>{ const req = this.props.req; const {username, password, super_proxy} = req.details; const headers = req.request.headers.map(h=>`-H "${h.name}: ` +`${h.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`); const proxy = super_proxy ? '-x '+(username||'')+':'+(password||'')+'@'+super_proxy : ''; const url = '"'+req.request.url+'"'; return ['curl', proxy, '-X', req.request.method, url, ...headers] .filter(Boolean).join(' '); }; render(){ const req = this.props.req; const general_entries = [ {name: 'Request URL', value: req.request.url}, {name: 'Request method', value: req.request.method}, {name: 'Status code', value: req.response.status}, {name: 'Super proxy IP', value: req.details.super_proxy}, {name: 'Peer proxy IP', value: req.details.proxy_peer}, {name: 'Username', value: req.details.username}, {name: 'Password', value: req.details.password}, {name: 'Sent from', value: req.details.remote_address}, ].filter(e=>e.value!==undefined); return <React.Fragment> <Copy_btn val={this.get_curl()} title="Copy as cURL" style={{position: 'absolute', right: 5, top: 5}} inner_style={{width: 'auto'}} /> <ol className="tree_outline"> <Preview_section title="General" pairs={general_entries}/> <Preview_section title="Response headers" pairs={req.response.headers}/> <Preview_section title="Request headers" pairs={req.request.headers}/> <Body_section title="Request body" body={req.request.postData && req.request.postData.text}/> </ol> </React.Fragment>; } } class Pane_response extends Pure_component { render(){ const req = this.props.req; const {port, content_type} = req.details; if (content_type=='unknown') return <Encrypted_response_data port={port}/>; if (!content_type||['xhr', 'css', 'js', 'font', 'html', 'other'] .includes(content_type)) { return <Codemirror_wrapper req={req}/>; } return <No_response_data/>; } } const Encrypted_response_data = withRouter( class Encrypted_response_data extends Pure_component { goto_ssl = ()=>{ this.props.history.push({ pathname: `/proxy/${this.props.port}`, state: {field: 'ssl'}, }); }; render(){ return <Pane_info> <div>This request is using SSL encryption.</div> <div> <span>You need to turn on </span> <a className="link" onClick={this.goto_ssl}> SSL analyzing</a> <span> to read the response here.</span> </div> </Pane_info>; } }); const Pane_info = ({children})=> <div className="empty_view"> <div className="block">{children}</div> </div>; const No_response_data = ()=> <div className="empty_view"> <div className="block">This request has no response data available.</div> </div>; class Codemirror_wrapper extends Pure_component { componentDidMount(){ this.cm = codemirror.fromTextArea(this.textarea, { readOnly: true, lineNumbers: true, }); this.cm.setSize('100%', '100%'); let text = this.props.req.response.content.text||''; try { text = JSON.stringify(JSON.parse(text), null, '\t'); } catch(e){} this.cm.doc.setValue(text); this.set_ct(); } componentDidUpdate(){ this.cm.doc.setValue(this.props.req.response.content.text||''); this.set_ct(); } set_ct(){ const content_type = this.props.req.details.content_type; let mode; if (!content_type||content_type=='xhr') mode = 'javascript'; if (content_type=='html') mode = 'htmlmixed'; this.cm.setOption('mode', mode); } set_textarea = ref=>{ this.textarea = ref; }; render(){ return <div className="codemirror_wrapper"> <textarea ref={this.set_textarea}/> </div>; } } class Body_section extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ if (!this.props.body) return null; let json; let raw_body; try { json = JSON.parse(this.props.body); } catch(e){ raw_body = this.props.body; } return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {this.props.title} </li>, <ol key="ol" className={classnames('children', {open: this.state.open})}> {!!json && <JSON_viewer json={json}/>} {!!raw_body && <Header_pair name="raw-data" value={raw_body}/>} </ol>, ]; } } class Preview_section extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ if (!this.props.pairs||!this.props.pairs.length) return null; return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {this.props.title} {!this.state.open ? ` (${this.props.pairs.length})` : ''} </li>, <ol key="ol" className={classnames('children', {open: this.state.open})}> {this.props.pairs.map(p=> <Header_pair key={p.name} name={p.name} value={p.value}/> )} </ol>, ]; } } const Header_pair = ({name, value})=>{ if (name=='Status code') value = <Status_value value={value}/>; return <li className="treeitem"> <div className="header_name">{name}: </div> <div className="header_value">{value}</div> </li>; }; const Status_value = ({value})=>{ const info = value=='unknown'; const green = /2../.test(value); const yellow = /3../.test(value); const red = /(canceled)|([45]..)/.test(value); const classes = classnames('small_icon', 'status', { info, green, yellow, red}); return <div className="status_wrapper"> <div className={classes}/>{value} </div>; }; const Pane_rules = withRouter(class Pane_rules extends Pure_component { goto_ssl = ()=>{ this.props.history.push({ pathname: `/proxy/${this.props.req.details.port}`, state: {field: 'trigger_type'}, }); }; render(){ const {details: {rules}} = this.props.req; if (!rules || !rules.length) { return <Pane_info> <div> <span>No rules have been triggered on this request. </span> <a className="link" onClick={this.goto_ssl}> Configure Rules</a> </div> </Pane_info>; } return <div className="rules_view_wrapper"> <ol className="tree_outline"> {rules.map((r, idx)=> <Rule_preview key={idx} rule={r} idx={idx+1}/> )} </ol> </div>; } }); class Rule_preview extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ const {rule, idx} = this.props; const children_classes = classnames('children', 'timeline', {open: this.state.open}); const first_trigger = trigger_types.find(t=>rule[t.value])||{}; return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {idx}. {first_trigger.key} </li>, <ol key="ol" className={children_classes}> <Trigger_section rule={rule}/> <Action_section actions={rule.action}/> </ol>, ]; } } const Trigger_section = ({rule})=> <div className="trigger_section"> {trigger_types.map(t=><Trigger key={t.value} type={t} rule={rule}/>)} </div>; const Trigger = ({type, rule})=>{ if (!rule[type.value]) return null; return <div className="trigger"> {type.key}: {rule[type.value]} </div>; }; const Action_section = ({actions})=> <div className="action_section"> {Object.keys(actions).map(a=> <Action key={a} action={a} value={actions[a]}/> )} </div>; const Action = ({action, value})=>{ const key = (action_types.find(a=>a.value==action)||{}).key; const val = action=='request_url' ? value&&value.url : value; return <div className="action"> {key} {val ? `: ${val}` : ''} </div>; }; class Pane_troubleshoot extends Pure_component { render(){ const response = this.props.req.response; const troubleshoot = get_troubleshoot(response.content.text, response.status, response.headers); if (troubleshoot.title) { return <div className="timing_view_wrapper"> <ol className="tree_outline"> <li key="li" onClick={this.toggle} className="parent_title expandable open"> {troubleshoot.title} </li> <ol>{troubleshoot.info}</ol> </ol> </div>; } return <Pane_info> <div>There's not troubleshooting for this request.</div> </Pane_info>; } } class Pane_timing extends Pure_component { state = {}; componentDidMount(){ this.setdb_on('head.recent_stats', stats=>this.setState({stats})); } render(){ const {startedDateTime} = this.props.req; const started_at = moment(new Date(startedDateTime)).format( 'YYYY-MM-DD HH:mm:ss'); return <div className="timing_view_wrapper"> <div className="timeline_info">Started at {started_at}</div> <ol className="tree_outline"> {this.props.req.details.timeline.map((timeline, idx)=> <Single_timeline key={idx} timeline={timeline} time={this.props.req.time} req={this.props.req}/> )} </ol> <div className="timeline_info total"> Total: {this.props.req.time} ms</div> {this.props.req.request.url.endsWith('443') && this.state.stats && this.state.stats.ssl_enable && <Enable_https port={this.props.req.details.port}/> } </div>; } } class Single_timeline extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ const sections = ['Resource Scheduling', 'Request/Response']; const perc = [ {label: 'Queueing', id: 'blocked', section: 0}, {label: 'Connected', id: 'wait', section: 1}, {label: 'Time to first byte', id: 'ttfb', section: 1}, {label: 'Response', id: 'receive', section: 1}, ].reduce((acc, el)=>{ const cur_time = this.props.timeline[el.id]; const left = acc.offset; const dur = Number((cur_time/this.props.time).toFixed(4)); const right = 1-acc.offset-dur; return {offset: acc.offset+dur, data: [...acc.data, {...el, left: `${left*100}%`, right: `${right*100}%`}]}; }, {offset: 0, data: []}).data .reduce((acc, el)=>{ if (el.section!=acc.last_section) return {last_section: el.section, data: [...acc.data, [el]]}; return { last_section: el.section, data: [ ...acc.data.slice(0, -1), [...acc.data.slice(-1)[0], el], ], }; }, {last_section: -1, data: []}).data; const children_classes = classnames('children', 'timeline', {open: this.state.open}); const {timeline} = this.props; return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {timeline.port} </li>, <ol key="ol" className={children_classes}> <table> <colgroup> <col className="labels"/> <col className="bars"/> <col className="duration"/> </colgroup> <tbody> {perc.map((s, i)=> <Timing_header key={i} title={sections[s[0].section]}> {s.map(p=> <Timing_row title={p.label} id={p.id} left={p.left} key={p.id} right={p.right} time={this.props.timeline[p.id]}/> )} </Timing_header> )} </tbody> </table> </ol>, ]; } } const Timing_header = ({title, children})=>[ <tr key="timing_header" className="table_header"> <td>{title}</td> <td></td> <td>TIME</td> </tr>, ...children, ]; const Timing_row = ({title, id, left, right, time})=> <tr className="timing_row"> <td>{title}</td> <td> <div className="timing_bar_wrapper"> <span className={classnames('timing_bar', id)} style={{left, right}}> &#8203;</span> </div> </td> <td><div className="timing_bar_title">{time} ms</div></td> </tr>; const Enable_https = withRouter(props=>{ const click = ()=>{ props.history.push({ pathname: `/proxy/${props.port}`, state: {field: 'ssl'}, }); }; return <div className="footer_link"> <a className="devtools_link" role="link" tabIndex="0" target="_blank" rel="noopener noreferrer" onClick={click} style={{display: 'inline', cursor: 'pointer'}} > Enable HTTPS logging </a> to view this timeline </div>; }); const is_json_str = str=>{ let resp; try { resp = JSON.parse(str); } catch(e){ return false; } return resp; }; class Pane_preview extends Pure_component { render(){ const content_type = this.props.req.details.content_type; const text = this.props.req.response.content.text; const port = this.props.req.details.port; let json; if (content_type=='unknown') return <Encrypted_response_data port={port}/>; if (content_type=='xhr' && (json = is_json_str(text))) return <JSON_viewer json={json}/>; if (content_type=='img') return <Img_viewer img={this.props.req.request.url}/>; if (content_type=='html') return <Codemirror_wrapper req={this.props.req}/>; return <div className="pane_preview"></div>; } } const Img_viewer = ({img})=> <div className="img_viewer"> <div className="image"> <img src={img}/> </div> </div>; const has_children = o=>!!o && typeof o=='object' && Object.keys(o).length; const JSON_viewer = ({json})=> <div className="json_viewer"> <ol className="tree_root"> <Pair open val={json}/> </ol> </div>; const Children = ({val, expanded})=>{ if (has_children(val) && expanded) { return <ol className="tree_children"> {Object.entries(val).map(e=> <Pair key={e[0]} label={e[0]} val={e[1]}/> )} </ol>; } return null; }; class Pair extends React.PureComponent { state = {expanded: this.props.open}; toggle = ()=>{ this.setState(prev=>({expanded: !prev.expanded})); }; render(){ const {label, val} = this.props; return [ <Tree_item expanded={this.state.expanded} label={label} val={val} toggle={this.toggle} key="tree_item"/>, <Children val={val} expanded={this.state.expanded} key="val"/>, ]; } } const Tree_item = ({label, val, expanded, toggle})=>{ const classes = classnames('tree_item', { parent: has_children(val), expanded, }); return <li className={classes} onClick={toggle}> {label ? [ <span key="name" className="name">{label}</span>, <span key="separator" className="separator">: </span> ] : null} <Value val={val} expanded={expanded}/> </li>; }; const Value = ({val})=>{ if (typeof val=='object') return <Value_object val={val}/>; else if (typeof val=='number') return <span className="value number">{val}</span>; else if (typeof val=='boolean') return <span className="value boolean">{val.toString()}</span>; else if (typeof val=='string') return <span className="value string">"{val}"</span>; else if (typeof val=='undefined') return <span className="value undefined">"{val}"</span>; else if (typeof val=='function') return null; }; const Value_object = ({val})=>{ if (val===null) return <span className="value null">null</span>; if (Array.isArray(val)) { if (!val.length) return <span className="value array empty">[]</span>; return <span className="value array long">[,...]</span>; } if (!Object.keys(val).length) return <span className="value object empty">{'{}'}</span>; return <span className="value object">{JSON.stringify(val)}</span>; }; const with_resizable_cols = Table=>{ class Resizable extends React.PureComponent { constructor(props){ super(props); this.state = {}; this.cols = zutil.clone_deep(this.props.table_cols); this.min_width = 22; this.moving_col = null; this.style = {position: 'relative', display: 'flex', flex: 'auto', width: '100%'}; } componentDidMount(){ this.resize_columns(); window.document.addEventListener('mousemove', this.on_mouse_move); window.document.addEventListener('mouseup', this.on_mouse_up); } componentWillUnmount(){ window.document.removeEventListener('mousemove', this.on_mouse_move); window.document.removeEventListener('mouseup', this.on_mouse_up); } set_ref = ref=>{ this.ref = ref; }; resize_columns = ()=>{ const total_width = this.ref.offsetWidth; const resizable_cols = this.cols.filter(c=>!c.hidden && !c.fixed); const total_fixed = this.cols.reduce((acc, c)=> acc+(!c.hidden && c.fixed || 0), 0); const width = (total_width-total_fixed)/resizable_cols.length; const next_cols = this.cols.reduce((acc, c, idx)=>{ const w = !c.fixed && width||!c.hidden && c.fixed || 0; return { cols: [...acc.cols, { ...c, width: w, offset: acc.offset, border: acc.border, }], offset: acc.offset+w, border: !!w, }; }, {cols: [], offset: 0, border: true}); this.setState({cols: next_cols.cols}); }; start_moving = (e, idx)=>{ if (e.nativeEvent.which!=1) return; this.start_offset = e.pageX; this.start_width = this.state.cols[idx].width; this.start_width_last = this.state.cols.slice(-1)[0].width; this.moving_col = idx; this.setState({moving: true}); }; on_mouse_move = e=>{ if (this.moving_col===null) return; this.setState(prev=>{ let offset = e.pageX-this.start_offset; if (this.start_width_last-offset<this.min_width) offset = this.start_width_last-this.min_width; if (this.start_width+offset<this.min_width) offset = this.min_width-this.start_width; let total_width = 0; const next_cols = prev.cols.map((c, idx)=>{ if (idx<this.moving_col) { total_width = total_width+c.width; return c; } else if (idx==this.moving_col) { const width = this.start_width+offset; total_width = total_width+width; return {...c, width, offset: total_width-width}; } else if (idx==this.state.cols.length-1) { const width = this.start_width_last-offset; return {...c, width, offset: total_width}; } total_width = total_width+c.width; return {...c, offset: total_width-c.width}; }); return {cols: next_cols}; }); }; on_mouse_up = ()=>{ this.moving_col = null; this.setState({moving: false}); }; render(){ const style = Object.assign({}, this.style, this.props.style||{}); return <div style={style} ref={this.set_ref} className={classnames({moving: this.state.moving})} > <Table {...this.props} cols={this.state.cols} resize_columns={this.resize_columns} /> <Grid_resizers show={!this.props.cur_preview} start_moving={this.start_moving} cols={this.state.cols} /> </div>; } } return Resizable; }; const Grid_resizers = ({cols, start_moving, show})=>{ if (!show||!cols) return null; return <div> {cols.slice(0, -1).map((c, idx)=> !c.fixed && <div key={c.title||idx} style={{left: c.width+c.offset-2}} onMouseDown={e=>start_moving(e, idx)} className="data_grid_resizer"/> )} </div>; }; const Search_box = ({val, on_change})=> <div className="search_box"> <input value={val} onChange={on_change} type="text" placeholder="Filter" /> </div>; const Toolbar_row = ({children})=> <div className="toolbar"> {children} </div>; const Toolbar_container = ({children})=> <div className="toolbar_container"> {children} </div>; const Sort_icon = ({show, dir})=>{ if (!show) return null; const classes = classnames('small_icon_mask', { sort_asc: dir==-1, sort_desc: dir==1, }); return <div className="sort_icon"><span className={classes}/></div>; }; const Devider = ()=><div className="devider"/>; export class Har_viewer extends Pure_component { moving_width = false; min_width = 50; state = { cur_preview: null, tables_width: 200, }; componentDidMount(){ window.document.addEventListener('mousemove', this.on_mouse_move); window.document.addEventListener('mouseup', this.on_mouse_up); } willUnmount(){ window.document.removeEventListener('mousemove', this.on_mouse_move); window.document.removeEventListener('mouseup', this.on_mouse_up); } open_preview = req=>this.setState({cur_preview: req}); close_preview = ()=>this.setState({cur_preview: null}); start_moving_width = e=>{ if (e.nativeEvent.which!=1) return; this.moving_width = true; $(this.main_panel).addClass('moving'); this.start_offset = e.pageX; this.start_width = this.state.tables_width; }; on_resize_width = e=>{ const offset = e.pageX-this.start_offset; let new_width = this.start_width+offset; if (new_width<this.min_width) new_width = this.min_width; const max_width = this.main_panel.offsetWidth-this.min_width; if (new_width>max_width) new_width = max_width; this.setState({tables_width: new_width}); }; on_mouse_move = e=>{ if (this.moving_width) this.on_resize_width(e); }; on_mouse_up = ()=>{ this.moving_width = false; $(this.main_panel).removeClass('moving'); }; set_main_panel_ref = ref=>{ this.main_panel = ref; }; main_panel_moving = ()=>{ $(this.main_panel).addClass('moving'); }; main_panel_stopped_moving = ()=>{ $(this.main_panel).removeClass('moving'); }; undock = ()=>{ if (this.props.dock_mode) return; const url = '/dock_logs'; const opts = 'directories=0,titlebar=0,toolbar=0,location=0,' +'status=0,menubar=0,scrollbars=0,resizable=0,height=500,' +'width=800'; const har_window = window.open(url, 'har_window', opts); if (window.focus) har_window.focus(); }; clear = ()=>{ this.props.clear_logs(()=>{ this.close_preview(); setdb.emit_path('head.har_viewer.reset_reqs'); }); }; render(){ if (!this.props.proxies) return null; const width = `calc(100% - ${this.state.tables_width}px`; const preview_style = {maxWidth: width, minWidth: width}; return <div id="har_viewer" className="har_viewer chrome"> <div className="main_panel vbox" ref={this.set_main_panel_ref}> <Toolbar undock={this.undock} clear={this.clear} dock_mode={this.props.dock_mode} filters={this.props.filters} set_filter={this.props.set_filter} proxies={this.props.proxies} type_filter={this.props.type_filter} set_type_filter={this.props.set_type_filter} on_change_search={this.props.on_change_search} search_val={this.props.search} disable_logs={this.props.disable_logs} /> <div className="split_widget vbox flex_auto"> <Tables_container Cell_value={this.props.Cell_value} table_cols={this.props.table_cols} main_panel_moving={this.main_panel_moving} main_panel_stopped_moving= {this.main_panel_stopped_moving} main_panel={this.main_panel} open_preview={this.open_preview} width={this.state.tables_width} cur_preview={this.state.cur_preview} set_sort={this.props.set_sort} sorted={this.props.sorted} reqs={this.props.reqs} handle_viewpoint_enter={this.props.handle_viewpoint_enter} /> <Preview cur_preview={this.state.cur_preview} style={preview_style} close={this.close_preview} /> <Tables_resizer show={!!this.state.cur_preview} start_moving={this.start_moving_width} offset={this.state.tables_width} /> </div> </div> </div>; } } class Toolbar extends Pure_component { state = {filters_visible: false}; toggle_filters = ()=> this.setState({filters_visible: !this.state.filters_visible}); render(){ return <Toolbar_container> <Toolbar_row> <Toolbar_button id="clear" tooltip="Clear" on_click={this.props.clear} /> {!this.props.dock_mode && <Toolbar_button id="docker" on_click={this.props.undock} tooltip="Undock into separate window" /> } <Toolbar_button id="filters" tooltip="Show/hide filters" on_click={this.toggle_filters} active={this.state.filters_visible} /> <Toolbar_button id="download" tooltip="Export as HAR file" href="/api/logs_har" /> <Toolbar_button id="close_btn" tooltip="Disable" placement="left" on_click={this.props.disable_logs} /> </Toolbar_row> {this.state.filters_visible && <Toolbar_row> <Search_box val={this.props.search_val} on_change={this.props.on_change_search} /> <Type_filters filter={this.props.type_filter} set={this.props.set_type_filter} /> <Devider/> <Filters set_filter={this.props.set_filter} filters={this.props.filters} /> </Toolbar_row> } </Toolbar_container>; } } class Filters extends Pure_component { state = {}; componentDidMount(){ this.setdb_on('head.logs_suggestions', suggestions=>{ suggestions && this.setState({suggestions}); }); } render(){ if (!this.state.suggestions) return null; const filters = [ { name: 'port', default_value: 'All proxy ports', tooltip: 'Filter requests by ports', }, { name: 'status_code', default_value: 'All status codes', tooltip: 'Filter requests by status codes', }, { name: 'protocol', default_value: 'All protocols', tooltip: 'Filter requests by protocols', }, ]; return <div className="filters"> {filters.map(f=> <Filter key={f.name} tooltip={f.tooltip} vals={this.state.suggestions[f.name+'s']} val={this.props.filters[f.name]} set={this.props.set_filter.bind(null, f.name)} default_value={f.default_value} /> )} </div>; } } export const Filter = ({vals, val, set, default_value, tooltip, format_text})=> <Tooltip title={tooltip} placement="bottom"> <div className="custom_filter"> <select value={val} onChange={set}> <option value="">{default_value}</option> {vals.map(p=> <option key={p} value={p}> {format_text ? format_text(p) : p} </option> )} </select> <span className="arrow"/> </div> </Tooltip>; const type_filters = [{name: 'XHR', tooltip: 'XHR and fetch'}, {name: 'HTML', tooltip: 'HTML'}, {name: 'JS', tooltip: 'Scripts'}, {name: 'CSS', tooltip: 'Stylesheets'}, {name: 'Img', tooltip: 'Images'}, {name: 'Media', tooltip: 'Media'}, {name: 'Font', tooltip: 'Fonts'}, {name: 'Other', tooltip: 'Other'}]; const Type_filters = ({filter, set})=> <div className="filters"> <Type_filter name="All" on_click={set.bind(null, 'All')} cur={filter} tooltip="All types"/> <Devider/> {type_filters.map(f=> <Type_filter on_click={set.bind(null, f.name)} key={f.name} name={f.name} cur={filter} tooltip={f.tooltip} /> )} </div>; const Type_filter = ({name, cur, tooltip, on_click})=> <Tooltip title={tooltip} placement="bottom"> <div className={classnames('filter', {active: cur==name})} onClick={on_click}>{name}</div> </Tooltip>; const Tables_resizer = ({show, offset, start_moving})=>{ if (!show) return null; return <div className="data_grid_resizer" style={{left: offset-2}} onMouseDown={start_moving} />; }; const Tables_container = with_resizable_cols( class Tables_container extends Pure_component { constructor(props){ super(props); this.state = {focused: false}; } componentDidUpdate(prev_props){ if (prev_props.cur_preview!=this.props.cur_preview) this.props.resize_columns(); } componentDidMount(){ window.addEventListener('resize', this.props.resize_columns); } willUnmount(){ window.removeEventListener('resize', this.props.resize_columns); } on_focus = ()=>this.setState({focused: true}); on_blur = ()=>this.setState({focused: false}); on_mouse_up = ()=>{ this.moving_col = null; this.props.main_panel_stopped_moving(); }; render(){ const style = {}; if (this.props.cur_preview) { style.flex = `0 0 ${this.props.width}px`; style.width = this.props.width; style.maxWidth = this.props.width; } return <div className="tables_container vbox" tabIndex="-1" style={style} onFocus={this.on_focus} onBlur={this.on_blur}> <div className="reqs_container"> <Header_container cols={this.props.cols} reqs={this.props.reqs} set_sort={this.props.set_sort} sorted={this.props.sorted} only_name={!!this.props.cur_preview}/> <Data_container Cell_value={this.props.Cell_value} cols={this.props.cols} reqs={this.props.reqs} handle_viewpoint_enter={this.props.handle_viewpoint_enter} focused={this.state.focused} cur_preview={this.props.cur_preview} open_preview={this.props.open_preview}/> </div> <Summary_bar stats={this.state.stats}/> </div>; } }); class Summary_bar extends Pure_component { render(){ let {total, sum_in, sum_out} = this.props.stats|| {total: 0, sum_in: 0, sum_out: 0}; sum_out = bytes_format(sum_out)||'0 B'; sum_in = bytes_format(sum_in)||'0 B'; const txt = `${total} requests | ${sum_out} sent ` +`| ${sum_in} received`; return <div className="summary_bar"> <span> <Tooltip title={txt}>{txt}</Tooltip> </span> </div>; } } class Header_container extends Pure_component { click = col=>{ this.props.set_sort(col.sort_by); }; render(){ let {cols, only_name, sorted} = this.props; if (!cols) return null; if (only_name) cols = [cols[1]]; return <div className="header_container"> <table className="chrome_table"> <colgroup> {cols.map((c, idx)=> <col key={c.title} style={{width: only_name||idx==cols.length-1 ? 'auto' : c.width}}/> )} </colgroup> <tbody> <tr> {cols.map(c=> <Tooltip key={c.title} title={c.tooltip||c.title}> <th key={c.title} onClick={()=>this.click(c)} style={{textAlign: only_name ? 'left' : null}}> <div>{c.title}</div> <Sort_icon show={c.sort_by==sorted.field} dir={sorted.dir}/> </th> </Tooltip> )} </tr> </tbody> </table> </div>; } } class Data_container extends Pure_component { componentDidMount(){ this.setdb_on('head.har_viewer.dc_top', ()=>{ if (this.dc.current) this.dc.current.scrollTop = 0; }); } dc = React.createRef(); render(){ let {cols, open_preview, cur_preview, focused, reqs} = this.props; const preview_mode = !!cur_preview; cols = (cols||[]).map((c, idx)=>{ if (!preview_mode) return c; if (preview_mode && idx==1) return {...c, width: 'auto'}; return {...c, width: 0}; }); return <div ref={this.dc} className="data_container"> <table className="chrome_table"> <colgroup> {cols.map((c, idx)=> <col key={c.title} style={{width: !preview_mode && idx==cols.length-1 ? 'auto': c.width}} /> )} </colgroup> <Data_rows Cell_value={this.props.Cell_value} reqs={reqs} cols={cols} open_preview={open_preview} cur_preview={cur_preview} focused={focused} /> </table> <Waypoint key={reqs.length} scrollableAncestor={this.dc.current} bottomOffset="-50px" onEnter={this.props.handle_viewpoint_enter} /> </div>; } } class Data_rows extends React.Component { shouldComponentUpdate(next_props){ return next_props.reqs!=this.props.reqs || next_props.cur_preview!=this.props.cur_preview || next_props.focused!=this.props.focused; } render(){ return <tbody> {this.props.reqs.map(r=> <Data_row Cell_value={this.props.Cell_value} cols={this.props.cols} key={r.uuid} open_preview={this.props.open_preview} cur_preview={this.props.cur_preview} focused={this.props.focused} req={r} /> )} <tr className="filler"> {this.props.cols.map(c=><td key={c.title}/>)} </tr> </tbody>; } } class Data_row extends React.Component { shouldComponentUpdate(next_props){ const selected = zutil.get(this.props.cur_preview, 'uuid')== this.props.req.uuid; const will_selected = zutil.get(next_props.cur_preview, 'uuid')== next_props.req.uuid; const selection_changed = selected!=will_selected; const focused_changed = this.props.focused!=next_props.focused; const pending_changed = this.props.req.pending!=next_props.req.pending; return selection_changed || focused_changed && selected || pending_changed; } cell_clicked = ()=>{ this.props.open_preview(this.props.req); }; render(){ const {cur_preview, cols, focused, req} = this.props; const selected = zutil.get(cur_preview, 'uuid')==req.uuid; const classes = classnames({ selected, focused: selected && focused, error: !req.details.success && !req.pending, pending: !!req.pending, }); return <tr className={classes}> {cols.map((c, idx)=> <td key={c.title} onClick={this.cell_clicked}> <this.props.Cell_value col={c.title} req={req}/> </td> )} </tr>; } }