summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHaishan <[email protected]>2020-06-07 17:09:23 +0800
committerHaishan <[email protected]>2020-06-07 17:09:23 +0800
commit201f6904c2d822d4d862ba39b8706de726eb7148 (patch)
tree5096c89fe014207f48010764c146f337eac9afe5 /src
parentbbb9f55183bbcf9fadb6a746b8bdc15a423793e8 (diff)
added: modal prompt to close previous connections when switch proxy
Diffstat (limited to 'src')
-rw-r--r--src/api/connections.js11
-rw-r--r--src/components/proxies/ClosePrevConns.tsx50
-rw-r--r--src/components/proxies/Proxies.tsx29
-rw-r--r--src/components/shared/Styled.module.css5
-rw-r--r--src/components/shared/Styled.tsx6
-rw-r--r--src/store/proxies.tsx (renamed from src/store/proxies.js)266
-rw-r--r--src/types.ts0
7 files changed, 303 insertions, 64 deletions
diff --git a/src/api/connections.js b/src/api/connections.js
index 528b2fc..50bd4fe 100644
--- a/src/api/connections.js
+++ b/src/api/connections.js
@@ -82,4 +82,15 @@ async function closeAllConnections(apiConfig) {
return await fetch(url + endpoint, { ...init, method: 'DELETE' });
}
+export async function fetchConns(apiConfig) {
+ const { url, init } = getURLAndInit(apiConfig);
+ return await fetch(url + endpoint, { ...init });
+}
+
+export async function closeConnById(apiConfig, id) {
+ const { url: baseURL, init } = getURLAndInit(apiConfig);
+ const url = `${baseURL}${endpoint}/${id}`;
+ return await fetch(url, { ...init, method: 'DELETE' });
+}
+
export { fetchData, closeAllConnections };
diff --git a/src/components/proxies/ClosePrevConns.tsx b/src/components/proxies/ClosePrevConns.tsx
new file mode 100644
index 0000000..567a3e5
--- /dev/null
+++ b/src/components/proxies/ClosePrevConns.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react';
+
+import Button from '../Button';
+import { FlexCenter } from '../shared/Styled';
+
+const { useRef, useEffect } = React;
+
+type Props = {
+ onClickPrimaryButton?: () => void;
+ onClickSecondaryButton?: () => void;
+};
+
+export function ClosePrevConns({
+ onClickPrimaryButton,
+ onClickSecondaryButton,
+}: Props) {
+ const primaryButtonRef = useRef<HTMLButtonElement>(null);
+ const secondaryButtonRef = useRef<HTMLButtonElement>(null);
+ useEffect(() => {
+ primaryButtonRef.current.focus();
+ }, []);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 39) {
+ secondaryButtonRef.current.focus();
+ } else if (e.keyCode === 37) {
+ primaryButtonRef.current.focus();
+ }
+ };
+
+ return (
+ <div onKeyDown={handleKeyDown}>
+ <h2>Close Connections?</h2>
+ <p>
+ Click "Yes" to close those connections that are still using the old
+ selected proxy in this group
+ </p>
+ <div style={{ height: 30 }} />
+ <FlexCenter>
+ <Button onClick={onClickPrimaryButton} ref={primaryButtonRef}>
+ Yes
+ </Button>
+ <div style={{ width: 20 }} />
+ <Button onClick={onClickSecondaryButton} ref={secondaryButtonRef}>
+ No
+ </Button>
+ </FlexCenter>
+ </div>
+ );
+}
diff --git a/src/components/proxies/Proxies.tsx b/src/components/proxies/Proxies.tsx
index 12e35ad..474d09f 100644
--- a/src/components/proxies/Proxies.tsx
+++ b/src/components/proxies/Proxies.tsx
@@ -1,12 +1,13 @@
import * as React from 'react';
-import { connect } from '../StateProvider';
+import { connect, useStoreActions } from '../StateProvider';
import Button from '../Button';
import ContentHeader from '../ContentHeader';
import { ProxyGroup } from './ProxyGroup';
import BaseModal from '../shared/BaseModal';
import Settings from './Settings';
+import { ClosePrevConns } from './ClosePrevConns';
import Equalizer from '../svg/Equalizer';
import { Zap } from 'react-feather';
@@ -19,6 +20,7 @@ import {
getDelay,
getProxyGroupNames,
getProxyProviders,
+ getShowModalClosePrevConns,
fetchProxies,
requestDelayAll,
} from '../../store/proxies';
@@ -26,7 +28,14 @@ import { getClashAPIConfig } from '../../store/app';
const { useState, useEffect, useCallback, useRef } = React;
-function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
+function Proxies({
+ dispatch,
+ groupNames,
+ delay,
+ proxyProviders,
+ apiConfig,
+ showModalClosePrevConns,
+}) {
const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>(
{}
);
@@ -69,6 +78,10 @@ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
setIsSettingsModalOpen(false);
}, []);
+ const {
+ proxies: { closeModalClosePrevConns, closePrevConnsAndTheModal },
+ } = useStoreActions();
+
return (
<>
<div className={s0.topBar}>
@@ -84,7 +97,7 @@ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
</BaseModal>
<ContentHeader title="Proxies" />
<div>
- {groupNames.map((groupName) => {
+ {groupNames.map((groupName: string) => {
return (
<div className={s0.group} key={groupName}>
<ProxyGroup
@@ -105,6 +118,15 @@ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
text="Test Latency"
position={fabPosition}
/>
+ <BaseModal
+ isOpen={showModalClosePrevConns}
+ onRequestClose={closeModalClosePrevConns}
+ >
+ <ClosePrevConns
+ onClickPrimaryButton={() => closePrevConnsAndTheModal(apiConfig)}
+ onClickSecondaryButton={closeModalClosePrevConns}
+ />
+ </BaseModal>
</>
);
}
@@ -131,6 +153,7 @@ const mapState = (s) => ({
groupNames: getProxyGroupNames(s),
proxyProviders: getProxyProviders(s),
delay: getDelay(s),
+ showModalClosePrevConns: getShowModalClosePrevConns(s),
});
export default connect(mapState)(Proxies);
diff --git a/src/components/shared/Styled.module.css b/src/components/shared/Styled.module.css
new file mode 100644
index 0000000..88b84ad
--- /dev/null
+++ b/src/components/shared/Styled.module.css
@@ -0,0 +1,5 @@
+.FlexCenter {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/components/shared/Styled.tsx b/src/components/shared/Styled.tsx
new file mode 100644
index 0000000..2dbb8b2
--- /dev/null
+++ b/src/components/shared/Styled.tsx
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import s from './Styled.module.css';
+
+export function FlexCenter({ children }: { children: React.ReactNode }) {
+ return <div className={s.FlexCenter}>{children}</div>;
+}
diff --git a/src/store/proxies.js b/src/store/proxies.tsx
index dc4c122..ac61ddf 100644
--- a/src/store/proxies.js
+++ b/src/store/proxies.tsx
@@ -1,20 +1,59 @@
import * as proxiesAPI from '../api/proxies';
+import * as connAPI from '../api/connections';
import { getLatencyTestUrl } from './app';
-// eslint-disable-next-line no-unused-vars
+type PrimitiveProxyType = 'Shadowsocks' | 'Snell' | 'Socks5' | 'Http' | 'Vmess';
+
+type LatencyHistory = Array<{ time: string; delay: number }>;
+
+type ProxyItem = {
+ name: string;
+ type: PrimitiveProxyType;
+ history: LatencyHistory;
+ all?: string[];
+ now?: string;
+};
+
type ProxyProvider = {
- name: string,
- type: 'Proxy',
- updatedAt: string,
- vehicleType: 'HTTP' | 'File' | 'Compatible',
- proxies: Array<{
- history: Array<{ time: string, delay: number }>,
- name: string,
- // Shadowsocks, Http ...
- type: string,
- }>,
+ name: string;
+ type: 'Proxy';
+ updatedAt: string;
+ vehicleType: 'HTTP' | 'File' | 'Compatible';
+ proxies: Array<ProxyItem>;
+};
+
+type FormattedProxyProvider = Omit<ProxyProvider, 'proxies'> & {
+ proxies: string[];
+};
+
+type ProxiesDict = { [name: string]: ProxyItem };
+
+type ProxiesState = {
+ proxies: ProxiesDict;
+ delay: { [key: string]: { number: number } };
+ groupNames: string[];
+ proxyProviders?: FormattedProxyProvider[];
+ dangleProxyNames?: string[];
+
+ showModalClosePrevConns: boolean;
+ switchProxyCtx?: {
+ to: { groupName: string; itemName: string };
+ };
+};
+
+type GlobalState = {
+ proxies: ProxiesState;
+};
+
+export const initialState: ProxiesState = {
+ proxies: {},
+ delay: {},
+ groupNames: [],
+ showModalClosePrevConns: false,
};
+const noop = () => null;
+
// see all types:
// https://github.com/Dreamacro/clash/blob/master/constant/adapters.go
@@ -32,22 +71,33 @@ const NonProxyTypes = [
'Unknown',
];
-export const getProxies = (s) => s.proxies.proxies;
-export const getDelay = (s) => s.proxies.delay;
-export const getProxyGroupNames = (s) => s.proxies.groupNames;
-export const getProxyProviders = (s) => s.proxies.proxyProviders || [];
-export const getDangleProxyNames = (s) => s.proxies.dangleProxyNames;
+export const getProxies = (s: GlobalState) => s.proxies.proxies;
+export const getDelay = (s: GlobalState) => s.proxies.delay;
+export const getProxyGroupNames = (s: GlobalState) => s.proxies.groupNames;
+export const getProxyProviders = (s: GlobalState) =>
+ s.proxies.proxyProviders || [];
+export const getDangleProxyNames = (s: GlobalState) =>
+ s.proxies.dangleProxyNames;
+export const getShowModalClosePrevConns = (s: GlobalState) =>
+ s.proxies.showModalClosePrevConns;
-export function fetchProxies(apiConfig) {
- return async (dispatch, getState) => {
+type APIConfig = {
+ hostname: string;
+ port: string;
+ secret?: string;
+};
+
+export function fetchProxies(apiConfig: APIConfig) {
+ return async (dispatch: any, getState: any) => {
const [proxiesData, providersData] = await Promise.all([
proxiesAPI.fetchProxies(apiConfig),
proxiesAPI.fetchProviderProxies(apiConfig),
]);
- const [proxyProviders, providerProxies] = formatProxyProviders(
- providersData.providers
- );
+ const {
+ providers: proxyProviders,
+ proxies: providerProxies,
+ } = formatProxyProviders(providersData.providers);
const proxies = { ...providerProxies, ...proxiesData.proxies };
const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies);
@@ -58,14 +108,8 @@ export function fetchProxies(apiConfig) {
const name = proxyNames[i];
const { history } = proxies[name] || { history: [] };
const h = history[history.length - 1];
- if (h) {
- const ret = { error: '' };
- if (h.delay === 0) {
- ret.error = 'LikelyTimeout';
- } else {
- ret.number = h.delay;
- }
- delayNext[name] = ret;
+ if (h && h.delay) {
+ delayNext[name] = { number: h.delay };
}
}
@@ -75,7 +119,7 @@ export function fetchProxies(apiConfig) {
if (!providerProxies[v]) dangleProxyNames.push(v);
}
- dispatch('store/proxies#fetchProxies', (s) => {
+ dispatch('store/proxies#fetchProxies', (s: GlobalState) => {
s.proxies.proxies = proxies;
s.proxies.groupNames = groupNames;
s.proxies.delay = delayNext;
@@ -85,7 +129,7 @@ export function fetchProxies(apiConfig) {
};
}
-export function updateProviderByName(apiConfig, name) {
+export function updateProviderByName(apiConfig: APIConfig, name: string) {
return async (dispatch) => {
try {
await proxiesAPI.updateProviderByName(apiConfig, name);
@@ -115,30 +159,119 @@ export function healthcheckProviderByName(apiConfig, name) {
};
}
-export function switchProxy(apiConfig, name1, name2) {
+async function closeGroupConns(
+ apiConfig: APIConfig,
+ groupName: string,
+ exceptionItemName: string
+) {
+ const res = await connAPI.fetchConns(apiConfig);
+ if (!res.ok) {
+ console.log('unable to fetch all connections', res.statusText);
+ /* throw new Error(); */
+ }
+ const json = await res.json();
+ const connections = json.connections;
+ const idsToClose = [];
+ for (const conn of connections) {
+ if (
+ // include the groupName
+ conn.chains.indexOf(groupName) > -1 &&
+ // but not include the itemName
+ conn.chains.indexOf(exceptionItemName) < 0
+ ) {
+ idsToClose.push(conn.id);
+ }
+ }
+
+ await Promise.all(
+ idsToClose.map((id) => connAPI.closeConnById(apiConfig, id).catch(noop))
+ );
+}
+
+function resolveChain(
+ proxies: ProxiesDict,
+ groupName: string,
+ itemName: string
+) {
+ const chain = [itemName, groupName];
+
+ let child: ProxyItem;
+ let childKey = itemName;
+ while ((child = proxies[childKey]) && child.now) {
+ chain.unshift(child.now);
+ childKey = child.now;
+ }
+ return chain;
+}
+
+async function switchProxyImpl(
+ dispatch: any,
+ apiConfig: APIConfig,
+ groupName: string,
+ itemName: string
+) {
+ try {
+ const res = await proxiesAPI.requestToSwitchProxy(
+ apiConfig,
+ groupName,
+ itemName
+ );
+ if (res.ok === false) {
+ throw new Error(`failed to switch proxy: res.statusText`);
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(err, 'failed to swith proxy');
+ throw err;
+ }
+
+ dispatch(fetchProxies(apiConfig));
+ dispatch('showModalClosePrevConns', (s: GlobalState) => {
+ s.proxies.showModalClosePrevConns = true;
+ s.proxies.switchProxyCtx = { to: { groupName, itemName } };
+ });
+}
+
+function closeModalClosePrevConns() {
+ return (dispatch) => {
+ dispatch('closeModalClosePrevConns', (s: GlobalState) => {
+ s.proxies.showModalClosePrevConns = false;
+ });
+ };
+}
+
+function closePrevConnsAndTheModal(apiConfig: APIConfig) {
+ return async (dispatch, getState) => {
+ const s = getState();
+ const switchTo = s.proxies.switchProxyCtx?.to;
+ if (!switchTo) {
+ dispatch(closeModalClosePrevConns());
+ return;
+ }
+
+ // we must have fetched the proxies before
+ // so the proxies here is fresh
+ const proxies = s.proxies.proxies;
+ const chain = resolveChain(proxies, switchTo.groupName, switchTo.itemName);
+ closeGroupConns(apiConfig, switchTo.groupName, chain[0]);
+
+ dispatch('closePrevConnsAndTheModal', (s: GlobalState) => {
+ s.proxies.showModalClosePrevConns = false;
+ s.proxies.switchProxyCtx = undefined;
+ });
+ };
+}
+
+export function switchProxy(apiConfig, groupName, itemName) {
return async (dispatch) => {
- proxiesAPI
- .requestToSwitchProxy(apiConfig, 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');
- }
- )
- .then(() => {
- dispatch(fetchProxies(apiConfig));
- });
+ // switch proxy asynchronously
+ switchProxyImpl(dispatch, apiConfig, groupName, itemName).catch(noop);
+
// optimistic UI update
dispatch('store/proxies#switchProxy', (s) => {
const proxies = s.proxies.proxies;
- if (proxies[name1] && proxies[name1].now) {
- proxies[name1].now = name2;
+ if (proxies[groupName] && proxies[groupName].now) {
+ proxies[groupName].now = itemName;
}
});
};
@@ -234,14 +367,24 @@ function retrieveGroupNamesFrom(proxies) {
return [groupNames, proxyNames];
}
-function formatProxyProviders(providersInput) {
+type ProvidersRaw = {
+ [key: string]: ProxyProvider;
+};
+
+function formatProxyProviders(
+ providersInput: ProvidersRaw
+): {
+ providers: Array<FormattedProxyProvider>;
+ proxies: { [key: string]: ProxyItem };
+} {
const keys = Object.keys(providersInput);
const providers = [];
const proxies = {};
for (let i = 0; i < keys.length; i++) {
- const provider = providersInput[keys[i]];
- if (provider.name === 'default' || provider.vehicleType === 'Compatible')
+ const provider: ProxyProvider = providersInput[keys[i]];
+ if (provider.name === 'default' || provider.vehicleType === 'Compatible') {
continue;
+ }
const proxiesArr = provider.proxies;
const names = [];
for (let j = 0; j < proxiesArr.length; j++) {
@@ -255,13 +398,14 @@ function formatProxyProviders(providersInput) {
providers.push(provider);
}
- return [providers, proxies];
+ return {
+ providers,
+ proxies,
+ };
}
-export const initialState = {
- proxies: {},
- delay: {},
- groupNames: [],
+export const actions = {
+ requestDelayForProxies,
+ closeModalClosePrevConns,
+ closePrevConnsAndTheModal,
};
-
-export const actions = { requestDelayForProxies };
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/types.ts