refactor(hooks): here be dragons!
This commit is contained in:
parent
7f75345c03
commit
e3afe5ff90
17 changed files with 389 additions and 390 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,7 +1,8 @@
|
|||
*.*~
|
||||
.DS_Store
|
||||
node_modules
|
||||
node_modules/
|
||||
public/
|
||||
experimental/
|
||||
deploy_ghpages.sh
|
||||
tags
|
||||
*.log
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"memoize-one": "^4.0.2",
|
||||
"modern-normalize": "^0.5.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.7.0-alpha.0",
|
||||
"react": "16.7.0-alpha.0",
|
||||
"react-cache": "^2.0.0-alpha.0",
|
||||
"react-dom": "^16.7.0-alpha.0",
|
||||
"react-modal": "^3.6.1",
|
||||
|
|
|
@ -53,7 +53,8 @@ const store = {
|
|||
function pump(reader) {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
console.log('done');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GET /logs streaming done');
|
||||
return;
|
||||
}
|
||||
const t = textDecoder.decode(value);
|
||||
|
@ -62,6 +63,7 @@ function pump(reader) {
|
|||
try {
|
||||
store.appendData(JSON.parse(s));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('JSON.parse error', JSON.parse(s));
|
||||
}
|
||||
});
|
||||
|
@ -80,7 +82,8 @@ function fetchLogs() {
|
|||
})
|
||||
.catch(err => {
|
||||
store.fetched = false;
|
||||
console.log('Error', err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GET /logs error', err);
|
||||
});
|
||||
return store;
|
||||
}
|
||||
|
|
|
@ -54,7 +54,8 @@ let fetched = false;
|
|||
function pump(reader) {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
console.log('done');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GET /traffic streaming done');
|
||||
fetched = false;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -16,4 +16,5 @@ root.render(<Root />);
|
|||
// };
|
||||
// render(Root, props);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Checkout the repo: https://github.com/haishanh/yacd');
|
||||
|
|
|
@ -1,122 +1,105 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useComponentState, useActions } from 'm/store';
|
||||
|
||||
import Input from 'c/Input';
|
||||
import Button from 'c/Button';
|
||||
|
||||
import s0 from './APIConfig.module.scss';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { getClashAPIConfig, updateClashAPIConfig } from 'd/app';
|
||||
|
||||
const mapStateToProps = s => {
|
||||
const apiConfig = getClashAPIConfig(s);
|
||||
return { apiConfig };
|
||||
};
|
||||
const mapStateToProps = s => ({
|
||||
apiConfig: getClashAPIConfig(s)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
updateClashAPIConfig: bindActionCreators(updateClashAPIConfig, dispatch)
|
||||
};
|
||||
};
|
||||
function APIConfig2() {
|
||||
const { apiConfig } = useComponentState(mapStateToProps);
|
||||
const [hostname, setHostname] = useState(apiConfig.hostname);
|
||||
const [port, setPort] = useState(apiConfig.port);
|
||||
const [secret, setSecret] = useState(apiConfig.secret);
|
||||
const actions = useActions({ updateClashAPIConfig });
|
||||
|
||||
class APIConfig extends Component {
|
||||
static propTypes = {
|
||||
apiConfig: PropTypes.object.isRequired,
|
||||
updateClashAPIConfig: PropTypes.func.isRequired
|
||||
};
|
||||
const contentEl = useRef(null);
|
||||
useEffect(() => {
|
||||
contentEl.current.focus();
|
||||
}, []);
|
||||
|
||||
state = {
|
||||
hostname: this.props.apiConfig.hostname,
|
||||
port: this.props.apiConfig.port,
|
||||
secret: this.props.apiConfig.secret
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.content.focus();
|
||||
}
|
||||
|
||||
handleInputOnChange = e => {
|
||||
const handleInputOnChange = e => {
|
||||
const target = e.target;
|
||||
const { name } = target;
|
||||
let value = target.value.trim();
|
||||
|
||||
let value;
|
||||
if (name === 'port') {
|
||||
if (Number(target.value) < 0 || Number(target.value) > 65535) return;
|
||||
}
|
||||
value = target.value.trim();
|
||||
if (value === '') return;
|
||||
this.setState({ [name]: value });
|
||||
switch (name) {
|
||||
case 'port':
|
||||
if (Number(value) < 0 || Number(value) > 65535) return;
|
||||
setPort(value);
|
||||
break;
|
||||
case 'hostname':
|
||||
setHostname(value);
|
||||
break;
|
||||
case 'secret':
|
||||
setSecret(value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
updateClashAPIConfig() {
|
||||
const { hostname, port, secret } = this.state;
|
||||
this.props.updateClashAPIConfig({ hostname, port, secret });
|
||||
function updateConfig() {
|
||||
actions.updateClashAPIConfig({ hostname, port, secret });
|
||||
}
|
||||
|
||||
handleConfirmOnClick = () => {
|
||||
this.updateClashAPIConfig();
|
||||
};
|
||||
|
||||
handleContentOnKeyDown = e => {
|
||||
function handleContentOnKeyDown(e) {
|
||||
// enter keyCode is 13
|
||||
if (e.keyCode !== 13) return;
|
||||
this.updateClashAPIConfig();
|
||||
};
|
||||
updateConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hostname, port, secret } = this.state;
|
||||
return (
|
||||
<div
|
||||
className={s0.root}
|
||||
ref={e => (this.content = e)}
|
||||
tabIndex="1"
|
||||
onKeyDown={this.handleContentOnKeyDown}
|
||||
>
|
||||
<div className={s0.header}>RESTful API config for Clash</div>
|
||||
<div className={s0.body}>
|
||||
<div className={s0.group}>
|
||||
<div className={s0.label}>Hostname and Port</div>
|
||||
<div className={s0.inputs}>
|
||||
<Input
|
||||
type="text"
|
||||
name="hostname"
|
||||
placeholder="Hostname"
|
||||
value={hostname}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
name="port"
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s0.group}>
|
||||
<div className={s0.label}>Authorization Secret (Optional)</div>
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
name="secret"
|
||||
value={secret}
|
||||
placeholder="Optional"
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={s0.root}
|
||||
ref={contentEl}
|
||||
tabIndex="1"
|
||||
onKeyDown={handleContentOnKeyDown}
|
||||
>
|
||||
<div className={s0.header}>RESTful API config for Clash</div>
|
||||
<div className={s0.body}>
|
||||
<div className={s0.group}>
|
||||
<div className={s0.label}>Hostname and Port</div>
|
||||
<div className={s0.inputs}>
|
||||
<Input
|
||||
type="text"
|
||||
name="hostname"
|
||||
placeholder="Hostname"
|
||||
value={hostname}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
name="port"
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s0.footer}>
|
||||
<Button label="Confirm" onClick={this.handleConfirmOnClick} />
|
||||
<div className={s0.group}>
|
||||
<div className={s0.label}>Authorization Secret (Optional)</div>
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
name="secret"
|
||||
value={secret}
|
||||
placeholder="Optional"
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={s0.footer}>
|
||||
<Button label="Confirm" onClick={updateConfig} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(APIConfig);
|
||||
export default APIConfig2;
|
||||
|
|
|
@ -1,53 +1,36 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useActions, useComponentState } from 'm/store';
|
||||
|
||||
import Modal from 'c/Modal';
|
||||
import APIConfig from 'c/APIConfig';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { closeModal } from 'd/modals';
|
||||
import { fetchConfigs } from 'd/configs';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const modals = state.modals;
|
||||
return { modals };
|
||||
const mapStateToProps = s => ({
|
||||
modals: s.modals
|
||||
});
|
||||
|
||||
const actions = {
|
||||
closeModal,
|
||||
fetchConfigs
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
closeModal: bindActionCreators(closeModal, dispatch),
|
||||
fetchConfigs: bindActionCreators(fetchConfigs, dispatch)
|
||||
};
|
||||
};
|
||||
export default function APIDiscovery() {
|
||||
const { modals } = useComponentState(mapStateToProps);
|
||||
const { closeModal, fetchConfigs } = useActions(actions);
|
||||
useEffect(() => {
|
||||
fetchConfigs();
|
||||
}, []);
|
||||
|
||||
class APIDiscovery extends Component {
|
||||
static propTypes = {
|
||||
closeModal: PropTypes.func,
|
||||
fetchConfigs: PropTypes.func,
|
||||
modals: PropTypes.object
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchConfigs();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { modals, closeModal } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modals.apiConfig}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
shouldCloseOnEsc={false}
|
||||
onRequestClose={() => closeModal('apiConfig')}
|
||||
>
|
||||
<APIConfig />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modals.apiConfig}
|
||||
shouldCloseOnOverlayClick={false}
|
||||
shouldCloseOnEsc={false}
|
||||
onRequestClose={() => closeModal('apiConfig')}
|
||||
>
|
||||
<APIConfig />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(APIDiscovery);
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useComponentState, useActions } from 'm/store';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { getConfigs, fetchConfigs, updateConfigs } from 'd/configs';
|
||||
|
||||
import ContentHeader from 'c/ContentHeader';
|
||||
|
||||
import Switch from 'c/Switch';
|
||||
import ToggleSwitch from 'c/ToggleSwitch';
|
||||
import Input from 'c/Input';
|
||||
|
@ -51,29 +48,16 @@ const actions = {
|
|||
updateConfigs
|
||||
};
|
||||
|
||||
const mapStateToProps = s => {
|
||||
return {
|
||||
configs: getConfigs(s)
|
||||
};
|
||||
};
|
||||
const mapStateToProps = s => ({ configs: getConfigs(s) });
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return bindActionCreators(actions, dispatch);
|
||||
};
|
||||
export default function Config2() {
|
||||
const { fetchConfigs, updateConfigs } = useActions(actions);
|
||||
const { configs } = useComponentState(mapStateToProps);
|
||||
useEffect(() => {
|
||||
fetchConfigs();
|
||||
}, []);
|
||||
|
||||
class Config extends Component {
|
||||
static propTypes = {
|
||||
configs: PropTypes.object,
|
||||
fetchConfigs: PropTypes.func,
|
||||
updateConfigs: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchConfigs();
|
||||
}
|
||||
|
||||
handleInputOnChange = ev => {
|
||||
const { configs } = this.props;
|
||||
function handleInputOnChange(ev) {
|
||||
const target = ev.target;
|
||||
const { name } = target;
|
||||
|
||||
|
@ -89,77 +73,69 @@ class Config extends Component {
|
|||
value = target.value;
|
||||
}
|
||||
if (configs[name] === value) return;
|
||||
this.props.updateConfigs({ [name]: value });
|
||||
};
|
||||
updateConfigs({ [name]: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { configs } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Config" />
|
||||
<div className={s0.root}>
|
||||
<div>
|
||||
<div className={s0.label}>HTTP Proxy Port</div>
|
||||
<Input
|
||||
name="port"
|
||||
value={configs.port}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Config" />
|
||||
<div className={s0.root}>
|
||||
<div>
|
||||
<div className={s0.label}>HTTP Proxy Port</div>
|
||||
<Input
|
||||
name="port"
|
||||
value={configs.port}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={s0.label}>SOCKS5 Proxy Port</div>
|
||||
<Input
|
||||
name="socket-port"
|
||||
value={configs['socket-port']}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>SOCKS5 Proxy Port</div>
|
||||
<Input
|
||||
name="socket-port"
|
||||
value={configs['socket-port']}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={s0.label}>Redir Port</div>
|
||||
<Input
|
||||
name="redir-port"
|
||||
value={configs['redir-port']}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>Redir Port</div>
|
||||
<Input
|
||||
name="redir-port"
|
||||
value={configs['redir-port']}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={s0.label}>Allow LAN</div>
|
||||
<Switch
|
||||
name="allow-lan"
|
||||
checked={configs['allow-lan']}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>Allow LAN</div>
|
||||
<Switch
|
||||
name="allow-lan"
|
||||
checked={configs['allow-lan']}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={s0.label}>Mode</div>
|
||||
<ToggleSwitch
|
||||
options={optionsRule}
|
||||
name="mode"
|
||||
value={configs.mode}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>Mode</div>
|
||||
<ToggleSwitch
|
||||
options={optionsRule}
|
||||
name="mode"
|
||||
value={configs.mode}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={s0.label}>Log Level</div>
|
||||
<ToggleSwitch
|
||||
options={optionsLogLevel}
|
||||
name="log-level"
|
||||
value={configs['log-level']}
|
||||
onChange={this.handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>Log Level</div>
|
||||
<ToggleSwitch
|
||||
options={optionsLogLevel}
|
||||
name="log-level"
|
||||
value={configs['log-level']}
|
||||
onChange={handleInputOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Config);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useActions, useComponentState } from 'm/store';
|
||||
|
||||
import ContentHeader from 'c/ContentHeader';
|
||||
import ProxyGroup from 'c/ProxyGroup';
|
||||
|
@ -7,8 +7,6 @@ import Button from 'c/Button';
|
|||
|
||||
import s0 from 'c/Proxies.module.scss';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
getUserProxies,
|
||||
getProxyGroupNames,
|
||||
|
@ -16,54 +14,38 @@ import {
|
|||
requestDelayAll
|
||||
} from 'd/proxies';
|
||||
|
||||
function mapStateToProps(s) {
|
||||
return {
|
||||
proxies: getUserProxies(s),
|
||||
groupNames: getProxyGroupNames(s)
|
||||
};
|
||||
}
|
||||
const mapStateToProps = s => ({
|
||||
proxies: getUserProxies(s),
|
||||
groupNames: getProxyGroupNames(s)
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
fetchProxies: bindActionCreators(fetchProxies, dispatch),
|
||||
requestDelayAll: bindActionCreators(requestDelayAll, dispatch)
|
||||
};
|
||||
}
|
||||
const actions = {
|
||||
fetchProxies,
|
||||
requestDelayAll
|
||||
};
|
||||
|
||||
class Proxies extends Component {
|
||||
static propTypes = {
|
||||
groupNames: PropTypes.array.isRequired,
|
||||
fetchProxies: PropTypes.func.isRequired,
|
||||
requestDelayAll: PropTypes.func.isRequired
|
||||
};
|
||||
export default function Proxies() {
|
||||
const { fetchProxies, requestDelayAll } = useActions(actions);
|
||||
useEffect(() => {
|
||||
fetchProxies();
|
||||
}, []);
|
||||
const { groupNames } = useComponentState(mapStateToProps);
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchProxies();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { groupNames, requestDelayAll } = this.props;
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Proxies" />
|
||||
<div>
|
||||
<ContentHeader title="Proxies" />
|
||||
<div>
|
||||
<div className={s0.btnGroup}>
|
||||
<Button label="Test Latency" onClick={requestDelayAll} />
|
||||
</div>
|
||||
{groupNames.map(groupName => {
|
||||
return (
|
||||
<div className={s0.group} key={groupName}>
|
||||
<ProxyGroup name={groupName} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={s0.btnGroup}>
|
||||
<Button label="Test Latency" onClick={requestDelayAll} />
|
||||
</div>
|
||||
{groupNames.map(groupName => {
|
||||
return (
|
||||
<div className={s0.group} key={groupName}>
|
||||
<ProxyGroup name={groupName} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Proxies);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useComponentState } from 'm/store';
|
||||
|
||||
import Icon from 'c/Icon';
|
||||
import ProxyLatency from 'c/ProxyLatency';
|
||||
|
@ -12,7 +13,6 @@ import fallback from 's/fallback.svg';
|
|||
|
||||
import s0 from './Proxy.module.scss';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { getDelay, getUserProxies } from 'd/proxies';
|
||||
|
||||
const colors = {
|
||||
|
@ -40,38 +40,32 @@ const mapStateToProps = s => {
|
|||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = null;
|
||||
function Proxy({ now, name }) {
|
||||
const { proxies, delay } = useComponentState(mapStateToProps);
|
||||
|
||||
class Proxy extends Component {
|
||||
static propTypes = {
|
||||
now: PropTypes.bool,
|
||||
delay: PropTypes.object,
|
||||
proxies: PropTypes.object,
|
||||
name: PropTypes.string
|
||||
};
|
||||
// const { name, proxies, delay, now } = this.props;
|
||||
const latency = delay[name];
|
||||
const proxy = proxies[name];
|
||||
const color = now ? colors[proxy.type] : '#555';
|
||||
const iconId = icons[proxy.type];
|
||||
|
||||
render() {
|
||||
const { name, proxies, delay, now } = this.props;
|
||||
const latency = delay[name];
|
||||
const proxy = proxies[name];
|
||||
const color = now ? colors[proxy.type] : '#555';
|
||||
const iconId = icons[proxy.type];
|
||||
|
||||
return (
|
||||
<div className={s0.proxy}>
|
||||
<div className={s0.left} style={{ color }}>
|
||||
<Icon id={iconId} width={80} height={80} />
|
||||
</div>
|
||||
<div className={s0.right}>
|
||||
<div className={s0.proxyName}>{name}</div>
|
||||
{latency ? <ProxyLatency latency={latency} /> : null}
|
||||
</div>
|
||||
return (
|
||||
<div className={s0.proxy}>
|
||||
<div className={s0.left} style={{ color }}>
|
||||
<Icon id={iconId} width={80} height={80} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={s0.right}>
|
||||
<div className={s0.proxyName}>{name}</div>
|
||||
{latency ? <ProxyLatency latency={latency} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Proxy.propTypes = {
|
||||
now: PropTypes.bool,
|
||||
delay: PropTypes.object,
|
||||
proxies: PropTypes.object,
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Proxy);
|
||||
export default Proxy;
|
||||
|
|
|
@ -1,80 +1,62 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import memoize from 'memoize-one';
|
||||
import { useActions, useComponentState } from 'm/store';
|
||||
|
||||
import Proxy from 'c/Proxy';
|
||||
|
||||
import s0 from './ProxyGroup.module.scss';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { getUserProxies, switchProxy } from 'd/proxies';
|
||||
|
||||
const mapStateToProps = s => {
|
||||
return {
|
||||
proxies: getUserProxies(s)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
switchProxy: bindActionCreators(switchProxy, dispatch)
|
||||
};
|
||||
};
|
||||
const mapStateToProps = s => ({
|
||||
proxies: getUserProxies(s)
|
||||
});
|
||||
|
||||
// should move this to sth like constants.js
|
||||
// const userProxyTypes = ['Shadowsocks', 'Vmess', 'Socks5'];
|
||||
export default function ProxyGroup2({ name }) {
|
||||
const { proxies } = useComponentState(mapStateToProps);
|
||||
const actions = useActions({ switchProxy });
|
||||
const group = proxies[name];
|
||||
const { all, now } = group;
|
||||
const list = useMemo(
|
||||
() => {
|
||||
if (all) {
|
||||
const a = [now];
|
||||
all.forEach(i => i !== now && a.push(i));
|
||||
return a;
|
||||
} else {
|
||||
return [now];
|
||||
}
|
||||
},
|
||||
[all, now]
|
||||
);
|
||||
|
||||
class ProxyGroup extends Component {
|
||||
static propTypes = {
|
||||
// group name
|
||||
name: PropTypes.string.isRequired,
|
||||
proxies: PropTypes.object,
|
||||
switchProxy: PropTypes.func
|
||||
};
|
||||
|
||||
reOrderProxies = memoize((list, now) => {
|
||||
const a = [now];
|
||||
list.forEach(i => i !== now && a.push(i));
|
||||
return a;
|
||||
});
|
||||
|
||||
render() {
|
||||
const { name, proxies, switchProxy } = this.props;
|
||||
const group = proxies[name];
|
||||
let list;
|
||||
if (group.all) {
|
||||
list = this.reOrderProxies(group.all, group.now);
|
||||
} else {
|
||||
list = [group.now];
|
||||
}
|
||||
return (
|
||||
<div className={s0.group}>
|
||||
<div className={s0.header}>
|
||||
<h2>
|
||||
<span>{name}</span>
|
||||
<span>{group.type}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className={s0.list}>
|
||||
{list.map(proxyName => {
|
||||
return (
|
||||
<div
|
||||
className={s0.proxy}
|
||||
key={proxyName}
|
||||
onClick={() => switchProxy(name, proxyName)}
|
||||
>
|
||||
<Proxy name={proxyName} now={proxyName === group.now} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<div className={s0.group}>
|
||||
<div className={s0.header}>
|
||||
<h2>
|
||||
<span>{name}</span>
|
||||
<span>{group.type}</span>
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={s0.list}>
|
||||
{list.map(proxyName => {
|
||||
return (
|
||||
<div
|
||||
className={s0.proxy}
|
||||
key={proxyName}
|
||||
onClick={() => actions.switchProxy(name, proxyName)}
|
||||
>
|
||||
<Proxy name={proxyName} now={proxyName === group.now} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ProxyGroup);
|
||||
ProxyGroup2.propTypes = {
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider as StoreProvider } from 'm/store';
|
||||
import { HashRouter as Router, Route } from 'react-router-dom';
|
||||
// import { hot } from 'react-hot-loader';
|
||||
// import createHistory from 'history/createHashHistory';
|
||||
|
@ -21,21 +22,25 @@ import { store } from '../store/configureStore';
|
|||
import './Root.scss';
|
||||
import s0 from './Root.module.scss';
|
||||
|
||||
window.store = store;
|
||||
|
||||
const Root = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div className={s0.app}>
|
||||
<APIDiscovery />
|
||||
<Route path="/" component={SideBar} />
|
||||
<div className={s0.content}>
|
||||
<Route exact path="/" render={() => <Home />} />
|
||||
<Route exact path="/overview" render={() => <Home />} />
|
||||
<Route exact path="/configs" render={() => <Config />} />
|
||||
<Route exact path="/logs" render={() => <Logs />} />
|
||||
<Route exact path="/proxies" render={() => <Proxies />} />
|
||||
<StoreProvider store={store}>
|
||||
<Router>
|
||||
<div className={s0.app}>
|
||||
<APIDiscovery />
|
||||
<Route path="/" component={SideBar} />
|
||||
<div className={s0.content}>
|
||||
<Route exact path="/" render={() => <Home />} />
|
||||
<Route exact path="/overview" render={() => <Home />} />
|
||||
<Route exact path="/configs" render={() => <Config />} />
|
||||
<Route exact path="/logs" render={() => <Logs />} />
|
||||
<Route exact path="/proxies" render={() => <Proxies />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</Router>
|
||||
</StoreProvider>
|
||||
</Provider>
|
||||
);
|
||||
// <Route exact path="/__0" component={StyleGuide} />
|
||||
|
|
|
@ -18,6 +18,7 @@ export function fetchConfigs() {
|
|||
res = await configsAPI.fetchConfigs();
|
||||
} catch (err) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error fetch configs', err);
|
||||
dispatch(openModal('apiConfig'));
|
||||
return;
|
||||
|
@ -27,6 +28,7 @@ export function fetchConfigs() {
|
|||
if (res.status === 404 || res.status === 401) {
|
||||
dispatch(openModal('apiConfig'));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error fetch configs', res.statusText);
|
||||
}
|
||||
return;
|
||||
|
@ -57,10 +59,12 @@ export function updateConfigs(partialConfg) {
|
|||
.then(
|
||||
res => {
|
||||
if (res.ok === false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error update configs', res.statusText);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error update configs', err);
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -69,10 +69,12 @@ export function switchProxy(name1, name2) {
|
|||
.then(
|
||||
res => {
|
||||
if (res.ok === false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('failed to swith proxy', res.statusText);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err, 'failed to swith proxy');
|
||||
}
|
||||
)
|
||||
|
|
35
src/misc/shallowEqual.js
Normal file
35
src/misc/shallowEqual.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
function is(x, y) {
|
||||
if (x === y) {
|
||||
return x !== 0 || y !== 0 || 1 / x === 1 / y;
|
||||
} else {
|
||||
return x !== x && y !== y;
|
||||
}
|
||||
}
|
||||
|
||||
export default function shallowEqual(objA, objB) {
|
||||
if (is(objA, objB)) return true;
|
||||
|
||||
if (
|
||||
typeof objA !== 'object' ||
|
||||
objA === null ||
|
||||
typeof objB !== 'object' ||
|
||||
objB === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
47
src/misc/store.js
Normal file
47
src/misc/store.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React, { createContext, useState, useEffect, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import shallowEqual from './shallowEqual';
|
||||
|
||||
export const StoreContext = createContext(null);
|
||||
|
||||
export function Provider({ store, children }) {
|
||||
return (
|
||||
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Provider.propTypes = {
|
||||
store: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export function useStore() {
|
||||
// return the context
|
||||
// which is the redux store
|
||||
return useContext(StoreContext);
|
||||
}
|
||||
|
||||
export function useActions(actions) {
|
||||
const { dispatch } = useStore();
|
||||
const a = typeof actions === 'function' ? actions() : actions;
|
||||
return bindActionCreators(a, dispatch);
|
||||
}
|
||||
|
||||
export function useComponentState(selector) {
|
||||
const store = useStore();
|
||||
const initialMappedState = selector(store.getState());
|
||||
const [compState, setCompState] = useState(initialMappedState);
|
||||
// subscribe to store change
|
||||
useEffect(() => {
|
||||
let compStateCurr = compState;
|
||||
return store.subscribe(() => {
|
||||
const compStateNext = selector(store.getState());
|
||||
if (shallowEqual(compStateCurr, compStateNext)) return;
|
||||
// update state if not equal
|
||||
compStateCurr = compStateNext;
|
||||
setCompState(compStateNext);
|
||||
});
|
||||
}, []);
|
||||
return compState;
|
||||
}
|
|
@ -6692,7 +6692,7 @@ react-router@^4.4.0-beta.4:
|
|||
path-to-regexp "^1.7.0"
|
||||
warning "^4.0.1"
|
||||
|
||||
react@^16.7.0-alpha.0:
|
||||
react@16.7.0-alpha.0:
|
||||
version "16.7.0-alpha.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.7.0-alpha.0.tgz#e2ed4abe6f268c9b092a1d1e572953684d1783a9"
|
||||
integrity sha512-V0za4H01aoAF0SdzahHepvfvzTQ1xxkgMX4z8uKzn+wzZAlVk0IVpleqyxZWluqmdftNedj6fIIZRO/rVYVFvQ==
|
||||
|
|
Loading…
Reference in a new issue