From 15bc0f69a8367a57fa1bf263e615285349ad4ab9 Mon Sep 17 00:00:00 2001 From: Haishan Date: Sun, 13 Sep 2020 16:34:18 +0800 Subject: feat: multi backends management --- src/components/APIConfig.module.css | 2 +- src/components/APIConfig.tsx | 24 ++++-- src/components/APIDiscovery.module.css | 2 +- src/components/BackendList.module.css | 99 ++++++++++++++++++++++ src/components/BackendList.tsx | 147 +++++++++++++++++++++++++++++++++ src/components/Config.js | 8 +- src/components/ErrorBoundary.js | 40 +-------- src/components/Root.js | 58 ++++++++----- src/components/TrafficChart.js | 3 +- 9 files changed, 313 insertions(+), 70 deletions(-) create mode 100644 src/components/BackendList.module.css create mode 100644 src/components/BackendList.tsx (limited to 'src/components') diff --git a/src/components/APIConfig.module.css b/src/components/APIConfig.module.css index 1092aed..afc2d53 100644 --- a/src/components/APIConfig.module.css +++ b/src/components/APIConfig.module.css @@ -20,7 +20,7 @@ } .body { - padding: 30px 0 0; + padding: 15px 0 0; } .hostnamePort { diff --git a/src/components/APIConfig.tsx b/src/components/APIConfig.tsx index 88bec8b..3befa5c 100644 --- a/src/components/APIConfig.tsx +++ b/src/components/APIConfig.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { fetchConfigs } from 'src/api/configs'; +import { BackendList } from 'src/components/BackendList'; import { ClashAPIConfig } from 'src/types'; -import { getClashAPIConfig, updateClashAPIConfig } from '../store/app'; +import { addClashAPIConfig, getClashAPIConfig } from '../store/app'; import s0 from './APIConfig.module.css'; import Button from './Button'; import Field from './Field'; @@ -16,9 +17,9 @@ const mapState = (s) => ({ apiConfig: getClashAPIConfig(s), }); -function APIConfig({ apiConfig, dispatch }) { - const [baseURL, setBaseURL] = useState(apiConfig.baseURL); - const [secret, setSecret] = useState(apiConfig.secret); +function APIConfig({ dispatch }) { + const [baseURL, setBaseURL] = useState(''); + const [secret, setSecret] = useState(''); const [errMsg, setErrMsg] = useState(''); const userTouchedFlagRef = useRef(false); @@ -47,14 +48,21 @@ function APIConfig({ apiConfig, dispatch }) { if (ret[0] !== Ok) { setErrMsg(ret[1]); } else { - dispatch(updateClashAPIConfig({ baseURL, secret })); + dispatch(addClashAPIConfig({ baseURL, secret })); } }); }, [baseURL, secret, dispatch]); const handleContentOnKeyDown = useCallback( - (e) => { + (e: React.KeyboardEvent) => { + if ( + e.target instanceof Element && + (!e.target.tagName || e.target.tagName.toUpperCase() !== 'INPUT') + ) { + return; + } if (e.key !== 'Enter') return; + onConfirm(); }, [onConfirm] @@ -90,8 +98,10 @@ function APIConfig({ apiConfig, dispatch }) {
{errMsg ? errMsg : null}
-
+
+
); } diff --git a/src/components/APIDiscovery.module.css b/src/components/APIDiscovery.module.css index f2aaf71..6c1295a 100644 --- a/src/components/APIDiscovery.module.css +++ b/src/components/APIDiscovery.module.css @@ -12,11 +12,11 @@ display: flex; justify-content: center; + overflow-y: auto; } .container { position: relative; - top: 10%; margin-left: 20px; margin-right: 20px; } diff --git a/src/components/BackendList.module.css b/src/components/BackendList.module.css new file mode 100644 index 0000000..1de1972 --- /dev/null +++ b/src/components/BackendList.module.css @@ -0,0 +1,99 @@ +.ul { + position: relative; + margin: 0; + padding: 0; + list-style: none; + line-height: 1.8; + + --width-max-content: 230px; +} + +.li { + position: relative; + margin: 5px 0; + padding: 10px 0; + border-radius: 10px; + display: grid; + place-content: center; + grid-template-columns: 40px 1fr 40px; + grid-template-rows: 30px; + grid-template-areas: 'close url .'; + column-gap: 10px; +} + +.li:hover { + background-color: var(--bg-near-transparent); +} + +.close { + opacity: 0; + grid-area: close; + place-self: center; +} + +.li:hover .close, +.li:hover .eye { + opacity: 1; +} +.close:focus, +.eye:focus { + opacity: 1; +} + +.hasSecret { + grid-template-rows: repeat(2, 30px); + grid-template-areas: + 'close url .' + 'close secret eye'; +} + +.url { + grid-area: url; +} +.secret { + grid-area: secret; +} +.eye { + grid-area: eye; + opacity: 0; + place-self: center; + cursor: pointer; +} + +.url, +.secret { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.btn { + outline: none; + appearance: none; + border: 1px solid transparent; + background-color: transparent; + color: inherit; + display: flex; + align-items: center; + padding: 5px; + border-radius: 100px; +} +.btn:focus { + border-color: var(--color-focus-blue); +} +.btn:hover:enabled { + background-color: var(--color-focus-blue); +} +.btn:active:enabled { + transform: scale(0.97); +} +.btn:disabled { + color: var(--color-text-secondary); +} + +.url { + cursor: pointer; +} +.url:hover { + color: var(--color-text-highlight); +} diff --git a/src/components/BackendList.tsx b/src/components/BackendList.tsx new file mode 100644 index 0000000..a0c993f --- /dev/null +++ b/src/components/BackendList.tsx @@ -0,0 +1,147 @@ +import cx from 'clsx'; +import * as React from 'react'; +import { Eye, EyeOff, X as Close } from 'react-feather'; +import { useToggle } from 'src/hooks/basic'; +import { + getClashAPIConfigs, + getSelectedClashAPIConfigIndex, +} from 'src/store/app'; +import { ClashAPIConfig } from 'src/types'; + +import s from './BackendList.module.css'; +import { connect, useStoreActions } from './StateProvider'; + +type Config = ClashAPIConfig & { addedAt: number }; + +const mapState = (s) => ({ + apiConfigs: getClashAPIConfigs(s), + selectedClashAPIConfigIndex: getSelectedClashAPIConfigIndex(s), +}); + +export const BackendList = connect(mapState)(BackendListImpl); + +function BackendListImpl({ + apiConfigs, + selectedClashAPIConfigIndex, +}: { + apiConfigs: Config[]; + selectedClashAPIConfigIndex: number; +}) { + const { + app: { removeClashAPIConfig, selectClashAPIConfig }, + } = useStoreActions(); + + const onRemove = React.useCallback( + (conf: ClashAPIConfig) => { + removeClashAPIConfig(conf); + }, + [removeClashAPIConfig] + ); + const onSelect = React.useCallback( + (conf: ClashAPIConfig) => { + selectClashAPIConfig(conf); + }, + [selectClashAPIConfig] + ); + + return ( + <> + + + ); +} + +function Item({ + baseURL, + secret, + disableRemove, + onRemove, + onSelect, +}: { + baseURL: string; + secret: string; + disableRemove: boolean; + onRemove: (x: ClashAPIConfig) => void; + onSelect: (x: ClashAPIConfig) => void; +}) { + const [show, toggle] = useToggle(); + const Icon = show ? EyeOff : Eye; + + const handleTap = React.useCallback((e: React.KeyboardEvent) => { + e.stopPropagation(); + }, []); + + return ( + <> + + onSelect({ baseURL, secret })} + onKeyUp={handleTap} + > + {baseURL} + + + {secret ? ( + <> + {show ? secret : '***'} + + + + ) : null} + + ); +} + +function Button({ + children, + onClick, + className, + disabled, +}: { + children: React.ReactNode; + + onClick?: (e: React.MouseEvent) => unknown; + className: string; + disabled?: boolean; +}) { + return ( + + ); +} diff --git a/src/components/Config.js b/src/components/Config.js index d5850d1..f14f7d9 100644 --- a/src/components/Config.js +++ b/src/components/Config.js @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import { - clearStorage, getClashAPIConfig, getLatencyTestUrl, getSelectedChartStyleIndex, } from '../store/app'; import { fetchConfigs, getConfigs, updateConfigs } from '../store/configs'; +import { openModal } from '../store/modals'; import Button from './Button'; import s0 from './Config.module.css'; import ContentHeader from './ContentHeader'; @@ -104,6 +104,10 @@ function ConfigImpl({ refConfigs.current = configs; }, [configs]); + const openAPIConfigModal = useCallback(() => { + dispatch(openModal('apiConfig')); + }, [dispatch]); + const setConfigState = useCallback( (name, val) => { setConfigStateInternal({ @@ -256,7 +260,7 @@ function ConfigImpl({
Action
-
diff --git a/src/components/ErrorBoundary.js b/src/components/ErrorBoundary.js index cc3898e..ff49e1e 100644 --- a/src/components/ErrorBoundary.js +++ b/src/components/ErrorBoundary.js @@ -1,13 +1,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +// import { getSentry } from '../misc/sentry'; import { deriveMessageFromError } from '../misc/errors'; -import { getSentry } from '../misc/sentry'; import ErrorBoundaryFallback from './ErrorBoundaryFallback'; -// XXX this is no Hook equivalents for componentDidCatch -// we have to use class for now - class ErrorBoundary extends Component { static propTypes = { children: PropTypes.node, @@ -15,44 +12,13 @@ class ErrorBoundary extends Component { state = { error: null }; - loadSentry = async () => { - if (this.sentry) return this.sentry; - const x = await getSentry(); - this.sentry = x; - return this.sentry; - }; - - // static getDerivedStateFromError(error) { - // return { error }; - // } - - componentDidMount() { - // this.loadSentry(); + static getDerivedStateFromError(error) { + return { error }; } - componentDidCatch(error, _info) { - this.setState({ error }); - // eslint-disable-next-line no-console - // console.log(error, errorInfo); - // this.setState({ error }); - // this.loadSentry().then(Sentry => { - // Sentry.withScope(scope => { - // Object.keys(errorInfo).forEach(key => { - // scope.setExtra(key, errorInfo[key]); - // }); - // Sentry.captureException(error); - // }); - // }); - } - - showReportDialog = () => { - this.loadSentry().then((Sentry) => Sentry.showReportDialog()); - }; - render() { if (this.state.error) { const { message, detail } = deriveMessageFromError(this.state.error); - //render fallback UI return ; } else { return this.props.children; diff --git a/src/components/Root.js b/src/components/Root.js index 6c13163..8c06100 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -1,11 +1,12 @@ import './Root.css'; import React, { lazy, Suspense } from 'react'; -import { HashRouter as Router, Route, Routes } from 'react-router-dom'; +import { HashRouter as Router, useRoutes } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; import { About } from 'src/components/about/About'; import { actions, initialState } from '../store'; +import APIConfig from './APIConfig'; import APIDiscovery from './APIDiscovery'; import ErrorBoundary from './ErrorBoundary'; import Home from './Home'; @@ -52,33 +53,50 @@ const Rules = lazy(() => ); const routes = [ - ['home', '/', ], - ['connections', '/connections', ], - ['configs', '/configs', ], - ['logs', '/logs', ], - ['proxies', '/proxies', ], - ['rules', '/rules', ], - ['about', '/about', ], - __DEV__ ? ['style', '/style', ] : false, + { path: '/', element: }, + { path: '/connections', element: }, + { path: '/configs', element: }, + { path: '/logs', element: }, + { path: '/proxies', element: }, + { path: '/rules', element: }, + { path: '/about', element: }, + __DEV__ ? { path: '/style', element: } : false, ].filter(Boolean); +function RouteInnerApp() { + return useRoutes(routes); +} + +function SideBarApp() { + return ( + <> + + +
+ }> + + +
+ + ); +} + +function App() { + return useRoutes([ + { path: '/backend', element: }, + { path: '*', element: }, + ]); +} + const Root = () => (
- - -
- }> - - {routes.map(([key, path, element]) => ( - - ))} - - -
+ }> + +
diff --git a/src/components/TrafficChart.js b/src/components/TrafficChart.js index 18bda77..bcfd4dc 100644 --- a/src/components/TrafficChart.js +++ b/src/components/TrafficChart.js @@ -25,8 +25,7 @@ export default connect(mapState)(TrafficChart); function TrafficChart({ apiConfig, selectedChartStyleIndex }) { const Chart = chartJSResource.read(); - const { hostname, port, secret } = apiConfig; - const traffic = fetchData({ hostname, port, secret }); + const traffic = fetchData(apiConfig); const data = useMemo( () => ({ labels: traffic.labels, -- cgit v1.3.1