added: test latency button for each proxy group

This commit is contained in:
Haishan 2020-06-03 22:47:26 +08:00
parent 07082c5b09
commit bf17be6a65
18 changed files with 216 additions and 177 deletions

View file

@ -8,7 +8,7 @@ import s0 from './Button.module.css';
const { memo, forwardRef, useCallback } = React; const { memo, forwardRef, useCallback } = React;
type ButtonInternalProps = { type ButtonInternalProps = {
children?: React.ReactChildren; children?: React.ReactNode;
label?: string; label?: string;
text?: string; text?: string;
start?: React.ReactElement | (() => React.ReactElement); start?: React.ReactElement | (() => React.ReactElement);

View file

@ -19,7 +19,7 @@ const Proxies = React.lazy(() =>
/* webpackChunkName: "proxies" */ /* webpackChunkName: "proxies" */
/* webpackPrefetch: true */ /* webpackPrefetch: true */
/* webpackPreload: true */ /* webpackPreload: true */
'./Proxies' './proxies/Proxies'
) )
); );
const Rules = React.lazy(() => const Rules = React.lazy(() =>
@ -38,7 +38,7 @@ const routes = [
['logs', '/logs', <Logs />], ['logs', '/logs', <Logs />],
['proxies', '/proxies', <Proxies />], ['proxies', '/proxies', <Proxies />],
['rules', '/rules', <Rules />], ['rules', '/rules', <Rules />],
__DEV__ ? ['style', '/style', <StyleGuide />] : false __DEV__ ? ['style', '/style', <StyleGuide />] : false,
].filter(Boolean); ].filter(Boolean);
const Root = () => ( const Root = () => (

View file

@ -1,17 +1,17 @@
import React from 'react'; import * as React from 'react';
import { connect } from './StateProvider'; import { connect } from '../StateProvider';
import Button from './Button'; import Button from '../Button';
import ContentHeader from './ContentHeader'; import ContentHeader from '../ContentHeader';
import ProxyGroup from './ProxyGroup'; import { ProxyGroup } from './ProxyGroup';
import BaseModal from './shared/BaseModal'; import BaseModal from '../shared/BaseModal';
import Settings from './proxies/Settings'; import Settings from './Settings';
import Equalizer from './svg/Equalizer'; import Equalizer from '../svg/Equalizer';
import { Zap } from 'react-feather'; import { Zap } from 'react-feather';
import ProxyProviderList from './ProxyProviderList'; import { ProxyProviderList } from './ProxyProviderList';
import { Fab, position as fabPosition } from './shared/Fab'; import { Fab, position as fabPosition } from '../shared/Fab';
import s0 from './Proxies.module.css'; import s0 from './Proxies.module.css';
@ -21,13 +21,15 @@ import {
getProxyProviders, getProxyProviders,
fetchProxies, fetchProxies,
requestDelayAll, requestDelayAll,
} from '../store/proxies'; } from '../../store/proxies';
import { getClashAPIConfig } from '../store/app'; import { getClashAPIConfig } from '../../store/app';
const { useState, useEffect, useCallback, useRef } = React; const { useState, useEffect, useCallback, useRef } = React;
function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) { function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
const refFetchedTimestamp = useRef({}); const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>(
{}
);
const [isTestingLatency, setIsTestingLatency] = useState(false); const [isTestingLatency, setIsTestingLatency] = useState(false);
const requestDelayAllFn = useCallback(() => { const requestDelayAllFn = useCallback(() => {
if (isTestingLatency) return; if (isTestingLatency) return;
@ -40,9 +42,9 @@ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
}, [apiConfig, dispatch, isTestingLatency]); }, [apiConfig, dispatch, isTestingLatency]);
const fetchProxiesHooked = useCallback(() => { const fetchProxiesHooked = useCallback(() => {
refFetchedTimestamp.current.startAt = new Date(); refFetchedTimestamp.current.startAt = Date.now();
dispatch(fetchProxies(apiConfig)).then(() => { dispatch(fetchProxies(apiConfig)).then(() => {
refFetchedTimestamp.current.completeAt = new Date(); refFetchedTimestamp.current.completeAt = Date.now();
}); });
}, [apiConfig, dispatch]); }, [apiConfig, dispatch]);
useEffect(() => { useEffect(() => {
@ -53,7 +55,7 @@ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
const fn = () => { const fn = () => {
if ( if (
refFetchedTimestamp.current.startAt && refFetchedTimestamp.current.startAt &&
new Date() - refFetchedTimestamp.current.startAt > 3e4 // 30s Date.now() - refFetchedTimestamp.current.startAt > 3e4 // 30s
) { ) {
fetchProxiesHooked(); fetchProxiesHooked();
} }

View file

@ -1,10 +1,10 @@
import React from 'react'; import * as React from 'react';
import cx from 'clsx'; import cx from 'clsx';
import { connect } from './StateProvider'; import { connect } from '../StateProvider';
import ProxyLatency from './ProxyLatency'; import { ProxyLatency } from './ProxyLatency';
import { getProxies, getDelay } from '../../store/proxies';
import { getProxies, getDelay } from '../store/proxies';
import s0 from './Proxy.module.css'; import s0 from './Proxy.module.css';
@ -21,7 +21,11 @@ const colorMap = {
na: '#909399', na: '#909399',
}; };
function getLabelColor({ number } = {}) { function getLabelColor({
number,
}: {
number?: number;
} = {}) {
if (number < 200) { if (number < 200) {
return colorMap.good; return colorMap.good;
} else if (number < 400) { } else if (number < 400) {
@ -32,27 +36,11 @@ function getLabelColor({ number } = {}) {
return colorMap.na; return colorMap.na;
} }
/*
const colors = {
Direct: '#408b43',
Fallback: '#3483e8',
Selector: '#387cec',
Vmess: '#ca3487',
Shadowsocks: '#1a7dc0',
Socks5: '#2a477a',
URLTest: '#3483e8',
Http: '#d3782d'
};
*/
type ProxyProps = { type ProxyProps = {
name: string, name: string;
now?: boolean, now?: boolean;
proxy: any;
// connect injected latency: any;
// TODO refine type
proxy: any,
latency: any,
}; };
function ProxySmallImpl({ now, name, latency }: ProxyProps) { function ProxySmallImpl({ now, name, latency }: ProxyProps) {
@ -73,7 +61,7 @@ function ProxySmallImpl({ now, name, latency }: ProxyProps) {
); );
} }
function Proxy({ now, name, proxy, latency }: ProxyProps) { function ProxyImpl({ now, name, proxy, latency }: ProxyProps) {
const color = useMemo(() => getLabelColor(latency), [latency]); const color = useMemo(() => getLabelColor(latency), [latency]);
return ( return (
<div <div
@ -95,7 +83,7 @@ function Proxy({ now, name, proxy, latency }: ProxyProps) {
); );
} }
const mapState = (s, { name }) => { const mapState = (s: any, { name }) => {
const proxies = getProxies(s); const proxies = getProxies(s);
const delay = getDelay(s); const delay = getDelay(s);
return { return {
@ -104,5 +92,5 @@ const mapState = (s, { name }) => {
}; };
}; };
export default connect(mapState)(Proxy); export const Proxy = connect(mapState)(ProxyImpl);
export const ProxySmall = connect(mapState)(ProxySmallImpl); export const ProxySmall = connect(mapState)(ProxySmallImpl);

View file

@ -0,0 +1,11 @@
.header {
margin-bottom: 12px;
}
.zapWrapper {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -1,28 +1,37 @@
import React from 'react'; import * as React from 'react';
import cx from 'clsx';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { Zap } from 'react-feather';
import { connect, useStoreActions } from './StateProvider'; import { connect, useStoreActions } from '../StateProvider';
import { getProxies } from '../store/proxies'; import { getProxies } from '../../store/proxies';
import { import {
getCollapsibleIsOpen, getCollapsibleIsOpen,
getProxySortBy, getProxySortBy,
getHideUnavailableProxies, getHideUnavailableProxies,
} from '../store/app'; } from '../../store/app';
import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import { switchProxy } from '../../store/proxies';
import Proxy, { ProxySmall } from './Proxy'; import CollapsibleSectionHeader from '../CollapsibleSectionHeader';
import Button from '../Button';
import { ProxyList, ProxyListSummaryView } from './ProxyList';
import s0 from './ProxyGroup.module.css'; import s0 from './ProxyGroup.module.css';
import { switchProxy } from '../store/proxies'; const { useCallback, useMemo, useState } = React;
const { useCallback, useMemo } = React; function ZapWrapper() {
return (
<div className={s0.zapWrapper}>
<Zap size={16} />
</div>
);
}
function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) { function ProxyGroupImpl({ name, all, type, now, isOpen, apiConfig, dispatch }) {
const isSelectable = useMemo(() => type === 'Selector', [type]); const isSelectable = useMemo(() => type === 'Selector', [type]);
const { const {
app: { updateCollapsibleIsOpen }, app: { updateCollapsibleIsOpen },
proxies: { requestDelayForProxies },
} = useStoreActions(); } = useStoreActions();
const toggle = useCallback(() => { const toggle = useCallback(() => {
@ -37,8 +46,18 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
[apiConfig, dispatch, name, isSelectable] [apiConfig, dispatch, name, isSelectable]
); );
const [isTestingLatency, setIsTestingLatency] = useState(false);
const testLatency = useCallback(async () => {
setIsTestingLatency(true);
try {
await requestDelayForProxies(apiConfig, all);
} catch (err) {}
setIsTestingLatency(false);
}, [all, apiConfig, requestDelayForProxies]);
return ( return (
<div className={s0.group}> <div className={s0.group}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<CollapsibleSectionHeader <CollapsibleSectionHeader
name={name} name={name}
type={type} type={type}
@ -46,6 +65,14 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
qty={all.length} qty={all.length}
isOpen={isOpen} isOpen={isOpen}
/> />
<Button
kind="minimal"
onClick={testLatency}
isLoading={isTestingLatency}
>
<ZapWrapper />
</Button>
</div>
{isOpen ? ( {isOpen ? (
<ProxyList <ProxyList
all={all} all={all}
@ -60,45 +87,6 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
); );
} }
type ProxyListProps = {
all: string[],
now?: string,
isSelectable?: boolean,
itemOnTapCallback?: (string) => void,
show?: boolean,
};
export function ProxyList({
all,
now,
isSelectable,
itemOnTapCallback,
sortedAll,
}: ProxyListProps) {
const proxies = sortedAll || all;
return (
<div className={s0.list}>
{proxies.map((proxyName) => {
const proxyClassName = cx(s0.proxy, {
[s0.proxySelectable]: isSelectable,
});
return (
<div
className={proxyClassName}
key={proxyName}
onClick={() => {
if (!isSelectable || !itemOnTapCallback) return;
itemOnTapCallback(proxyName);
}}
>
<Proxy name={proxyName} now={proxyName === now} />
</div>
);
})}
</div>
);
}
const getSortDelay = (d, w) => { const getSortDelay = (d, w) => {
if (d === undefined) { if (d === undefined) {
return 0; return 0;
@ -172,36 +160,7 @@ export const filterAvailableProxiesAndSort = memoizeOne(
filterAvailableProxiesAndSortImpl filterAvailableProxiesAndSortImpl
); );
export function ProxyListSummaryView({ export const ProxyGroup = connect((s, { name, delay }) => {
all,
now,
isSelectable,
itemOnTapCallback,
}: ProxyListProps) {
return (
<div className={s0.list}>
{all.map((proxyName) => {
const proxyClassName = cx(s0.proxy, {
[s0.proxySelectable]: isSelectable,
});
return (
<div
className={proxyClassName}
key={proxyName}
onClick={() => {
if (!isSelectable || !itemOnTapCallback) return;
itemOnTapCallback(proxyName);
}}
>
<ProxySmall name={proxyName} now={proxyName === now} />
</div>
);
})}
</div>
);
}
export default connect((s, { name, delay }) => {
const proxies = getProxies(s); const proxies = getProxies(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s); const collapsibleIsOpen = getCollapsibleIsOpen(s);
const proxySortBy = getProxySortBy(s); const proxySortBy = getProxySortBy(s);
@ -220,4 +179,4 @@ export default connect((s, { name, delay }) => {
now, now,
isOpen: collapsibleIsOpen[`proxyGroup:${name}`], isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
}; };
})(ProxyGroup); })(ProxyGroupImpl);

View file

@ -1,13 +1,13 @@
import React from 'react'; import * as React from 'react';
import s0 from './ProxyLatency.module.css'; import s0 from './ProxyLatency.module.css';
type ProxyLatencyProps = { type ProxyLatencyProps = {
number: number, number: number;
color: string color: string;
}; };
export default function ProxyLatency({ number, color }: ProxyLatencyProps) { export function ProxyLatency({ number, color }: ProxyLatencyProps) {
return ( return (
<span className={s0.proxyLatency} style={{ color }}> <span className={s0.proxyLatency} style={{ color }}>
<span>{number} ms</span> <span>{number} ms</span>

View file

@ -1,7 +1,3 @@
.header {
margin-bottom: 12px;
}
.list { .list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -0,0 +1,75 @@
import * as React from 'react';
import cx from 'clsx';
import { Proxy, ProxySmall } from './Proxy';
import s from './ProxyList.module.css';
type ProxyListProps = {
all: string[];
now?: string;
isSelectable?: boolean;
itemOnTapCallback?: (x: string) => void;
show?: boolean;
};
export function ProxyList({
all,
now,
isSelectable,
itemOnTapCallback,
}: ProxyListProps) {
const proxies = all;
return (
<div className={s.list}>
{proxies.map((proxyName) => {
const proxyClassName = cx(s.proxy, {
[s.proxySelectable]: isSelectable,
});
return (
<div
className={proxyClassName}
key={proxyName}
onClick={() => {
if (!isSelectable || !itemOnTapCallback) return;
itemOnTapCallback(proxyName);
}}
>
<Proxy name={proxyName} now={proxyName === now} />
</div>
);
})}
</div>
);
}
export function ProxyListSummaryView({
all,
now,
isSelectable,
itemOnTapCallback,
}: ProxyListProps) {
return (
<div className={s.list}>
{all.map((proxyName) => {
const proxyClassName = cx(s.proxy, {
[s.proxySelectable]: isSelectable,
});
return (
<div
className={proxyClassName}
key={proxyName}
onClick={() => {
if (!isSelectable || !itemOnTapCallback) return;
itemOnTapCallback(proxyName);
}}
>
<ProxySmall name={proxyName} now={proxyName === now} />
</div>
);
})}
</div>
);
}

View file

@ -1,45 +1,43 @@
import React from 'react'; import * as React from 'react';
import { RotateCw, Zap } from 'react-feather'; import { RotateCw, Zap } from 'react-feather';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { connect, useStoreActions } from './StateProvider'; import { connect, useStoreActions } from '../StateProvider';
import Collapsible from './Collapsible'; import Collapsible from '../Collapsible';
import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import CollapsibleSectionHeader from '../CollapsibleSectionHeader';
import { import { filterAvailableProxiesAndSort } from './ProxyGroup';
ProxyList, import { ProxyList, ProxyListSummaryView } from './ProxyList';
ProxyListSummaryView, import Button from '../Button';
filterAvailableProxiesAndSort,
} from './ProxyGroup';
import Button from './Button';
import { import {
getClashAPIConfig, getClashAPIConfig,
getCollapsibleIsOpen, getCollapsibleIsOpen,
getProxySortBy, getProxySortBy,
getHideUnavailableProxies, getHideUnavailableProxies,
} from '../store/app'; } from '../../store/app';
import { import {
getDelay, getDelay,
updateProviderByName, updateProviderByName,
healthcheckProviderByName, healthcheckProviderByName,
} from '../store/proxies'; } from '../../store/proxies';
import s from './ProxyProvider.module.css'; import s from './ProxyProvider.module.css';
const { useState, useCallback } = React; const { useState, useCallback } = React;
type Props = { type Props = {
name: string, name: string;
proxies: Array<string>, proxies: Array<string>;
type: 'Proxy' | 'Rule', type: 'Proxy' | 'Rule';
vehicleType: 'HTTP' | 'File' | 'Compatible', vehicleType: 'HTTP' | 'File' | 'Compatible';
updatedAt?: string, updatedAt?: string;
dispatch: (any) => void, dispatch: (x: any) => Promise<any>;
isOpen: boolean, isOpen: boolean;
apiConfig: any;
}; };
function ProxyProvider({ function ProxyProviderImpl({
name, name,
proxies, proxies,
vehicleType, vehicleType,
@ -63,9 +61,6 @@ function ProxyProvider({
app: { updateCollapsibleIsOpen }, app: { updateCollapsibleIsOpen },
} = useStoreActions(); } = useStoreActions();
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
// const toggle = useCallback(() => setCollapsibleOpen(x => !x), []);
const toggle = useCallback(() => { const toggle = useCallback(() => {
updateCollapsibleIsOpen('proxyProvider', name, !isOpen); updateCollapsibleIsOpen('proxyProvider', name, !isOpen);
}, [isOpen, updateCollapsibleIsOpen, name]); }, [isOpen, updateCollapsibleIsOpen, name]);
@ -104,7 +99,6 @@ function ProxyProvider({
const button = { const button = {
rest: { scale: 1 }, rest: { scale: 1 },
// hover: { scale: 1.1 },
pressed: { scale: 0.95 }, pressed: { scale: 0.95 },
}; };
const arrow = { const arrow = {
@ -147,4 +141,4 @@ const mapState = (s, { proxies, name }) => {
}; };
}; };
export default connect(mapState)(ProxyProvider); export const ProxyProvider = connect(mapState)(ProxyProviderImpl);

View file

@ -1,16 +1,16 @@
import React from 'react'; import * as React from 'react';
import ContentHeader from './ContentHeader'; import ContentHeader from '../ContentHeader';
import ProxyProvider from './ProxyProvider'; import { ProxyProvider } from './ProxyProvider';
function ProxyProviderList({ items }) { export function ProxyProviderList({ items }) {
if (items.length === 0) return null; if (items.length === 0) return null;
return ( return (
<> <>
<ContentHeader title="Proxy Provider" /> <ContentHeader title="Proxy Provider" />
<div> <div>
{items.map(item => ( {items.map((item) => (
<ProxyProvider <ProxyProvider
key={item.name} key={item.name}
name={item.name} name={item.name}
@ -24,5 +24,3 @@ function ProxyProviderList({ items }) {
</> </>
); );
} }
export default ProxyProviderList;

View file

@ -0,0 +1 @@
export { ProxyList } from './ProxyList';

View file

@ -4,7 +4,7 @@ import {
updateAppConfig, updateAppConfig,
updateCollapsibleIsOpen, updateCollapsibleIsOpen,
} from './app'; } from './app';
import { initialState as proxies } from './proxies'; import { initialState as proxies, actions as proxiesActions } from './proxies';
import { initialState as modals } from './modals'; import { initialState as modals } from './modals';
import { initialState as configs } from './configs'; import { initialState as configs } from './configs';
import { initialState as rules } from './rules'; import { initialState as rules } from './rules';
@ -27,4 +27,5 @@ export const actions = {
updateCollapsibleIsOpen, updateCollapsibleIsOpen,
updateAppConfig, updateAppConfig,
}, },
proxies: proxiesActions,
}; };

View file

@ -20,7 +20,6 @@ type ProxyProvider = {
// const ProxyTypeBuiltin = ['DIRECT', 'GLOBAL', 'REJECT']; // const ProxyTypeBuiltin = ['DIRECT', 'GLOBAL', 'REJECT'];
// const ProxyGroupTypes = ['Fallback', 'URLTest', 'Selector', 'LoadBalance']; // const ProxyGroupTypes = ['Fallback', 'URLTest', 'Selector', 'LoadBalance'];
// const ProxyTypes = ['Shadowsocks', 'Snell', 'Socks5', 'Http', 'Vmess']; // const ProxyTypes = ['Shadowsocks', 'Snell', 'Socks5', 'Http', 'Vmess'];
const NonProxyTypes = [ const NonProxyTypes = [
@ -180,6 +179,19 @@ export function requestDelayForProxy(apiConfig, name) {
}; };
} }
export function requestDelayForProxies(apiConfig, names) {
return async (dispatch, getState) => {
const proxyNames = getDangleProxyNames(getState());
const works = names
// remove names that are provided by proxy providers
.filter((p) => proxyNames.indexOf(p) > -1)
.map((p) => dispatch(requestDelayForProxy(apiConfig, p)));
await Promise.all(works);
await dispatch(fetchProxies(apiConfig));
};
}
export function requestDelayAll(apiConfig) { export function requestDelayAll(apiConfig) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const proxyNames = getDangleProxyNames(getState()); const proxyNames = getDangleProxyNames(getState());
@ -251,3 +263,5 @@ export const initialState = {
delay: {}, delay: {},
groupNames: [], groupNames: [],
}; };
export const actions = { requestDelayForProxies };