@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
828 lines (779 loc) • 27.3 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*jslint react:true*/
/* eslint-disable react/display-name */
import React, {useState, useEffect, useMemo, useCallback} from 'react';
import Pure_component from '/www/util/pub/pure_component.js';
import React_tooltip from 'react-tooltip';
import {Alert as RB_Alert} from 'react-bootstrap';
import classnames from 'classnames';
import codemirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/lib/codemirror.css';
import $ from 'jquery';
import styled from 'styled-components';
import {
Tooltip as UIKitTooltip,
Input as UIKitInput,
Typography,
withPopover,
IconButton,
Layout,
Menu,
Icon,
theme,
} from 'uikit';
import bsm from '/www/util/pub/bootstrap_methods.js';
import conv from '../../util/conv.js';
import date from '../../util/date.js';
import presets from './common/presets.js';
import {Pins, Select_status, Select_number, Yes_no, Regex, Json, Textarea,
Typeahead_wrapper, Input, Select, Url_input, Select_number_new, Select_new,
Url_input_new,
} from './common/controls.js';
import Tooltip from './common/tooltip.js';
import {T, t, Language} from './common/i18n.js';
import {
bytes_format,
report_exception,
Form_toggle_transform,
Clipboard,
} from './util.js';
import CP_ipc from './cp_ipc.js';
export const www_api = 'https://brightdata.com';
export const www_help = 'https://help.brightdata.com';
export const lpm_faq_article = '12632549957649';
export const Inline_wrapper = styled.div`
display: flex;
align-items: center;
column-gap: 5px;
`;
Inline_wrapper.displayName = 'Inline_wrapper';
export const Column_wrapper = styled.div`
display: flex;
flex-direction: column;
flex-wrap: wrap;
`;
Column_wrapper.displayName = 'Column_wrapper';
const uikit_from_theme = (pname, default_value)=>props=>{
let value = props[pname]||default_value;
if (value!=null)
{
if (pname.indexOf('padding')==0||pname.indexOf('margin')==0)
return theme.spacing[value]||value;
if (['font_weight', 'font_size', 'line_height', 'letter_spacing',
'color', 'box_shadow'].includes(pname))
{
return theme[pname][value];
}
}
return value;
};
export const Vertical_divider = styled.div`
width: 2px;
height: ${p=>p.height||'100%'};
background-color: ${theme.color.gray_5};
margin-top: ${uikit_from_theme('margin_top', '00')};
margin-bottom: ${uikit_from_theme('margin_bottom', '00')};
margin-left: ${uikit_from_theme('margin_left', '04')};
margin-right: ${uikit_from_theme('margin_right', '04')};
margin: ${uikit_from_theme('margin')};
`;
Vertical_divider.displayName = 'Vertical_divider';
export const Copy_icon = ({text, size, tooltip_placement})=>{
let tt_timeout = null;
const [tt, set_tt] = useState('Copy');
const on_click = useCallback(()=>{
if (!Clipboard.copy(text))
return;
if (tt_timeout)
clearTimeout(tt_timeout);
tt_timeout = setTimeout(()=>set_tt('Copy'), 3*date.ms.SEC);
set_tt('Copied!');
}, [text]);
useEffect(()=>()=>{
if (tt_timeout)
clearTimeout(tt_timeout);
}, []);
return <IconButton
aria-label="Icon Button"
icon="Copy"
onClick={on_click}
tooltip={t(tt)}
tooltipPlacement={tooltip_placement||'right'}
size={size||'xs'}
variant="ghost"
/>;
};
export const Tooltip_bytes = props=>{
let {bytes, bytes_out, bytes_in, cost} = props;
bytes = bytes||0;
let n;
if (bytes < 1e3)
n = 0;
else if (bytes < 1e6)
n = 1;
else if (bytes < 1e9)
n = 2;
else
n = 3;
const bw = bytes_format(bytes, n);
const bw_out = bytes_format(bytes_out, n);
const bw_in = bytes_format(bytes_in, n);
const display_cost = cost&&conv.fmt_currency(cost||0);
const details = bytes_out&&bytes_in&&`(${bw_out} up | ${bw_in} down)` ||
cost&&`(Estimated savings ${display_cost})` || '';
const tooltip = `<div><strong>${bw}</strong> ${details}</div>`;
return <UIKitTooltip tooltip={bytes ? tooltip : ''}>
<div className="disp_value">
{display_cost||bytes_format(bytes)||'—'}
</div>
</UIKitTooltip>;
};
export const Warnings = props=>
<div>
{(props.warnings||[]).map((w, i)=>
<Warning key={i} text={w.msg} id={w.id}/>
)}
</div>;
export class Warning extends Pure_component {
constructor(props){
super(props);
const dismissed_warnings = JSON.parse(window.localStorage.getItem(
'dismissed-warnings'))||{};
this.state = {dismissed: props.id && dismissed_warnings[props.id]};
}
dismiss = ()=>{
this.setState({dismissed: true}, this.store);
};
store = ()=>{
const dismissed_warnings = JSON.parse(window.localStorage.getItem(
'dismissed-warnings'))||{};
dismissed_warnings[this.props.id] = true;
window.localStorage.setItem('dismissed-warnings', JSON.stringify(
dismissed_warnings));
};
render(){
if (this.state.dismissed)
return null;
const content = this.props.text||this.props.children;
return <UIKitTooltip className="wide"
title={this.props.tooltip}
placement="bottom">
<div className="warning">
<Icon
size="lg"
color="gray_10"
name="AttentionCircle"
className="padding_right_10"
/>
<div className="hbox">{content}</div>
{this.props.id &&
<IconButton
aria-label="Warning dismiss"
icon="ChevronUp"
onClick={this.dismiss}
tooltip="Dismiss"
variant="ghost"
className="transparent"
/>
}
</div>
</UIKitTooltip>;
}
}
export const Loader = ({show})=>{
if (!show)
return null;
return <div className="loader_wrapper">
<div className="mask"/>
<div className="loader">
<div className="spinner"/>
</div>
</div>;
};
export const Loader_small = props=>{
let {show, saving, loading_msg, std_msg='', std_tooltip} = props;
saving = show||saving;
loading_msg = loading_msg||'Saving...';
const msg = saving ? loading_msg : std_msg;
const tooltip = saving ? '' : std_tooltip;
return <div className="loader_small">
<div className={classnames('spinner', {show: saving})}/>
<div className={classnames('saving_label', {saving})}>
<Tooltip title={tooltip}>{msg}</Tooltip>
</div>
</div>;
};
// XXX krzysztof: refactoring: reuse Copy_btn component
export class Code extends Pure_component {
componentDidMount(){
$(this.ref).find('.btn_copy').tooltip('show')
.attr('title', t('Copy to clipboard')).tooltip(bsm.fix_title);
}
set_ref(e){ this.ref = e; }
copy(){
if (this.props.on_click)
this.props.on_click();
const area = $(this.ref).children('textarea')[0];
const source = $(this.ref).children('.source')[0];
area.value = source.innerText;
area.select();
try {
document.execCommand('copy');
$(this.ref).find('.btn_copy').attr('title', t('Copied!'))
.tooltip(bsm.fix_title)
.tooltip('show').attr('title', t('Copy to clipboard'))
.tooltip(bsm.fix_title);
} catch(e){
this.etask(function*(){
yield report_exception(e, 'common.Code.copy');
});
}
}
render(){
return <code ref={this.set_ref.bind(this)}>
<span className="source">{this.props.children}</span>
<textarea style={{position: 'fixed', top: '-1000px'}}/>
<button onClick={this.copy.bind(this)} data-container="body"
className="btn btn_lpm btn_lpm_small btn_copy">
{t('Copy')}
</button>
</code>;
}
}
export const with_proxy_ports = Component=>{
const port_select = data=>function port_select_inner(props){
return <Select val={props.val}
data={data} on_change_wrapper={props.on_change}
disabled={props.disabled}/>;
};
class With_proxy_ports extends Pure_component {
state = {};
port_select = port_select([]);
componentDidMount(){
this.setdb_on('head.proxies_running', proxies=>{
if (!proxies)
return;
const ports = proxies.map(p=>p.port);
const ports_opt = proxies.map(p=>{
const name = p.internal_name ?
` (${p.internal_name})` : '';
const key = p.port+name;
return {key, value: p.port};
});
const def_port = ports[0];
this.port_select = port_select(ports_opt);
this.setState({ports, ports_opt, def_port,
ports_loaded: true});
});
}
render(){
if (!this.state.ports_loaded)
return <Loader show/>;
return <Component {...this.props}
port_select={this.port_select}
def_port={this.state.def_port}
ports={this.state.ports}
ports_opt={this.state.ports_opt}
/>;
}
}
return With_proxy_ports;
};
export const with_www_api = Component=>{
class With_www_api extends Pure_component {
state = {};
componentDidMount(){
this.setdb_on('head.defaults', defaults=>{
if (defaults)
{
this.setState({
www_api: defaults.www_api,
www_help: defaults.www_help
});
}
});
}
render(){
let _www_api = this.state.www_api || www_api;
let _www_help = this.state.www_help || www_help;
return React.createElement(Component,
{...this.props, www_api: _www_api, www_help: _www_help});
}
}
return With_www_api;
};
export const Form_controller = props=>{
const type = props.type;
if (type=='select')
return <Select {...props}/>;
else if (type=='typeahead')
return <Typeahead_wrapper {...props}/>;
else if (type=='textarea')
return <Textarea {...props}/>;
else if (type=='json')
return <Json {...props}/>;
else if (type=='url')
return <Url_input {...props}/>;
else if (type=='regex')
return <Regex {...props}/>;
else if (type=='regex_text')
return <Regex {...props} no_tip_box={true}/>;
else if (type=='yes_no')
return <Yes_no {...props}/>;
else if (type=='select_number')
return <Select_number {...props}/>;
else if (type=='select_status')
return <Select_status {...props}/>;
else if (type=='pins')
return <Pins {...props}/>;
return <Input {...props}/>;
};
export const Form_controller_new = props=>{
const type = props.type;
if (type=='select')
return <Select_new {...props}/>;
else if (type=='typeahead')
return <Typeahead_wrapper {...props}/>;
else if (type=='textarea')
return <Textarea {...props}/>;
else if (type=='json')
return <Json {...props}/>;
else if (type=='url')
return <Url_input_new {...props}/>;
else if (type=='regex')
return <Regex {...props}/>;
else if (type=='regex_text')
return <Regex {...props} no_tip_box={true}/>;
else if (type=='yes_no')
return <Yes_no {...props}/>;
else if (type=='select_number')
return <Select_number_new {...props}/>;
else if (type=='select_status')
return <Select_status {...props}/>;
else if (type=='pins')
return <Pins {...props}/>;
return <Input {...props}/>;
};
export class Copy_btn extends Pure_component {
textarea = React.createRef();
btn = React.createRef();
refreshTooltip(){
$(this.btn.current)
.attr('title', t('Copy to clipboard')).tooltip(bsm.fix_title);
}
componentDidMount(){ this.refreshTooltip(); }
componentDidUpdate(){ this.refreshTooltip(); }
copy = ()=>{
const txt = this.textarea.current;
const area = $(txt)[0];
area.value = this.props.val;
area.select();
try {
document.execCommand('copy');
$(this.btn.current).attr('title', t('Copied!'))
.tooltip(bsm.fix_title)
.tooltip('show').attr('title', t('Copy to clipboard'))
.tooltip(bsm.fix_title);
} catch(e){
this.etask(function*(){
yield report_exception(e, 'common.Copy_btn.copy');
});
}
};
render(){
return <div className="copy_btn" style={this.props.style}>
<button onClick={this.copy}
data-container="body"
style={this.props.inner_style}
ref={this.btn}
className="btn btn_lpm btn_lpm_small btn_copy">
<T>{this.props.title||'Copy'}</T>
</button>
<textarea ref={this.textarea}
style={{position: 'fixed', top: '-1000px'}}/>
</div>;
}
}
export class Cm_wrapper extends Pure_component {
componentDidMount(){
const opt = {mode: 'javascript'};
if (this.props.readonly)
opt.readOnly = 'nocursor';
this.cm = codemirror.fromTextArea(this.textarea, opt);
this.cm.on('change', this.on_cm_change);
this.cm.setSize('100%', '100%');
this.cm.doc.setValue(this.props.val||'');
}
componentDidUpdate(prev_props){
if (prev_props.val!=this.props.val &&
this.cm.doc.getValue()!=this.props.val)
{
this.cm.doc.setValue(this.props.val||'');
}
}
on_cm_change = cm=>{
const new_val = cm.doc.getValue();
if (new_val==this.props.val)
return;
this.props.on_change(new_val);
};
set_ref = ref=>{ this.textarea = ref; };
render(){
return <div className="code_mirror_wrapper">
<Copy_btn val={this.props.val}/>
<textarea ref={this.set_ref}/>
</div>;
}
}
export const Faq_link = with_www_api(props=>{
const click = ()=>{
const article = props.article || lpm_faq_article;
const anchor = props.anchor ? `#${props.anchor}` : '';
const url = `${props.www_help}/hc/en-us/articles/${article}${anchor}`;
window.open(url, '_blank');
};
return <Tooltip title={t('Read more')}>
<span onClick={click} className="fa fa-question faq_link"/>
</Tooltip>;
});
export const Faq_button = with_www_api(props=>{
const art = props.article || lpm_faq_article;
const anc = props.anchor ? `#${props.anchor}` : '';
const url = `${props.www_help}/hc/en-us/articles/${art}${anc}`;
return <IconButton
aria-label="Icon Button"
icon="Info"
as="a"
href={url}
size={props.size||'sm'}
target="_blank"
tooltip="Read more"
variant="icon"
/>;
});
export const Note = props=>
<div className="note">
<span>{props.children}</span>
</div>;
export const Field_row_raw = ({disabled, animated, ...props})=>{
const classes = classnames('field_row', {disabled});
const inner_classes = classnames('field_row_inner', props.inner_class_name,
{animated});
return <div className="field_row_wrapper">
<div className={classes}>
<div className={inner_classes} style={props.inner_style}>
{props.children}
</div>
</div>
</div>;
};
export const Field_col_raw = ({disabled, animated, ...props})=>{
const classes = classnames('field_col', {disabled});
const inner_classes = classnames('field_col_inner', props.inner_class_name,
{animated});
return <div className="field_col_wrapper">
<div className={classes}>
<div className={inner_classes} style={props.inner_style}>
{props.children}
</div>
</div>
</div>;
};
export const Labeled_controller = props=>
<Field_row_raw
disabled={props.disabled}
animated={props.animated}
inner_style={props.field_row_inner_style}>
<div className="desc" style={props.desc_style}>
<Tooltip title={t(props.tooltip)}>{t(props.label)}</Tooltip>
{props.faq && <Faq_link article={props.faq.article}
anchor={props.faq.anchor}/>}
</div>
<div>
<div className="field" data-tip data-for={props.id+'tip'}>
{props.prefix && <span className="prefix">{t(props.prefix)}</span>}
{props.children ||
<Form_controller disabled={props.disabled} {...props}/>
}
{props.sufix && <span className="sufix">{t(props.sufix)}</span>}
{props.field_tooltip &&
<React_tooltip id={props.id+'tip'} type="light" effect="solid"
delayHide={30} delayUpdate={300} place="right">
{t(props.field_tooltip)}
</React_tooltip>
}
</div>
{props.note && <Note>{t(props.note)}</Note>}
</div>
</Field_row_raw>;
export const Labeled_controller_new = props=>
<Field_col_raw
disabled={props.disabled}
animated={props.animated}
inner_style={props.field_row_inner_style}>
{props.label && <div className="desc" style={props.desc_style}>
<UIKitTooltip tooltip={t(props.tooltip)}>
{t(props.label)}
</UIKitTooltip>
{props.faq && <Faq_link article={props.faq.article}
anchor={props.faq.anchor}/>}
</div>}
<div>
<div className="field" data-tip data-for={props.id+'tip'}>
{props.prefix && <span className="prefix">{t(props.prefix)}</span>}
{props.children ||
<Form_controller disabled={props.disabled} {...props}/>
}
{props.sufix && <span className="sufix">{t(props.sufix)}</span>}
{props.field_tooltip &&
<React_tooltip id={props.id+'tip'} type="light" effect="solid"
delayHide={30} delayUpdate={300} place="right">
{t(props.field_tooltip)}
</React_tooltip>
}
</div>
{props.note && <Note>{t(props.note)}</Note>}
</div>
</Field_col_raw>;
export const Labeled_section = props=>{
const is_toggle = props.type=='toggle';
const typed = props.type && !is_toggle;
const {label, tooltip, val, default: def, on_change_wrapper, faq,
disabled, children, note, toggle_transform, class_name,
label_variant='lg', inline, short, small, w20} = props;
const toggle_on_change = toggle_state=>{
if (toggle_transform instanceof Form_toggle_transform)
toggle_state = toggle_transform.out(toggle_state);
on_change_wrapper(toggle_state);
};
const toggle_val = toggle_transform instanceof Form_toggle_transform ?
toggle_transform.in(val) : val!=undefined ? !!val : !!def;
const content = useMemo(()=><>
<div className='labeled_section'>
<div className={classnames('labeled_section_label',
{short, small, w20})}>
<Typography.Label variant={label_variant}>
<UIKitTooltip tooltip={tooltip}>{label}</UIKitTooltip>
</Typography.Label>
{faq && <Faq_button article={faq.article} anchor={faq.anchor}/>}
</div>
{is_toggle && <div className='labeled_section_toggle'>
{note||null}
<UIKitInput.Toggle
value={toggle_val}
disabled={disabled}
onChange={toggle_on_change}
/>
</div>}
</div>
{typed && <Form_controller_new {...props} />}
{!is_toggle && !typed && children}
</>, [label_variant, tooltip, label, faq, is_toggle, note, toggle_val,
disabled, short]);
return <Layout.Box width="500px" max_width="500px" className={class_name}>
{inline ? <Inline_wrapper>{content}</Inline_wrapper> : content}
</Layout.Box>;
};
export const Checkbox = props=>
<div className="form-check">
<label className="form-check-label">
<input
type="checkbox"
className={props.className ? 'form-check-input '+props.className :
'form-check-input'}
value={props.value}
onChange={props.on_change}
onClick={props.on_click}
checked={props.checked}
readOnly={props.readonly}
/>
{props.text}
</label>
</div>;
export const Nav = ({title, subtitle, warning})=>
<div className="nav_header">
<h3><T>{title}</T></h3>
<div className="subtitle"><T>{subtitle}</T></div>
<Warning_msg warning={warning}/>
</div>;
const Warning_msg = ({warning})=>{
if (!warning)
return null;
return <Warning text={warning}/>;
};
export const Link_icon = props=>{
let {tooltip, on_click, id, classes, disabled, invisible, small, img} =
props;
if (invisible)
tooltip = '';
if (disabled||invisible)
on_click = ()=>null;
classes = classnames(classes, {small});
const icon = img
? <div className="img_icon"
style={{backgroundImage: `url(${img})`}}></div>
: <i className={classnames('fa', 'fa-'+id)}/>;
return <Tooltip title={t(tooltip)} key={id}>
<span className={classnames('link', 'icon_link', classes)}
onClick={on_click}>
{icon}
</span>
</Tooltip>;
};
export const Add_icon = ({click, tooltip})=>
<Tooltip title={tooltip}>
<span className="link icon_link top right add_header" onClick={click}>
<i className="fa fa-plus"/>
</span>
</Tooltip>;
export const Remove_icon = ({click, tooltip})=>
<Tooltip title={tooltip}>
<span className="link icon_link top" onClick={click}>
<i className="fa fa-trash"/>
</span>
</Tooltip>;
export const Logo = with_www_api(class Logo extends Pure_component {
state = {};
componentDidMount(){
this.setdb_on('head.version', ver=>this.setState({ver}));
}
render(){
return <div className="nav_top">
<a href={`${this.props.www_api}/cp`} rel="noopener noreferrer"
target="_blank" className="logo_big"/>
<div className="version">V{this.state.ver}</div>
<div className="nav_top_right">
<Language/>
</div>
</div>;
}
});
export const Icon_text = ({
fi,
name,
color,
size,
verticalAlign,
text
})=>
<Inline_wrapper>
{fi ? <span className={'fi fi-'+fi}/>
: <Icon
color={color}
name={name}
size={size}
verticalAlign={verticalAlign}
/>
}
<T>{text}</T>
</Inline_wrapper>;
export const Icon_check = ()=><Icon name="Check" color="green_11" />;
export const Icon_close = ()=><Icon name="Close" color="gray_9" />;
export const any_flag = ()=>
<UIKitTooltip tooltip={t('Any')}>
<Icon_text name="Globe" text="Any" color="gray_10" size="sm"/>
</UIKitTooltip>;
export const flag_with_title = (country, title, tooltip)=>
<UIKitTooltip tooltip={tooltip||country.toUpperCase()}>
<Icon_text fi={country} text={title} />
</UIKitTooltip>;
export const Preset_description = ({preset, rule_clicked})=>{
if (!preset)
return null;
const desc = presets.get(preset).subtitle.replace(/ +(?= )/g, '')
.replace(/\n /g, '\n');
return <div><div className="desc"><T>{desc}</T></div></div>;
};
export const Ext_tooltip = with_www_api(props=>
<div>
This feature is only available when using{' '}
<a className="link"
target="_blank"
rel="noopener noreferrer"
href={`${props.www_api}/cp/zones`}>
proxies by Bright Data network
</a>
</div>);
export const debug = str=>{
const d = new Date();
console.debug('%d:%d:%d.%d: %s', d.getHours(), d.getMinutes(),
d.getSeconds(), d.getMilliseconds(), str);
};
export const No_zones = with_www_api(class No_zones extends Pure_component {
state = {};
componentDidMount(){
this.setdb_on('head.settings', settings=>{
settings && this.setState({settings});
});
}
render(){
if (!this.state.settings)
return null;
const link_props = this.state.settings.zagent && window.parent ?
{onClick: ()=>CP_ipc.post('no_zones')} :
{target: '_blank', rel: 'noopener noreferrer',
href: `${this.props.www_api}/cp/zones`};
return <div>
<div className="no_zones">
<T>No active zones found. You can activate them</T>{' '}
<a className="link" {...link_props}><T>here</T></a>.
</div>
</div>;
}
});
export const Toolbar_button = props=>
<UIKitTooltip tooltip={props.tooltip}>
<div className={classnames('cp_icon', props.id)}
onClick={props.on_click}/>
</UIKitTooltip>;
export const Alert = props=>{
const [close_tm, set_close_tm] = useState(null);
useEffect(()=>{
const tm = setTimeout(()=>props.on_close(),
props.duration||10*date.ms.SEC);
set_close_tm(tm);
return ()=>{ clearTimeout(tm); };
}, []);
return <RB_Alert
className="alert_wrapper"
variant={props.variant}
dismissible={props.dismissible}
transition={false}
closeLabel="Close"
onClose={()=>{ clearTimeout(close_tm); props.on_close(); }}>
{props.heading && <RB_Alert.Heading>{props.heading}</RB_Alert.Heading>}
{props.text && <div>{props.text}</div>}
</RB_Alert>;
};
const Context_menu_button = props=>{
const {popover, tooltip} = props;
return <IconButton icon="DotsVertical" variant="icon" tooltip={tooltip}
onClick={e=>{
popover.toggle(e);
e.stopPropagation();
}} />;
};
const Context_menu_pop = props=>{
const {items, popover} = props;
const hide_on_click = cb=>(...args)=>{
cb(...args);
popover.hide();
};
const _items = items.map(i=>{
if (i.onClick)
{
return Object.assign({}, i,
{onClick: hide_on_click(i.onClick)});
}
return i;
});
return <Menu items={_items}/>;
};
const Context_menu_comp = withPopover(Context_menu_button, Context_menu_pop,
{wrapperClassName: 'context-menu-wrapper'});
export const Context_menu = props=>{
const {items, tooltip} = props;
const popoverProps = useMemo(()=>({items, tooltip}), [items, tooltip]);
return <Context_menu_comp popoverProps={popoverProps}/>;
};