diff options
| author | Haishan <[email protected]> | 2020-06-07 17:09:23 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2020-06-07 17:09:23 +0800 |
| commit | 201f6904c2d822d4d862ba39b8706de726eb7148 (patch) | |
| tree | 5096c89fe014207f48010764c146f337eac9afe5 /src | |
| parent | bbb9f55183bbcf9fadb6a746b8bdc15a423793e8 (diff) | |
added: modal prompt to close previous connections when switch proxy
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/connections.js | 11 | ||||
| -rw-r--r-- | src/components/proxies/ClosePrevConns.tsx | 50 | ||||
| -rw-r--r-- | src/components/proxies/Proxies.tsx | 29 | ||||
| -rw-r--r-- | src/components/shared/Styled.module.css | 5 | ||||
| -rw-r--r-- | src/components/shared/Styled.tsx | 6 | ||||
| -rw-r--r-- | src/store/proxies.tsx (renamed from src/store/proxies.js) | 266 | ||||
| -rw-r--r-- | src/types.ts | 0 |
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 |
