UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

336 lines (334 loc) 12.9 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint react:true, es6:true*/ import React from 'react'; import Pure_component from '/www/util/pub/pure_component.js'; import $ from 'jquery'; import _ from 'lodash'; import setdb from '../../../util/setdb.js'; import ajax from '../../../util/ajax.js'; import {Modal} from '../common/modals.js'; import {report_exception} from '../util.js'; import {Infinite_chrome_table} from '../chrome_widgets.js'; import conv from '../../../util/conv.js'; import zcountry from '../../../util/country.js'; import {flag_with_title} from '../common.js'; import {Filter} from '../har/viewer.js'; export default class Alloc_modal extends Pure_component { set_field = setdb.get('head.proxy_edit.set_field'); state = { available_list: [], rendered_list: [], selected_all: false, ip_filter: '', cn_filter: '', countries: [] }; constructor(props){ super(props); let sync_rendered_list = ()=>this.setState( {rendered_list: this.filter_ips(this.state.available_list)}); this.sync_rendered_list = _.debounce(sync_rendered_list, 300); } componentDidMount(){ this.setdb_on('head.proxy_edit.zone_name', ()=>{ this.setState({available_list: [], rendered_list: []}); }); this.setdb_on('head.proxies_running', proxies=> proxies && this.setState({proxies})); $('#allocated_ips').on('show.bs.modal', this.load); } static getDerivedStateFromProps(props, state){ if (!state.available_list.length) return null; return { selected_all: (props.form[props.type]||[]).length== state.rendered_list.length, }; } close = ()=>$('#allocated_ips').modal('hide'); load = ()=>{ const {type, form} = this.props; if (this.state.available_list.length) return; this.loading(true); let endpoint; if (type=='ips') endpoint = '/api/allocated_ips'; else endpoint = '/api/allocated_vips'; const url = `${endpoint}?zone=${this.props.zone}`; const _this = this; this.etask(function*(){ this.on('uncaught', e=>_this.etask(function*(){ yield report_exception(e, 'alloc_modal.Alloc_modal.load'); _this.loading(false); })); const res = yield ajax.json({url}); const _available_list = type=='ips' ? res.ips_cn : res.vips_cn; const available_hash = _available_list.reduce((acc, value)=>{ acc[value.ip] = value; return acc; }, {}); const chosen_ips = []; const chosen_ips_hash = {}; form[type].forEach(v=>{ const found_ip = available_hash[v]; if (found_ip!==undefined) { chosen_ips.push(found_ip); chosen_ips_hash[v] = found_ip; } }); const not_chosen_ips = []; const countries = new Set(); _available_list.forEach(v=>{ if (chosen_ips_hash[v.ip]===undefined) not_chosen_ips.push(v); const country = v.country || v.maxmind; countries.add(country); }); const available_list = [...chosen_ips, ...not_chosen_ips]; _this.setState({ available_list, countries: [...countries], rendered_list: _this.filter_ips(available_list), }, _this.sync_selected_vals); _this.loading(false); }); }; sync_selected_vals = ()=>{ const curr_vals = this.props.form[this.props.type]; const new_vals = curr_vals.filter(v=> this.state.available_list.find(r=>r.ip==v)); this.set_field(this.props.type, new_vals); }; loading = loading=>{ setdb.set('head.proxy_edit.loading', loading); this.setState({loading}); }; checked = ip=>(this.props.form[this.props.type]||[]).includes(ip); toggle = e=>{ const value = e.rowData.ip; const checked = !this.checked(value); const {type, form} = this.props; let new_alloc; if (checked) { const selected = new Set(); form[type].forEach(v=>selected.add(v)); selected.add(value); new_alloc = this.state.rendered_list .filter(v=>selected.has(v.ip)) .map(v=>v.ip); } else new_alloc = form[type].filter(r=>r!=value); this.set_field(type, new_alloc); this.update_multiply_and_pool_size(new_alloc.length); }; unselect_all = ()=>{ this.set_field(this.props.type, []); this.set_field('pool_size', ''); this.set_field('multiply', 1); }; select_all = ()=>{ const {rendered_list} = this.state; this.set_field(this.props.type, rendered_list.map(r=>r.ip)); this.update_multiply_and_pool_size(rendered_list.length); }; refresh_chosen = ()=>{ if (this.props.type=='ips') this.refresh(this.props.form.ips); else this.refresh(this.props.form.vips); }; refresh_one = val=>{ this.refresh([val]); }; refresh = vals=>{ const _this = this; this.etask(function*(){ this.on('uncaught', e=>_this.etask(function*(){ yield report_exception(e, 'alloc_modal.Alloc_modal.refresh.uncaught'); })); this.on('finally', ()=>{ _this.loading(false); }); _this.loading(true); const data = {zone: _this.props.zone}; if (_this.props.type=='ips') data.ips = vals; else data.vips = vals; const url = '/api/refresh_ips'; const res = yield ajax.json({method: 'POST', url, data}); if (res.error || !res.ips && !res.vips) { return void (yield report_exception(res.error, 'alloc_modal.Alloc_modal.refresh')); } const new_vals = _this.props.type=='ips' ? res.ips : res.vips.map(v=>({...v, ip: v.vip})); const norm_vals = _this.normalize_vals(new_vals); const map = _this.map_vals(norm_vals); const new_ips = _this.props.form.ips.map(val=>map[val]); const new_vips = _this.props.form.vips.map(val=>map[val]); const countries = new Set(); for (let i=0, l=norm_vals.length; i<l; i++) { const val = norm_vals[i]; const country = val.country||val.maxmind; countries.add(country); } _this.setState({ available_list: norm_vals, countries: [...countries], rendered_list: _this.filter_ips(norm_vals), }); _this.set_field('ips', new_ips); _this.set_field('vips', new_vips); yield _this.update_other_proxies(map); }); }; update_other_proxies = map=>{ const _this = this; return this.etask(function*(){ const proxies_to_update = _this.state.proxies.filter(p=> p.zone==_this.props.zone && p.port!=_this.props.form.port && p.proxy_type=='persist'); for (let i=0; i<proxies_to_update.length; i++) { const proxy = proxies_to_update[i]; const new_vals = proxy[_this.props.type].map(v=>map[v]); const data = {port: proxy.port, [_this.props.type]: new_vals}; yield ajax({method: 'POST', url: '/api/update_ips', data}); } }); }; normalize_vals = new_vals=>{ const old_vals = this.state.available_list; if (old_vals.length!=new_vals.length) { this.etask(function*(){ yield report_exception('error ips/vips length mismatch', 'alloc_modal.Alloc_modal.normalize_vals'); return; }); } const old_vals_hash = {}; old_vals.forEach(v=>old_vals_hash[v.ip] = v); const refreshed = []; const stable = {}; for (let i=0, l=new_vals.length; i<l; i++) { const new_val = new_vals[i]; if (old_vals_hash[new_val.ip]!==undefined) stable[new_val.ip] = new_val; else refreshed.push(new_val); } const normalized = []; for (let i=0, l=old_vals.length; i<l; i++) { const old_val = old_vals[i]; if (stable[old_val.ip]!==undefined) normalized.push(old_val); else normalized.push(refreshed.shift()); } return normalized; }; map_vals = normalized=>{ const old_vals = this.state.available_list; const map = {}; for (let i = 0; i < old_vals.length; i++) map[old_vals[i].ip] = normalized[i].ip; return map; }; update_multiply_and_pool_size = size=>{ if (!this.props.form.multiply_ips && !this.props.form.multiply_vips) this.set_field('pool_size', size); else { this.set_field('pool_size', 1); this.set_field('multiply', size); } }; is_refresh_enabled = ()=>{ const {plan, type} = this.props; return type!='ips' || !!plan.ips; }; cols = [ {id: 'ip', title: 'IP'}, ]; on_filter_change(e, field_name){ this.setState({[field_name]: e.target.value}, this.sync_rendered_list); } filter_ips(available_list){ let rows = available_list; const {ip_filter, cn_filter} = this.state; if (ip_filter) rows = rows.filter(r=>r.ip.indexOf(ip_filter)>=0); if (cn_filter) rows = rows.filter(r=>r.country==cn_filter||r.maxmind==cn_filter); return rows; } flag_by_code = code=>code ? flag_with_title(code, code.toUpperCase(), zcountry.code2label(code)) : null; get_extra_cols = ()=>this.props.type=='ips' ? [{id: 'maxmind', title: 'Maxmind'}] : [{id: 'country', title: 'Country'}]; render(){ const type_label = this.props.type=='ips' ? 'IPs' : 'gIPs'; const title = 'Select the '+type_label+' ('+this.props.zone+')'; const refresh_enabled = this.is_refresh_enabled(); const selected_list = this.props.form[this.props.type]||[]; const zones = this.props.zones && this.props.zones.zones || []; const zone = zones.find(z=>z.name==this.props.zone); const refresh_cost = zone && zone.refresh_cost; const total_cost = selected_list.length*refresh_cost; const Footer = <div className="default_footer"> {refresh_enabled && <button onClick={this.refresh_chosen} className="btn btn_lpm" disabled={!selected_list.length}> Refresh {total_cost>0 && ` (${conv.fmt_currency(total_cost)})`} </button> } <button onClick={this.close} className="btn btn_lpm btn_lpm_primary">OK</button> </div>; const sub_title = `IPs: ${selected_list.length}/` +`${this.props.form.pool_size} out of` +` ${this.state.rendered_list.length} available`; return <Modal id="allocated_ips" className="allocated_ips_modal" title={title} footer={Footer}> <Infinite_chrome_table cols={[...this.cols, ...this.get_extra_cols()]} title={sub_title} toolbar={<React.Fragment> <div className="search_box"> <input value={this.state.ip_filter} placeholder="IP filter" onChange={e=>this.on_filter_change(e, 'ip_filter')}/> </div> <Filter vals={this.state.countries} val={this.state.cn_filter} format_text={zcountry.code2label} tooltip="Countries" default_value="All countries" set={e=>this.on_filter_change(e, 'cn_filter')}/> </React.Fragment>} class_name="in_modal_table" selectable toggle={this.toggle} select_all={this.select_all} unselect_all={this.unselect_all} selected_list={selected_list} selected_all={this.state.selected_all} rows={this.state.rendered_list.map(v=>({ ...v, maxmind: this.flag_by_code(v.maxmind), country: this.flag_by_code(v.country) }))} /> </Modal>; } }