diff options
| author | Haishan <[email protected]> | 2019-12-01 22:41:59 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2019-12-01 22:41:59 +0800 |
| commit | 8b5ecb3f1839808d5e88f635d286fcfdfffd4f86 (patch) | |
| tree | fbbaef42b57a1fe3cb244103ccbb58915e631c66 /src | |
| parent | 19ecf435de90800fe284e3333b3a4957d600f410 (diff) | |
feat: support close all connections
for https://github.com/haishanh/yacd/issues/338
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/connections.js | 38 | ||||
| -rw-r--r-- | src/components/Button.js | 29 | ||||
| -rw-r--r-- | src/components/Button.module.css | 8 | ||||
| -rw-r--r-- | src/components/ConnectionTable.js | 10 | ||||
| -rw-r--r-- | src/components/Connections.js | 28 | ||||
| -rw-r--r-- | src/components/ModalCloseAllConnections.js | 44 | ||||
| -rw-r--r-- | src/components/ModalCloseAllConnections.module.css | 23 | ||||
| -rw-r--r-- | src/components/Proxies.js | 17 | ||||
| -rw-r--r-- | src/components/Proxies.module.css | 7 | ||||
| -rw-r--r-- | src/components/Root.css | 9 | ||||
| -rw-r--r-- | src/components/Rules.js | 19 | ||||
| -rw-r--r-- | src/components/Rules.module.css | 6 | ||||
| -rw-r--r-- | src/components/SvgYacd.js | 4 |
13 files changed, 203 insertions, 39 deletions
diff --git a/src/api/connections.js b/src/api/connections.js index 594fc5e..fbc9abd 100644 --- a/src/api/connections.js +++ b/src/api/connections.js @@ -1,10 +1,39 @@ +import { getURLAndInit } from 'm/request-helper'; + const endpoint = '/connections'; let fetched = false; let subscribers = []; +// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41 +type UUID = string; +type ConnectionItem = { + id: UUID, + metadata: { + network: 'tcp' | 'udp', + type: 'HTTP' | 'HTTP Connect' | 'Socks5' | 'Redir' | 'Unknown', + sourceIP: string, + destinationIP: string, + sourcePort: string, + destinationPort: string, + host: string + }, + upload: number, + download: number, + // e.g. "2019-11-30T22:48:13.416668+08:00", + start: string, + chains: Array<string>, + // e.g. 'Match', 'DomainKeyword' + rule: string +}; +type ConnectionsData = { + downloadTotal: number, + uploadTotal: number, + connections: Array<ConnectionItem> +}; + function appendData(s) { - let o; + let o: ConnectionsData; try { o = JSON.parse(s); } catch (err) { @@ -48,4 +77,9 @@ function subscribe(listener) { }; } -export { fetchData }; +async function closeAllConnections(apiConfig) { + const { url, init } = getURLAndInit(apiConfig); + return await fetch(url + endpoint, { ...init, method: 'DELETE' }); +} + +export { fetchData, closeAllConnections }; diff --git a/src/components/Button.js b/src/components/Button.js index d7928c7..f56049e 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -1,20 +1,29 @@ import React from 'react'; -import PropTypes from 'prop-types'; import s0 from 'c/Button.module.css'; const noop = () => {}; -const Button = React.memo(function Button({ label, onClick = noop }) { +const { memo, forwardRef } = React; + +function Button({ children, label, onClick = noop }, ref) { + return ( + <button className={s0.btn} ref={ref} onClick={onClick}> + {children || label} + </button> + ); +} + +function WithIcon({ text, icon, onClick = noop }, ref) { return ( - <button className={s0.btn} onClick={onClick}> - {label} + <button className={s0.btn} ref={ref} onClick={onClick}> + <div className={s0.withIconWrapper}> + {icon} + <span className={s0.txt}>{text}</span> + </div> </button> ); -}); +} -Button.propTypes = { - label: PropTypes.string.isRequired, - onClick: PropTypes.func -}; +export const ButtonWithIcon = memo(forwardRef(WithIcon)); -export default Button; +export default memo(forwardRef(Button)); diff --git a/src/components/Button.module.css b/src/components/Button.module.css index 8fb6a92..205bfe9 100644 --- a/src/components/Button.module.css +++ b/src/components/Button.module.css @@ -25,3 +25,11 @@ padding: 6px 12px; } } + +.withIconWrapper { + display: flex; + align-items: center; + .txt { + margin-left: 5px; + } +} diff --git a/src/components/ConnectionTable.js b/src/components/ConnectionTable.js index 17293c0..2e7ba7f 100644 --- a/src/components/ConnectionTable.js +++ b/src/components/ConnectionTable.js @@ -34,6 +34,13 @@ function renderCell(cell, now) { } } +const tableState = { + sortBy: [ + // maintain a more stable order + { id: 'start', desc: true } + ] +}; + function Table({ data }) { const now = new Date(); const { @@ -45,7 +52,8 @@ function Table({ data }) { } = useTable( { columns, - data + data, + initialState: tableState }, useSortBy ); diff --git a/src/components/Connections.js b/src/components/Connections.js index 2607606..8953722 100644 --- a/src/components/Connections.js +++ b/src/components/Connections.js @@ -4,12 +4,15 @@ import ConnectionTable from 'c/ConnectionTable'; import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight'; import { useStoreState } from 'm/store'; import { getClashAPIConfig } from 'd/app'; +import { X as IconClose } from 'react-feather'; import SvgYacd from './SvgYacd'; +import { ButtonWithIcon } from './Button'; +import ModalCloseAllConnections from './ModalCloseAllConnections'; import * as connAPI from '../api/connections'; import s from './Connections.module.css'; -const { useEffect, useState, useRef } = React; +const { useEffect, useState, useRef, useCallback, useMemo } = React; const paddingBottom = 30; @@ -31,6 +34,17 @@ function Conn() { const [refContainer, containerHeight] = useRemainingViewPortHeight(); const config = useStoreState(getClashAPIConfig); const [conns, setConns] = useState([]); + const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false); + const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []); + const closeCloseAllModal = useCallback( + () => setIsCloseAllModalOpen(false), + [] + ); + const closeAllConnections = useCallback(() => { + connAPI.closeAllConnections(config); + closeCloseAllModal(); + }, [config, closeCloseAllModal]); + const iconClose = useMemo(() => <IconClose width={16} />, []); const prevConnsRef = useRef(conns); useEffect(() => { function read({ connections }) { @@ -65,6 +79,18 @@ function Conn() { )} </div> </div> + <div className="fabgrp"> + <ButtonWithIcon + text="Close" + icon={iconClose} + onClick={openCloseAllModal} + /> + </div> + <ModalCloseAllConnections + isOpen={isCloseAllModalOpen} + primaryButtonOnTap={closeAllConnections} + onRequestClose={closeCloseAllModal} + /> </div> ); } diff --git a/src/components/ModalCloseAllConnections.js b/src/components/ModalCloseAllConnections.js new file mode 100644 index 0000000..8a06393 --- /dev/null +++ b/src/components/ModalCloseAllConnections.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import Modal from 'react-modal'; +import Button from './Button'; +import cx from 'classnames'; + +import modalStyle from './Modal.module.css'; +import s from './ModalCloseAllConnections.module.css'; + +const { useRef, useCallback, useMemo } = React; + +export default function Comp({ isOpen, onRequestClose, primaryButtonOnTap }) { + const primaryButtonRef = useRef(null); + const onAfterOpen = useCallback(() => { + primaryButtonRef.current.focus(); + }, []); + const className = useMemo( + () => ({ + base: cx(modalStyle.content, s.cnt), + afterOpen: s.afterOpen, + beforeClose: '' + }), + [] + ); + return ( + <Modal + isOpen={isOpen} + onRequestClose={onRequestClose} + onAfterOpen={onAfterOpen} + className={className} + overlayClassName={cx(modalStyle.overlay, s.overlay)} + > + <p>Are you sure you want to close all connections?</p> + <div className={s.btngrp}> + <Button onClick={primaryButtonOnTap} ref={primaryButtonRef}> + I'm sure + </Button> + {/* im lazy :) */} + <div style={{ width: 20 }} /> + <Button onClick={onRequestClose}>No</Button> + </div> + </Modal> + ); +} diff --git a/src/components/ModalCloseAllConnections.module.css b/src/components/ModalCloseAllConnections.module.css new file mode 100644 index 0000000..f3b54c1 --- /dev/null +++ b/src/components/ModalCloseAllConnections.module.css @@ -0,0 +1,23 @@ +.overlay { + background-color: rgba(0, 0, 0, 0.6); +} +.cnt { + background-color: var(--bg-modal); + color: var(--color-text); + max-width: 300px; + line-height: 1.4; + transform: translate(-50%, -50%) scale(1.5); + opacity: 0.6; + transition: all 0.3s ease; +} +.afterOpen { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.btngrp { + display: flex; + align-items: center; + justify-content: center; + margin-top: 30px; +} diff --git a/src/components/Proxies.js b/src/components/Proxies.js index c66ed83..fef6a14 100644 --- a/src/components/Proxies.js +++ b/src/components/Proxies.js @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useActions, useStoreState } from 'm/store'; import ContentHeader from 'c/ContentHeader'; import ProxyGroup from 'c/ProxyGroup'; -import Button from 'c/Button'; +import { ButtonWithIcon } from 'c/Button'; +import { Zap } from 'react-feather'; import s0 from 'c/Proxies.module.css'; @@ -14,6 +15,8 @@ import { requestDelayAll } from 'd/proxies'; +const { useEffect, useMemo } = React; + const mapStateToProps = s => ({ proxies: getProxies(s), groupNames: getProxyGroupNames(s) @@ -33,13 +36,19 @@ export default function Proxies() { })(); }, [fetchProxies, requestDelayAll]); const { groupNames } = useStoreState(mapStateToProps); + const icon = useMemo(() => <Zap width={16} />, []); return ( <> <ContentHeader title="Proxies" /> <div className={s0.body}> - <div className={s0.fabgrp}> - <Button label="Test Latency" onClick={requestDelayAll} /> + <div className="fabgrp"> + <ButtonWithIcon + text="Test Latency" + icon={icon} + onClick={requestDelayAll} + /> + {/* <Button onClick={requestDelayAll}>Test Latency</Button> */} </div> {groupNames.map(groupName => { return ( diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css index a832ebe..72b70fb 100644 --- a/src/components/Proxies.module.css +++ b/src/components/Proxies.module.css @@ -8,10 +8,3 @@ padding: 10px 40px; } } - -.fabgrp { - position: fixed; - z-index: 1; - right: 20px; - bottom: 20px; -} diff --git a/src/components/Root.css b/src/components/Root.css index 88b3d6c..ae25dca 100644 --- a/src/components/Root.css +++ b/src/components/Root.css @@ -92,6 +92,7 @@ body.dark { --color-btn-fg: #bebebe; --color-bg-proxy-selected: #303030; --color-row-odd: #282828; + --bg-modal: #1f1f20; } body.light { @@ -109,4 +110,12 @@ body.light { --color-btn-fg: #101010; --color-bg-proxy-selected: #cfcfcf; --color-row-odd: #f5f5f5; + --bg-modal: #fbfbfb; +} + +/* TODO remove fabgrp in component css files */ +.fabgrp { + position: fixed; + right: 20px; + bottom: 20px; } diff --git a/src/components/Rules.js b/src/components/Rules.js index 96056ee..2e2db3c 100644 --- a/src/components/Rules.js +++ b/src/components/Rules.js @@ -1,7 +1,8 @@ -import React, { memo, useEffect } from 'react'; +import React from 'react'; import { useActions, useStoreState } from 'm/store'; -import Button from 'c/Button'; +import { ButtonWithIcon } from 'c/Button'; import { FixedSizeList as List, areEqual } from 'react-window'; +import { RotateCw } from 'react-feather'; import ContentHeader from 'c/ContentHeader'; import Rule from 'c/Rule'; @@ -10,7 +11,9 @@ import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight'; import { getRules, fetchRules, fetchRulesOnce } from 'd/rules'; -import s0 from './Rules.module.css'; +const { memo, useEffect, useMemo } = React; + +// import s from './Rules.module.css'; const paddingBottom = 30; const mapStateToProps = s => ({ @@ -43,7 +46,7 @@ export default function Rules() { fetchRulesOnce(); }, [fetchRulesOnce]); const [refRulesContainer, containerHeight] = useRemainingViewPortHeight(); - + const refreshIcon = useMemo(() => <RotateCw width={16} />, []); return ( <div> <ContentHeader title="Rules" /> @@ -60,8 +63,12 @@ export default function Rules() { {Row} </List> </div> - <div className={s0.fabgrp}> - <Button label="Refresh" onClick={fetchRules} /> + <div className="fabgrp"> + <ButtonWithIcon + text="Refresh" + icon={refreshIcon} + onClick={fetchRules} + /> </div> </div> ); diff --git a/src/components/Rules.module.css b/src/components/Rules.module.css index 1fb94eb..79a9626 100644 --- a/src/components/Rules.module.css +++ b/src/components/Rules.module.css @@ -1,5 +1 @@ -.fabgrp { - position: fixed; - right: 20px; - bottom: 20px; -} +/* */ diff --git a/src/components/SvgYacd.js b/src/components/SvgYacd.js index 42cd425..b1bc8f0 100644 --- a/src/components/SvgYacd.js +++ b/src/components/SvgYacd.js @@ -12,8 +12,6 @@ function SvgYacd({ c1 = '#eee' }) { const faceClasName = cx({ [s.path]: animate }); - // fill="#2A477A" - return ( <svg width={width} @@ -26,7 +24,7 @@ function SvgYacd({ <path d="M71.689 53.055c9.23-1.487 25.684 27.263 41.411 56.663 18.572-8.017 71.708-7.717 93.775 0 4.714-15.612 31.96-57.405 41.626-56.663 3.992.088 13.07 31.705 23.309 94.96 2.743 16.949 7.537 47.492 14.38 91.63-42.339 17.834-84.37 26.751-126.095 26.751-41.724 0-83.756-8.917-126.095-26.751C52.973 116.244 65.536 54.047 71.689 53.055z" stroke={c1} - strokeWidth="2" + strokeWidth="4" strokeLinecap="round" fill={c0} className={faceClasName} |
