@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
1,191 lines (1,163 loc) • 38.5 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*jslint react:true, es6:true*/
import React, {useMemo} from 'react';
import Pure_component from '/www/util/pub/pure_component.js';
import {OverlayTrigger, Tooltip as B_tooltip} from 'react-bootstrap';
import {withRouter} from 'react-router-dom';
import filesaver from 'file-saver';
import $ from 'jquery';
import _ from 'lodash4';
import {
Input,
Modal,
Tooltip,
Button,
Table,
Link,
} from 'uikit';
import ajax from '../../util/ajax.js';
import setdb from '../../util/setdb.js';
import csv from '../../util/csv.js';
import etask from '../../util/etask.js';
import zutil from '../../util/util.js';
import {
get_static_country,
report_exception,
networks,
is_local,
} from './util.js';
import Proxy_blank from './proxy_blank.js';
import {
any_flag,
flag_with_title,
No_zones,
Inline_wrapper,
Tooltip_bytes,
Warnings,
Icon_text,
Icon_check,
Icon_close,
Context_menu,
} from './common.js';
import Zone_description from './common/zone_desc.js';
import {t} from './common/i18n.js';
import 'react-virtualized/styles.css';
import ws from './ws.js';
import {main as Api} from './api.js';
import Toolbar from './proxies_toolbar.js';
import './css/proxies.less';
const {Popup} = Modal, {Checkbox} = Input, {keys, values, assign} = Object;
const Actions_cell = ({row, column})=>{
const {proxy, mgr} = row.original;
return <Actions proxy={proxy} open_delete_dialog={mgr.open_delete_dialog}
update_proxies={mgr.update} get_status={mgr.get_status}
show_error_ntf={mgr.show_error_ntf} tooltip={column.tooltip}/>;
};
const Targeting_cell = props=>{
const {proxy, mgr} = props.row.original;
const title = (t_country, t_state, t_city, t_zip)=>{
let _val = t_country;
if (t_state)
_val += ` (${t_state})`;
if (t_state && t_city)
_val += `, ${t_city}`;
if (t_zip)
_val += ` (${t_zip})`;
return _val;
};
const zones = mgr.state.zones;
const static_country = get_static_country(proxy, zones);
if (static_country && static_country!='any' && static_country!='*')
{
return flag_with_title(static_country,
static_country.toUpperCase());
}
let val = proxy.country;
if (!val||val=='any'||val=='*')
return any_flag();
return flag_with_title(proxy.country, title(val.toUpperCase(),
proxy.state && proxy.state.toUpperCase(), proxy.city, proxy.zip));
};
const Status_cell = ({value: proxy})=>{
const status = proxy.status;
const status_details = proxy.status_details;
let details = (status_details||[]).map(d=>d.msg).join(',');
if (!details.length && status!='ok')
details = status;
if (status=='testing')
{
return <Tooltip tooltip="Status is being tested">
Testing
</Tooltip>;
}
else if (status && status!='ok')
{
return <Tooltip tooltip={details}>
{details}
</Tooltip>;
}
else if (status=='ok' && details)
{
return <Tooltip tooltip={details}>
<Icon_text
name="Warning"
text="Ok"
color="gray_10"
size="xs"
verticalAlign="top"
/>
</Tooltip>;
}
else if (status=='ok' && !details)
{
return <Tooltip tooltip="This proxy works correctly">
OK
</Tooltip>;
}
return <Tooltip tooltip="Status of this proxy is unknown">
?
</Tooltip>;
};
const Boolean_cell = ({value})=>value===true ? <Icon_check />
: <Icon_close />;
const Static_ip_cell = props=>{
const {proxy, mgr} = props.row.original;
if (proxy.ip)
return proxy.ip;
const curr_zone = mgr.state.zones.zones.find(z=>z.name==proxy.zone);
const curr_plan = curr_zone && curr_zone.plan;
const is_static = curr_plan && (curr_plan.type||'').startsWith('static');
if (is_static && proxy.pool_size)
{
if (Array.isArray(proxy.ips) && proxy.ips.length==1)
return proxy.ips[0];
return `Pool of ${proxy.pool_size} IP${proxy.pool_size==1 ? '' : 's'}`;
}
return null;
};
const Type_cell = ({value: ext_proxy})=>{
let nw = ext_proxy ? 'ext' : 'brd';
let {label, desc} = networks.find(n=>n.value==nw)||{};
return <Tooltip tooltip={t(desc)}>{t(label||nw)}</Tooltip>;
};
class Browser_cell extends Pure_component {
open_browser = e=>{
e.stopPropagation();
const _this = this;
ws.post_event('Browser Click');
this.etask(function*(){
const res = yield Api.get(`browser/${_this.props.value.port}`);
if ((res||'').includes('Fetching'))
$('#fetching_chrome_modal').modal('show');
});
};
render(){
const tooltip = 'Open browser configured with this port';
return is_local() && <Tooltip tooltip={t(tooltip)}>
<Link
icon="ArrowUpRight"
onClick={this.open_browser}
text={t('Browser')}
/>
</Tooltip>;
}
}
const Port_cell = ({value: proxy})=>{
const is_multiplied = !proxy.master_port && proxy.multiply &&
proxy.multiply>1;
const val = is_multiplied ? proxy.port+':1..'+proxy.multiply :
proxy.port;
const pl = is_multiplied ? 's' : '';
const tip = `${is_multiplied ? proxy.port+'-'+(proxy.port+proxy.multiply)
+' are' : proxy.port+' is a'} proxy port${pl} that refers to a specific
virtual location${pl} on a computer. You can use it as a virtual proxy
to send requests`;
return <Tooltip tooltip={tip}>{val}</Tooltip>;
};
const Success_rate_cell = ({value: proxy})=>{
const val = !proxy.reqs ? '—' :
(proxy.success/proxy.reqs*100).toFixed(2)+'%';
const tip = `${t('Total')}: ${proxy.reqs||0}, ${t('Success')}:`
+proxy.success||0;
return <Tooltip title={tip}>
{val}
</Tooltip>;
};
const Reqs_cell = ({value: reqs=0})=>{
const tip = `${+reqs} requests sent through this proxy port`;
return <Tooltip tooltip={tip}>
{+reqs}
</Tooltip>;
};
const Zone_cell = props=>{
const {proxy, mgr} = props.row.original;
const zones = mgr.state.zones;
return <div>
<OverlayTrigger
placement="top"
overlay={
<B_tooltip id={`${proxy.port}-zone-tooltip`}>
<div className="zone_tooltip">
<Zone_description zones={zones} zone_name={proxy.zone}/>
</div>
</B_tooltip>
}
>
<span>{proxy.zone}</span>
</OverlayTrigger>
</div>;
};
const Rules_cell = ({value: {rules=[]}})=>{
const tip = 'Number of defined rules for this proxy port';
const val = rules.length;
return !!val && <Tooltip tooltip={t(tip)}>{t(val)}</Tooltip>;
};
const DNS_cell = ({value})=>value||'local';
const Header_cell = ({column})=>{
if (column.hide_col_title)
return '';
return <Tooltip tooltip={column.tooltip||column.title||column.id}>
{t(column.title||column.id)}
</Tooltip>;
};
const T_cell = ({value})=>{
return <Tooltip tooltip={value}>
{value}
</Tooltip>;
};
const Select_rows_header = ({column})=>{
const {mgr: {all_rows_select, state: {checked_all}}} = column;
return <Checkbox onChange={all_rows_select} checked={checked_all} />;
};
const Select_row_cell = props=>{
const {proxy, mgr} = props.row.original;
let checked = mgr.state.checked_all
|| !!mgr.state.selected_proxies[proxy.port];
return <Checkbox
onChange={mgr.on_row_select.bind(mgr, proxy)}
checked={checked}
/>;
};
const columns = [
{
id: 'internal_name',
Cell: T_cell,
title: 'Internal name',
tooltip: `An internal name is used for proxy ports to be easily
distinguished.\nThose don't change any proxy behavior and it's only
cosmetic.`,
maxWidth: 100,
minWidth: 70,
can_sort: true,
},
{
id: 'port',
title: 'Proxy port',
accessor: 'proxy',
Cell: Port_cell,
tooltip: 'A proxy port is a number that refers to a specific virtual '
+'location on a computer. Create and configure proxy ports, then '
+'connect the crawler to send requests through the port',
sticky: true,
default: true,
ext: true,
dynamic: true,
maxWidth: 90,
minWidth: 50,
can_sort: true,
},
{
id: 'proxy_type',
accessor: 'proxy.ext_proxies',
title: 'Type',
Cell: Type_cell,
tooltip: 'Type of connected proxy - Bright Data proxy or external'
+' proxy (non Bright Data)',
ext: true,
maxWidth: 70,
minWidth: 50,
},
{
id: 'status',
title: 'Status',
accessor: 'proxy',
Cell: Status_cell,
tooltip: 'Real time proxy status',
default: true,
type: 'string',
ext: true,
dynamic: true,
width: 40,
can_sort: true,
},
{
id: 'iface',
title: 'Interface',
tooltip: 'Specific network interface on which the local machine is '
+'running. Switch interfaces in the proxy configuration page',
type: 'string',
ext: true,
width: 50,
},
{
id: 'multiply',
title: 'Multiply',
tooltip: 'Number of multiplied proxy ports. A proxy port can be '
+'multiplied in the proxy configuration page',
type: 'string',
ext: true,
can_sort: true,
width: 50,
},
{
id: 'ssl',
title: 'SSL Analyzing',
accessor: 'proxy.ssl',
Cell: Boolean_cell,
tooltip: 'In order to log HTTPS requests, enable SSL request logs in '
+'proxy configuration',
ext: true,
width: 40,
},
{
id: 'proxy_connection_type',
title: 'SP Protocol',
tooltip: 'Connection type between Proxy Manager and Super Proxy',
ext: true,
width: 30,
},
{
id: 'zone',
title: 'Zone',
default: true,
Cell: Zone_cell,
tooltip: 'Specific Bright Data zone configured for this proxy. Switch '
+'zones in proxy configuration page.',
width: 30,
},
{
id: 'country',
title: 'Targeting',
default: true,
Cell: Targeting_cell,
tooltip: 'Exit node (IP) GEO location',
width: 30,
},
{
id: 'asn',
title: 'ASN',
tooltip: 'ASN uniquely identifies each network on the internet. You '
+'can target exit nodes (IPs) on specific ASNs',
},
{
id: 'rotate_session',
title: 'Rotate IPs',
accessor: 'proxy.rotate_session',
Cell: Boolean_cell,
ext: true,
},
{
id: 'pool_size',
title: 'Pool size',
type: 'number',
ext: true,
},
{
id: 'sticky_ip',
title: 'Sticky IP',
accessor: 'proxy.sticky_ip',
Cell: Boolean_cell,
ext: true,
},
{
id: 'rules',
title: 'Rules',
accessor: 'proxy',
Cell: Rules_cell,
ext: true,
},
{
id: 'dns',
title: 'DNS',
accessor: 'proxy.dns',
Cell: DNS_cell,
},
{
id: 'ip',
title: 'Static IPs',
calc_show: proxies=>proxies.some(p=>p.multiply_ips),
Cell: Static_ip_cell,
},
{
id: 'user',
title: 'User',
ext: true,
calc_show: proxies=>proxies.some(p=>p.multiply_users),
},
{
id: 'vip',
title: 'gIP',
tooltip: 'A gIP is a group of exclusive residential IPs. Using gIPs '
+'ensures that nobody else uses the same IPs with the same target '
+'sites as you do.',
},
{
id: 'proxy',
title: 'SP',
tooltip: 'Super Proxy',
},
{
id: 'throttle',
title: 'Throttle',
tooltip: 'Throttle concurrent connections',
ext: true,
},
{
id: 'success',
title: 'Success',
tooltip: 'The ratio of successful requests out of total requests. A '
+'request is considered as successful if the server of the '
+'destination website responded',
accessor: 'proxy',
Cell: Success_rate_cell,
default: true,
ext: true,
dynamic: true,
width: 58,
},
{
id: 'in_bw',
title: 'BW up',
accessor: 'proxy',
Cell: ({value})=>Tooltip_bytes({bytes: value.out_bw}),
ext: true,
tooltip: 'Data transmitted to destination website. This includes'
+'request headers, request data, response headers, response data',
dynamic: true,
width: 60,
},
{
id: 'out_bw',
title: 'BW down',
accessor: 'proxy',
Cell: ({value})=>Tooltip_bytes({bytes: value.in_bw}),
ext: true,
tooltip: 'Data transmitted to destination website. This includes'
+'request headers, request data, response headers, response data',
dynamic: true,
width: 60,
},
{
id: 'bw',
title: 'BW',
accessor: 'proxy',
Cell: ({value})=>Tooltip_bytes({bytes: value.bw,
bytes_out: value.out_bw, bytes_in: value.in_bw}),
default: true,
ext: true,
tooltip: 'Data transmitted to destination website. This includes'
+'request headers, request data, response headers, response data',
dynamic: true,
width: 60,
},
{
id: 'reqs',
title: 'Requests',
Cell: Reqs_cell,
default: true,
ext: true,
tooltip: 'Number of all requests sent from this proxy port',
dynamic: true,
grow: 1,
width: 60,
},
...is_local() ? [{
id: 'browser',
title: 'Browser',
accessor: 'proxy',
Cell: Browser_cell,
default: true,
tooltip: 'Open browser configured with this port',
width: 80,
grow: 0,
shrink: 0,
hide_col_title: true,
}] : [],
{
id: 'actions',
title: 'Actions',
tooltip: `Delete/duplicate/refresh sessions/open browser`,
ext: true,
sticky: true,
Cell: Actions_cell,
width: 10,
maxWidth: 10,
minWidth: 10,
shrink: 0,
grow: 0,
csv: false,
hide_col_title: true,
},
].map(col=>assign({
Header: Header_cell,
accessor: ({proxy})=>proxy[col.id]||'',
width: 30,
maxWidth: 50,
minWidth: 50,
}, col));
class Columns_modal extends Pure_component {
constructor(props){
super(props);
this.state = {
selected_cols: [],
};
}
componentDidMount(){
this.setdb_on('head.settings', settings=>{
if (!settings)
return;
this.setState({settings});
});
const selected_cols = this.props.selected_cols.reduce((acc, e)=>
assign(acc, {[e]: true}), {});
this.setState({selected_cols, saved_cols: selected_cols});
}
on_change = value=>{
this.setState(prev=>({selected_cols: {
...prev.selected_cols,
[value]: !prev.selected_cols[value],
}}));
};
click_ok = ()=>{
const new_columns = keys(this.state.selected_cols).filter(c=>
this.state.selected_cols[c]);
this.props.update_selected_cols(new_columns);
window.localStorage.setItem('columns', JSON.stringify(
this.state.selected_cols));
this.props.on_hide();
};
click_cancel = ()=>
this.setState({selected_cols: this.state.saved_cols},
this.props.on_hide);
select_all = ()=>{
this.setState({selected_cols: columns.filter(c=>!c.sticky)
.reduce((acc, e)=>assign(acc, {[e.key]: true}), {})});
};
select_none = ()=>this.setState({selected_cols: {}});
select_default = ()=>{
this.setState({selected_cols: columns.filter(c=>!c.sticky&&c.default)
.reduce((acc, e)=>assign(acc, {[e.key]: true}), {})});
};
render(){
const header = <Inline_wrapper>
<Button
onClick={this.select_all}
text="Check all"
variant="secondary"
size="sm"
/>
<Button
onClick={this.select_none}
text="Uncheck all"
variant="secondary"
size="sm"
/>
<Button
onClick={this.select_default}
text="Default"
variant="secondary"
size="sm"
/>
</Inline_wrapper>;
const content = <div className="row columns">
{columns.filter(col=>!col.sticky).map(col=>
<div key={col.id} className="col-md-6">
<Checkbox
label={t(col.title||col.id)}
onChange={this.on_change.bind(this, col.id)}
checked={!!this.state.selected_cols[col.id]}
/>
</div>
)}
</div>;
return <Popup
show={this.props.show}
onOk={this.click_ok}
onCancel={this.click_cancel}
title={header}
content={content}
shadow="sm"
size="lg"
/>;
}
}
const Proxies = withRouter(class Proxies extends Pure_component {
update_window_dimensions = ()=>
this.setState({height: window.innerHeight});
constructor(props){
super(props);
let from_storage = JSON.parse(window.localStorage.getItem(
'columns'))||{};
from_storage = keys(from_storage).filter(c=>from_storage[c]);
const default_cols = columns.filter(c=>c.default).map(col=>col.id);
this.state = {
selected_cols: from_storage.length && from_storage || default_cols,
proxies: [],
selected_proxies: {},
checked_all: false,
proxy_filter: '',
sort: {sort_by: 'port', sort_direction: 'asc'},
visible_proxies: null,
loaded: false,
height: window.innerHeight,
open_delete_dialog: false,
delete_proxies: [],
errors: [],
err_ntf_list: [],
show_errors_modal: false,
show_columns_modal: false,
};
setdb.set('head.proxies.update', this.update);
}
componentDidMount(){
this.setdb_on('head.proxies_running', proxies=>{
if (!proxies)
return;
let proxy_filter = proxies.length ? this.state.proxy_filter : '';
proxies = this.prepare_proxies(proxies);
const visible_proxies = this.get_visible_proxies(proxies,
proxy_filter);
this.setState(
{proxies, visible_proxies, proxy_filter, loaded: true});
});
this.setdb_on('head.locations', locations=>{
if (!locations)
return;
const countries = locations.countries_by_code;
this.setState({countries});
});
this.setdb_on('head.settings', settings=>{
if (!settings)
return;
this.setState({settings});
});
this.setdb_on('ws.zones', zones=>{
if (!zones)
return;
this.setState({zones});
return this.update();
});
this.setdb_on('head.app_errors', errors=>{
if ((errors||[]).length)
this.setState({errors});
});
this.timeout_id = window.setTimeout(this.req_status);
this.update_window_dimensions();
window.addEventListener('resize', this.update_window_dimensions);
}
willUnmount(){
window.clearTimeout(this.timeout_id);
window.removeEventListener('resize', this.update_window_dimensions);
}
get select_rows_col(){
return {
id: 'select_rows',
Header: Select_rows_header,
Cell: Select_row_cell,
mgr: this,
ext: true,
sticky: true,
style: {
width: 'fit-content',
},
maxWidth: 40,
shrink: 0,
grow: 0,
csv: false,
};
}
get_visible_proxies = (proxies, proxy_filter, sort)=>{
if (proxy_filter===undefined)
proxy_filter = this.state.proxy_filter;
sort = sort||this.state.sort;
const master_ports_idx = new Map();
proxies.forEach(p=>!p.master_port && master_ports_idx.set(p.port, p));
proxies = proxies.filter(p=>{
if (proxy_filter &&
!(p.internal_name||'').includes(proxy_filter) &&
!(p.zone||'').includes(proxy_filter) &&
!(''+p.port||'').includes(proxy_filter))
{
return false;
}
if (p.master_port && !master_ports_idx.has(p.master_port))
throw 'duplicate_port_number';
if (p.proxy_type=='duplicate')
return master_ports_idx.get(p.master_port).expanded;
return true;
});
let {zones} = this.state;
const res = [...proxies].sort((p1, p2)=>{
let v1 = p1[sort.sort_by], v2 = p2[sort.sort_by];
if (sort.sort_by=='country')
{
v1 = get_static_country(p1, zones)||p1.country||'any';
v2 = get_static_country(p2, zones)||p2.country||'any';
}
return (sort.sort_direction=='asc' ? 1 : -1) *
(v1 > v2 ? 1 : v1 < v2 ? -1 : 0);
});
return res;
};
prepare_proxies = proxies=>{
for (let i=0; i<proxies.length; i++)
{
const cur = proxies[i];
if (Array.isArray(cur.proxy)&&cur.proxy.length==1)
cur.proxy = cur.proxy[0];
cur.status_details = cur.status_details||[];
}
return proxies;
};
req_status = ()=>{
const _this = this;
this.etask(function*(){
this.on('uncaught', e=>_this.etask(function*(){
yield report_exception(e, 'proxies.Proxies.req_status');
}));
const stats = yield Api.json.get('recent_stats');
setdb.set('head.recent_stats', stats);
if (!_this.state.proxies.length ||
zutil.equal_deep(stats, _this.state.stats))
{
yield etask.sleep(1000);
_this.req_status();
return;
}
_this.setState(prev=>{
const new_proxies_a = prev.proxies.map(p=>{
let stat = {reqs: 0, success: 0, in_bw: 0, out_bw: 0,
bw: 0};
if (''+p.port in stats.ports)
stat = stats.ports[p.port];
p.in_bw = stat.in_bw;
p.out_bw = stat.out_bw;
p.bw = p.in_bw+p.out_bw;
p.reqs = stat.reqs;
p.success = stat.success;
p.last_req = {url: stat.url, ts: stat.timestamp};
if (p.proxy_type!='duplicate')
p.expanded = !!p.expanded;
return p;
});
const new_proxies = {};
for (let p of new_proxies_a)
new_proxies[p.port] = p;
for (let p of values(new_proxies))
{
if (p.proxy_type=='duplicate')
{
const master_port = new_proxies[p.master_port];
master_port.in_bw += p.in_bw;
master_port.out_bw += p.out_bw;
master_port.bw += p.bw;
master_port.reqs += p.reqs;
master_port.success += p.success;
if (!master_port.last_req.ts ||
p.last_req.ts &&
p.last_req.ts>master_port.last_req.ts)
{
master_port.last_req = p.last_req;
}
}
}
const visible_proxies = _this.get_visible_proxies(
values(new_proxies));
return {proxies: values(new_proxies), visible_proxies,
stats};
});
yield etask.sleep(1000);
_this.req_status();
});
};
update = ()=>{
this.setState({selected_proxies: {}, checked_all: false});
return this.etask(function*(){
const proxies = yield Api.json.get('proxies_running');
setdb.set('head.proxies_running', proxies);
});
};
download_csv = ()=>{
const cols = this.get_cols().filter(c=>c.csv==undefined || c.csv);
const titles = [cols.map(col=>col.title)];
const data = titles.concat(this.state.proxies.map(p=>{
return cols.map(col=>{
const val = zutil.get(p, col.key);
if (val==undefined)
return '-';
return val;
});
}));
ws.post_event('Toolbar Download CVS Clicked');
filesaver.saveAs(csv.to_blob(data), 'proxies.csv');
};
edit_columns = ()=>{
ws.post_event('Toolbar Edit Columns Clicked');
this.setState({show_columns_modal: true});
};
hide_edit_columns = ()=>{
this.setState({show_columns_modal: false});
};
update_selected_columns = new_columns=>
this.setState({selected_cols: new_columns});
on_row_select = (proxy, value, e)=>{
e.stopPropagation();
const {selected_proxies, visible_proxies} = this.state;
const new_selected = assign({}, selected_proxies);
if (!value)
delete new_selected[proxy.port];
else
new_selected[proxy.port] = proxy;
const checked_all = value &&
keys(new_selected).length==visible_proxies.length;
this.setState({selected_proxies: new_selected, checked_all});
};
all_rows_select = ()=>{
const checked_all = !this.state.checked_all;
const selected_proxies = checked_all ?
this.state.visible_proxies.reduce((obj, p)=>{
obj[p.port] = p;
return obj;
}, {}) : {};
this.setState({selected_proxies, checked_all});
};
show_error_ntf = err_ntf_list=>
this.setState({err_ntf_list, show_errors_modal: true});
hide_error_ntf = ()=>
this.setState({err_ntf_list: [], show_errors_modal: false});
on_row_click = ({proxy})=>{
if (!document.getSelection().isCollapsed)
return;
if (proxy.proxy_type!='persist')
return;
if (proxy.master_port)
return;
ws.post_event('Proxy Port Click', {port: proxy.port});
this.props.history.push(`/proxy/${proxy.port}`);
};
get_cols = ()=>[this.select_rows_col, ...columns].filter(col=>{
if (col.sticky)
return true;
if (col.calc_show && col.calc_show(this.state.visible_proxies||[]))
return true;
return this.state.selected_cols.includes(col.id);
});
open_delete_dialog = proxies=>{
this.setState({delete_proxies: proxies, open_delete_dialog: true});
};
close_delete_dialog = ()=>{
this.setState({open_delete_dialog: false});
};
set_proxy_filter = value=>{
this.setState({
visible_proxies: this.get_visible_proxies(this.state.proxies,
value),
proxy_filter: value,
selected_proxies: {},
});
};
set_sort({sortBy: sort_by, sortDirection: sort_direction}){
if (sort_by=='select')
return;
sort_direction = sort_direction.toLowerCase();
let sort = {sort_by, sort_direction};
const visible_proxies = this.get_visible_proxies(this.state.proxies,
undefined, sort);
this.setState({sort, visible_proxies, selected_proxies: {}});
}
get_row_data = proxy=>({
proxy,
mgr: this,
});
render(){
let {proxies, visible_proxies, show_columns_modal, proxy_filter}
= this.state;
let is_loading = !this.state.zones || !this.state.countries
|| !visible_proxies;
if (this.state.errors.length)
{
return this.state.errors.map(e=>
<div key={e} className="warning error settings-alert">
{e}
</div>);
}
let show_table = !!proxies.length;
if (this.state.zones && !this.state.zones.zones.length)
return <No_zones/>;
if (this.state.loaded && !show_table)
return <Proxy_blank/>;
return <React.Fragment>
<div className="main_panel vbox cp_panel proxies_panel
force_cp_panel">
<Header_panel
selected={this.state.selected_proxies}
open_delete_dialog={this.open_delete_dialog}
download_csv={this.download_csv}
edit_columns={this.edit_columns}
set_proxy_filter={this.set_proxy_filter}
proxy_filter={proxy_filter}
toggle_stats={this.props.toggle_stats||_.noop}
request_stats={this.props.request_stats}
/>
{this.state.loaded && show_table &&
<Table.Provider
fullWidth
hideFooter
disableEditing
disablePinning
manualPagination
disableHiding
isLoading={is_loading}
columns={this.get_cols()}
data={visible_proxies.map(this.get_row_data)}
>
<Table
rowDensity="busy_bee"
onRowClick={this.on_row_click}
/>
</Table.Provider>
}
</div>
<Columns_modal selected_cols={this.state.selected_cols}
update_selected_cols={this.update_selected_columns}
show={show_columns_modal} on_hide={this.hide_edit_columns}/>
<Delete_dialog open={this.state.open_delete_dialog}
close_dialog={this.close_delete_dialog}
proxies={this.state.delete_proxies}
update_proxies={this.update}/>
<Popup
show={this.state.show_errors_modal}
onOk={this.hide_error_ntf}
cancelDisabled
title="Errors"
content={<Warnings warnings={this.state.err_ntf_list}/>}
shadow="sm"
size="lg"
/>
</React.Fragment>;
}
});
const Actions_bulk = ({selected, open_delete_dialog})=>{
const to_delete = useMemo(()=>
values(selected).filter(p=>p.proxy_type=='persist'), [selected]);
const bulk_refresh_session = e=>{
e.stopPropagation();
ws.post_event('Toolbar Action Refresh Click',
{ports: to_delete.map(p=>p.port)});
values(selected).forEach(p=>
setdb.emit_path('actions.refresh_sessions.'+p.port));
};
const bulk_open_delete_dialog = e=>{
e.stopPropagation();
ws.post_event('Toolbar Action Remove Click',
{ports: to_delete.map(p=>p.port)});
open_delete_dialog(to_delete);
};
return <div className="cp_panel_header bulk_actions">
<Link
icon="Refresh"
onClick={bulk_refresh_session}
text={t('Refresh')}
size="lg"
/>
{to_delete.length && <Link
icon="Delete"
onClick={bulk_open_delete_dialog}
text={t('Delete')}
size="lg"
/>}
</div>;
};
const Header_panel = props=>{
const {selected, open_delete_dialog} = props;
const any_selected = useMemo(()=>values(selected).length,
[selected]);
return any_selected ? <Actions_bulk selected={selected}
open_delete_dialog={open_delete_dialog} />
: <div className="cp_panel_header">
<h2>
<Icon_text
color="gray_11"
name="MenuExpand"
verticalAlign="middle"
text="All ports"
/>
</h2>
<Toolbar {...props}/>
</div>;
};
class Delete_dialog extends Pure_component {
delete_proxies = e=>{
e.stopPropagation();
const _this = this;
const ports = _this.props.proxies.map(p=>p.port);
this.etask(function*(){
yield Api.json.post('proxies/delete', {ports});
yield _this.props.update_proxies();
_this.props.close_dialog();
});
};
render(){
const {proxies, open, close_dialog} = this.props;
let title = 'Are you sure you want to delete ';
if (proxies.length==1)
title += `proxy port ${proxies[0].port}?`;
else
title += `${proxies.length} proxy ports?`;
return <Popup
show={open}
onOk={this.delete_proxies}
onCancel={close_dialog}
title={title}
content={null}
shadow="sm"
size="lg"
/>;
}
}
class Actions extends Pure_component {
componentDidMount(){
if (!this.props.proxy.status)
this.get_status();
this.add_refresh_listener();
}
componentWillUnmount(){
if (this.status_req)
ajax.abort(this.status_req);
}
componentDidUpdate(prev_props){
this.remove_refresh_listener(prev_props.proxy.port);
this.add_refresh_listener();
}
add_refresh_listener = (port=this.props.proxy.port)=>
this.setdb_on('actions.refresh_sessions.'+port, this.refresh_sessions,
{init: false});
remove_refresh_listener = (port=this.props.proxy.port)=>
this.setdb_off('actions.refresh_sessions.'+port);
get items(){
const persist = this.props.proxy.proxy_type=='persist';
return [
...persist ? [
{
key: '1',
icon: 'Refresh',
text: 'Refresh',
onClick: this.refresh_sessions,
},
{
key: '2',
icon: 'Copy',
text: 'Duplicate',
onClick: this.duplicate,
},
{
key: '1-divider',
type: 'divider'
},
] : [],
{
key: '3',
icon: 'Trash',
text: 'Delete',
onClick: this.open_delete_dialog_with_port,
variant: 'negative'
}
];
}
// XXX krzysztof: this logic is a mess, rewrite it
get_status = (opt={})=>{
const proxy = this.props.proxy;
if (!opt.force && proxy.status=='ok')
return;
const _this = this;
return this.etask(function*(){
this.on('uncaught', e=>{
proxy.status = 'error';
proxy.status_details = [{msg: e.message}];
setdb.emit_path('head.proxies_running');
});
const params = {};
if (!proxy.status)
proxy.status = 'testing';
if (opt.force)
{
proxy.status = 'testing';
params.force = true;
}
if (proxy.status=='testing')
setdb.emit_path('head.proxies_running');
_this.status_req = yield Api.json.get('proxy_status/'+proxy.port,
{qs: params});
const res = yield _this.status_req;
delete _this.status_req;
if (res===undefined)
{
delete proxy.status;
setdb.emit_path('head.proxies_running');
return;
}
if (res.status!='ok')
{
const errors = res.status_details||[];
res.status_details = errors.length ?
errors : [{msg: res.status}];
res.status = 'error';
}
proxy.status = res.status;
proxy.status_details = res.status_details||[];
setdb.emit_path('head.proxies_running');
});
};
post_action(action){
ws.post_event(`Port Action ${action} Click`,
{port: this.props.proxy.port});
}
refresh_sessions = e=>{
if (e)
e.stopPropagation();
const _this = this;
this.etask(function*(){
yield Api.json.post('refresh_sessions/'+_this.props.proxy.port);
yield _this.get_status({force: true});
if (e)
_this.post_action('Refresh');
});
};
duplicate = event=>{
event.stopPropagation();
const _this = this;
this.etask(function*(){
this.on('uncaught', e=>_this.etask(function*(){
yield report_exception(e, 'proxies.Actions.duplicate');
}));
const resp = yield Api.json.post('proxy_dup',
{port: _this.props.proxy.port});
if (resp.errors)
_this.props.show_error_ntf(resp.errors);
_this.post_action('Duplicate');
yield _this.props.update_proxies();
});
};
open_delete_dialog_with_port = e=>{
e.stopPropagation();
this.post_action('Remove');
this.props.open_delete_dialog([this.props.proxy]);
};
render(){
return <Context_menu
tooltip={this.props.tooltip}
items={this.items}
/>;
}
}
export default Proxies;