luminati-proxy-stripped
Version:
A configurable local proxy for luminati.io
326 lines (306 loc) • 11.4 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*jslint react:true, es6:true*/
import Pure_component from '../../www/util/pub/pure_component.js';
import React from 'react';
import JSON_viewer from './json_viewer.js';
import codemirror from 'codemirror/lib/codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/htmlmixed/htmlmixed';
import classnames from 'classnames';
import moment from 'moment';
import $ from 'jquery';
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},
];
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_preview}
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 {
render(){
const {req} = this.props;
const general_entries = [{name: 'Request URL', value: req.request.url},
{name: 'Status Code', value: req.response.status}];
return <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}/>
</ol>;
}
}
class Pane_response extends Pure_component {
render(){
const content_type = this.props.req.details.content_type;
if (!content_type||['xhr', 'css', 'js', 'font'].includes(content_type))
return <Codemirror_wrapper req={this.props.req}/>;
return <No_response_data/>;
}
}
const No_response_data = ()=>
<div className="empty_view">
<div>This request has no response data available.</div>
</div>;
class Codemirror_wrapper extends Pure_component {
componentDidMount(){
this.cm = codemirror.fromTextArea(this.textarea, {
readOnly: 'nocursor',
lineNumbers: true,
});
this.cm.setSize('100%', '100%');
this.cm.doc.setValue(this.props.req.response.content.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';
this.cm.setOption('mode', mode);
}
set_textarea = ref=>{ this.textarea = ref; };
render(){
return <div className="codemirror_wrapper">
<textarea ref={this.set_textarea}/>
</div>;
}
}
class Preview_section extends Pure_component {
state = {open: true};
toggle = ()=>this.setState(prev=>({open: !prev.open}));
render(){
return [
<li key="li" onClick={this.toggle}
className={classnames('parent', {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 green = /2../.test(value);
const yellow = /3../.test(value);
const red = /(4|5)../.test(value);
const classes = classnames('small_icon', 'status', {
green, yellow, red});
return <div className="status_wrapper">
<div className={classes}/>{value}
</div>;
};
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/>
}
</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: '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});
return [
<li key="li" onClick={this.toggle}
className={classnames('parent', {open: this.state.open})}>
{this.props.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}}>
​</span>
</div>
</td>
<td><div className="timing_bar_title">{time} ms</div></td>
</tr>;
class Enable_https extends Pure_component {
click = ()=>$('#enable_ssl_modal').modal();
render(){
return <tr className="footer_link">
<td colSpan="2">
<a className="devtools_link" role="link" tabIndex="0"
target="_blank" rel="noopener noreferrer"
onClick={this.click}
style={{display: 'inline', cursor: 'pointer'}}>
Enable HTTPS logging
</a> to view this timeline
</td>
<td></td>
</tr>;
}
}
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;
let json;
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}/>;
return <div className="pane_preview"></div>;
}
}
const Img_viewer = ({img})=>
<div className="img_viewer">
<div className="image">
<img src={img}/>
</div>
</div>;
export default Preview;