UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

596 lines (579 loc) 21 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint react:true*/ import React from 'react'; import Pure_component from '/www/util/pub/pure_component.js'; import React_select from 'react-select/creatable'; import React_tooltip from 'react-tooltip'; import {withRouter} from 'react-router-dom'; import classnames from 'classnames'; import {Netmask} from 'netmask'; import {Typeahead} from 'react-bootstrap-typeahead'; import codemirror from 'codemirror/lib/codemirror'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/lib/codemirror.css'; import setdb from '../../../util/setdb.js'; import ajax from '../../../util/ajax.js'; import zurl from '../../../util/url.js'; import Tooltip from './tooltip.js'; import {T} from './i18n.js'; import {Ext_tooltip, Loader} from '../common.js'; import Zone_description from './zone_desc.js'; import {Modal_dialog} from './modals.js'; const ANY_IP = '0.0.0.0/0'; export class Pins extends Pure_component { state = { pins: [], max_id: 0, modal_open: false, pending: this.props.pending||[], disabled: false, }; static getDerivedStateFromProps(props, state){ if (state.disabled==props.disabled && (props.val==state.raw_val||!props.val)) { return null; } const ips = props.val||[]; const disabled_ips = props.disabled_ips||[]; const pins = ips.map((p, id)=>({ id, val: p, edit: false, disabled: props.disabled || !!disabled_ips.find(i=>i==p), })); if (!props.disabled) { const edited_pin = state.pins.find(p=>p.edit); if (edited_pin) pins.push(edited_pin); } return { raw_val: props.val, pins, max_id: ips.length, disabled: props.disabled, }; } add_pin = (pin='')=>{ this.setState(prev=>({ pins: [...prev.pins, {id: prev.max_id+1, val: pin, edit: true}], max_id: prev.max_id+1, })); if (pin && this.state.pending.includes(pin)) this.setState({pending: this.state.pending.filter(p=>p!=pin)}); }; add_empty_pin = ()=>{ this.add_pin(); }; remove = id=>{ this.setState(prev=>({ pins: prev.pins.filter(p=>p.id!=id), }), this.fire_on_change); }; set_edit = (id, edit)=>{ this.setState(prev=>({ pins: prev.pins.map(p=>{ if (p.id!=id) return p; return {...p, edit}; }), })); }; update_pin = (id, val)=>{ this.setState(prev=>({ pins: prev.pins.map(p=>{ if (p.id!=id) return p; return {...p, val}; }), })); }; fire_on_change = ()=>{ const val = this.state.pins.map(p=>p.val); this.props.on_change_wrapper(val); }; save_pin = (id, val)=>{ if (this.state.pins.find(p=>p.id!=id && p.val==val)) return this.remove(id); this.setState(prev=>({ pins: prev.pins.map(p=>{ if (p.id!=id) return p; return {...p, val, edit: false}; }), }), this.fire_on_change); }; dismiss_modal = ()=>this.setState({modal_open: false}); open_modal = ()=>this.setState({modal_open: true}); render(){ const {pending, disabled, pins} = this.state; const pending_btn_title = `Add recent IPs (${pending.length})`; const has_any = pins.find(p=>p.val==ANY_IP); return <div className="pins_field"> <div className="pins"> {pins.map(p=> <Pin key={p.id} update_pin={this.update_pin} id={p.id} set_edit={this.set_edit} edit={p.edit} exact={this.props.exact} save_pin={this.save_pin} show_any={!this.props.no_any && !has_any} remove={this.remove} disabled={p.disabled}> {p.val} </Pin> )} </div> <T>{t=><Pin_btn title={t('Add IP')} tooltip={t('Add new IP to the list')} on_click={this.add_empty_pin} disabled={disabled}/>}</T> {!!pending.length && !disabled && <Pin_btn on_click={this.open_modal} title={pending_btn_title}/> } <Modal_dialog title="Add recent IPs" open={this.state.modal_open} ok_clicked={this.dismiss_modal} no_cancel_btn> {pending.map(ip=> <Add_pending_btn key={ip} ip={ip} add_pin={this.add_pin}/> )} {!pending.length && <span>No more pending IPs to whitelist</span> } </Modal_dialog> </div>; } } const Add_pending_btn = ({ip, add_pin})=> <div> <span style={{marginRight: 10}}>{ip}</span> <Pin_btn title="Add IP" on_click={()=>add_pin(ip)}/> </div>; class Pin extends Pure_component { input = React.createRef(); componentDidMount(){ this.input.current.focus(); } componentDidUpdate(){ if (this.props.edit) this.input.current.focus(); } edit = ()=>{ if (!this.props.disabled) this.props.set_edit(this.props.id, true); }; key_up = e=>{ if (e.keyCode==13) this.validate_and_save(); }; validate_and_save = ()=>{ let val = (this.props.children||'').trim(); if (this.props.exact && val) return this.props.save_pin(this.props.id, val); try { const netmask = new Netmask(val); val = netmask.base; if (netmask.bitmask!=32) val += '/'+netmask.bitmask; } catch(e){ val = ''; } if (!val) return this.props.remove(this.props.id); this.props.save_pin(this.props.id, val); }; on_change = e=>this.props.update_pin(this.props.id, e.target.value); on_any_click = ()=>{ this.props.update_pin(this.props.id, ANY_IP); this.setState({children: ANY_IP}, this.validate_and_save); }; remove = ()=>this.props.remove(this.props.id); on_blur = e=>{ const target = e.relatedTarget; const any_click = target && target.classList.contains('any'); if (!any_click) this.validate_and_save(); }; get_label = ip=>ip==ANY_IP ? 'any' : ip; render(){ const {children, edit, disabled, show_any} = this.props; const input_classes = classnames({hidden: !edit}); return <div className={classnames('pin', {active: edit, disabled})} onBlur={this.on_blur}> {!disabled && <div className="x" onClick={this.remove}> <div className="glyphicon glyphicon-remove"/> </div>} <div className="content" onClick={this.edit}> {!edit && this.get_label(children)} <input ref={this.input} type="text" value={children} onChange={this.on_change} className={input_classes} onKeyUp={this.key_up}/> </div> {edit && show_any && <button className="any" onClick={this.on_any_click}> <T>any</T> </button> } {edit && <div className="v"> <div className="glyphicon glyphicon-ok"/> </div> } </div>; } } export const Pin_btn = ({on_click, title, tooltip, disabled})=> <Tooltip title={tooltip}> <button className="btn btn_lpm btn_lpm_small add_pin" onClick={on_click} disabled={disabled}> {title} <i className="glyphicon glyphicon-plus"/> </button> </Tooltip>; export class Select_status extends Pure_component { status_types = ['200', '2..', '403', '404', '500', '503', '(4|5)..']; value_to_option = (t, value)=>{ if (!value) return {value: '', label: t('--Select--')}; return {value, label: value}; }; on_change = e=>this.props.on_change_wrapper(e && e.value || ''); render(){ return <T>{t=><Select_multiple {...this.props} class_name="status" options={this.status_types.map(v=>this.value_to_option(t, v))} on_change={this.on_change} validation={v=>!!v} value_to_option={this.value_to_option.bind(this, t)}/>}</T>; } } export class Select_number extends Pure_component { _fmt_num = n=>n && n.toLocaleString({useGrouping: true}) || n; _get_data = ()=>this.props.data ? this.props.data : this.opt_from_range(); value_to_option = value=>{ if (value==null) return false; const label = value==0 ? <T>Disabled</T> : this._fmt_num(+value); return {value, label}; }; opt_from_range = ()=>{ let res; if (this.props.range=='medium') res = [0, 1, 10, 100, 1000]; else if (this.props.range=='ms') res = [0, 500, 2000, 5000, 10000]; else res = [0, 1, 3, 5, 10, 20]; return res; }; on_change = e=>{ let value = e && +e.value || ''; const allow_zero = this._get_data().includes(0); if (!value && !allow_zero) value = this.props.default||1; this.props.on_change_wrapper(value); }; validation = s=>!!s && Number(s)==s; render(){ const data = this._get_data(); const options = data.map(this.value_to_option); return <Select_multiple {...this.props} options={options} on_change={this.on_change} validation={this.validation} value_to_option={this.value_to_option} no_options_message={()=>'You can use only numbers here'}/>; } } class Select_multiple extends Pure_component { styles = { clearIndicator: base=>({ ...base, padding: '1px', }), dropdownIndicator: base=>({ ...base, padding: '1px', }), option: (base, state)=>({ ...base, padding: '2px 12px', backgroundColor: state.isFocused ? '#f5f5f5' : 'white', color: '#004d74', }), control: (_, state)=>({ alignItems: 'center', display: 'flex', height: 32, borderRadius: 3, border: 'solid 1px', borderColor: state.isFocused ? '#004d74' : state.isDisabled ? '#e0e9ee' : '#ccdbe3', backgroundColor: state.isDisabled ? '#f5f5f5;' : 'white', }), singleValue: (base, state)=>({ ...base, color: state.isDisabled ? '#8e8e8e' : '#004d74', }), }; render(){ return <React_select styles={this.styles} className={classnames('select_multiple', this.props.class_name)} isClearable noOptionsMessage={this.props.no_options_message} classNamePrefix="react_select" value={this.props.value_to_option(this.props.val)} onChange={this.props.on_change} options={this.props.options} isValidNewOption={this.props.validation} pageSize={9} placeholder={this.props.placeholder} blurInputOnSelect={true} isDisabled={this.props.disabled}/>; } } export class Yes_no extends Pure_component { options = t=>{ const default_label = this.props.default ? 'Yes' : 'No'; return [ {key: 'No', value: false}, {key: t('Default')+' ('+t(default_label)+')', value: ''}, {key: 'Yes', value: true}, ]; }; render(){ return <T>{t=><Select {...this.props} data={this.options(t)}/>}</T>; } } export class Regex extends Pure_component { state = {recognized: false, checked: {}, invalid_regexp: undefined}; formats = ['png', 'jpg', 'jpeg', 'svg', 'gif', 'mp3', 'mp4', 'avi']; componentDidMount(){ this.recognize_regexp(); } componentDidUpdate(prev_props){ if (prev_props.val!=this.props.val) this.recognize_regexp(); } classes = f=>{ const active = this.state.recognized && this.state.checked[f]; return classnames('check', {active}); }; toggle = f=>{ this.setState( prev=>({checked: {...prev.checked, [f]: !prev.checked[f]}}), this.gen_regexp); }; recognize_regexp = ()=>{ const m = this.props.val && this.props.val.match(/\\\.\((.+)\)\$/); if (m&&m[1]) { const checked = m[1].split('|').reduce( (acc, e)=>({...acc, [e]: true}), {}); this.setState({recognized: true, checked}); } else this.setState({recognized: false, checked: {}}); }; gen_regexp = ()=>{ const formats = Object.keys(this.state.checked) .filter(f=>this.state.checked[f]).join('|'); let regexp = ''; if (formats) regexp = `\\.(${formats})$`; this.props.on_change_wrapper(regexp, this.props.id); }; tip = f=>{ if (this.state.checked[f]) return `Remove file format ${f} from regexp`; return `Add file format ${f} to regexp`; }; check_regexp = regexp=>{ try { new RegExp(regexp); return true; } catch(e){ return false; } }; on_input_change = regexp=>{ const is_regexp_valid = this.check_regexp(regexp); this.setState({invalid_regexp: !is_regexp_valid ? regexp : undefined}); if (is_regexp_valid) this.props.on_change_wrapper(regexp, this.props.id); }; render(){ const val = this.props.val||''; return <div className="regex_field"> <div className="regex_input"> {!this.props.no_tip_box && <div className="tip_box active"> <div className="checks"> {this.formats.map(f=> <Tooltip key={f+!!this.state.checked[f]} title={this.tip(f)}> <button onClick={this.toggle.bind(null, f)} className={this.classes(f)} disabled={this.props.disabled|| !!this.state.invalid_regexp}>.{f}</button> </Tooltip> )} </div> </div>} <Input className="regex" {...this.props} val={this.state.invalid_regexp||val} type="text" on_change_wrapper={this.on_input_change}/> <span className={classnames('regex_error', {active: this.state.invalid_regexp})}>Invalid regex</span> </div> </div>; } } export class Json extends Pure_component { state = {}; componentDidMount(){ this.cm = codemirror.fromTextArea(this.textarea, {mode: 'javascript'}); this.cm.on('change', this.on_cm_change); this.cm.setSize('auto', '100%'); this.cm.doc.setValue(this.props.val); } on_cm_change = cm=>{ const new_val = cm.doc.getValue(); let correct = true; try { JSON.parse(new_val); } catch(e){ correct = false; } if (correct) this.props.on_change_wrapper(new_val); this.setState({correct}); }; set_ref = ref=>{ this.textarea = ref; }; render(){ const classes = classnames('code_mirror_wrapper', 'json', {error: !this.state.correct}); return <Tooltip title={this.props.tooltip}> <div className={classes}> <textarea ref={this.set_ref}/> </div> </Tooltip>; } } export class Url_input extends Pure_component { constructor(props){ super(props); this.state = {url: props.val, valid: true}; } on_url_change = (url, id)=>{ const valid = zurl.is_valid_url(url); if (valid) this.props.on_change_wrapper(url, id); this.setState({url, valid}); }; render(){ const input_props = Object.assign({}, this.props, { val: this.state.url, on_change_wrapper: this.on_url_change, className: classnames({error: !this.state.valid}), }); return <Input {...input_props}/>; } } export const Textarea = props=>{ return <textarea value={props.val} rows={props.rows||3} placeholder={props.placeholder} onChange={e=>props.on_change_wrapper(e.target.value)}/>; }; export const Typeahead_wrapper = props=> <Typeahead id={props.id} options={props.data} maxResults={10} minLength={1} disabled={props.disabled} selectHintOnEnter onChange={props.on_change_wrapper} selected={props.val} onInputChange={props.on_input_change} filterBy={props.filter_by}/>; export const Select = props=>{ const update = val=>{ if (val=='true') val = true; else if (val=='false') val = false; if (props.on_change_wrapper) props.on_change_wrapper(val); }; const conf = (props.data||[]).find(c=>c.value==props.val); return <Tooltip key={props.val} title={conf&&conf.tooltip||''}> <T>{t=><select value={''+props.val} onChange={e=>update(e.target.value)} disabled={props.disabled}> {(props.data||[]).map((c, i)=> <option key={i} value={c.value||c}>{t(c.key||c)}</option> )} </select>}</T> </Tooltip>; }; export const Input = props=>{ const update = val=>{ if (props.type=='number' && val) val = Number(val); if (props.on_change_wrapper) props.on_change_wrapper(val, props.id); }; return <T>{t=><input style={props.style} type={props.type} value={props.val} disabled={props.disabled} onChange={e=>update(e.target.value)} className={props.className} placeholder={t(props.placeholder)} onBlur={props.on_blur} onKeyUp={props.on_key_up}/>}</T>; }; export const Select_zone = withRouter( class Select_zone extends Pure_component { state = {refreshing_zones: false, zones: {zones: []}}; componentDidMount(){ this.setdb_on('head.zones', zones=>{ if (zones) this.setState({zones}); }); } refresh_zones = ()=>{ const _this = this; this.etask(function*(){ this.on('uncaught', ()=>{ _this.setState({refreshing_zones: false}); }); _this.setState({refreshing_zones: true}); const result = yield window.fetch('/api/refresh_zones', {method: 'POST'}); if (result.status!=200) return _this.props.history.push({pathname: '/login'}); const zones = yield ajax.json({url: '/api/zones'}); _this.setState({refreshing_zones: false}); setdb.set('head.zones', zones); }); }; render(){ const {val, on_change_wrapper, disabled, preview} = this.props; const tooltip = preview ? '' : this.props.tooltip; const zone_opt = this.state.zones.zones.map(z=>{ if (z.name==this.state.zones.def) return {key: `Default (${z.name})`, value: z.name}; return {key: z.name, value: z.name}; }); const selected = val || this.state.zones.def; return <div className="select_zone"> <Tooltip title={tooltip}> <span data-tip data-for="zone-tip"> {preview && <React_tooltip id="zone-tip" type="light" effect="solid" place="bottom" delayHide={0} delayUpdate={300}> {disabled ? <Ext_tooltip/> : <div className="zone_tooltip"> <Zone_description zone_name={selected}/> </div> } </React_tooltip> } <Select val={selected} type="select" on_change_wrapper={on_change_wrapper} label="Default zone" tooltip={tooltip} data={zone_opt} disabled={disabled}/> </span> </Tooltip> <T>{t=><Tooltip title={t('Refresh zones')}> <div className="chrome_icon refresh" onClick={this.refresh_zones}/> </Tooltip>}</T> <Loader show={this.state.refreshing_zones}/> </div>; } });