feat: support close all connections

for https://github.com/haishanh/yacd/issues/338
This commit is contained in:
Haishan 2019-12-01 22:41:59 +08:00
parent 19ecf435de
commit 8b5ecb3f18
16 changed files with 229 additions and 40 deletions

View file

@ -10,7 +10,8 @@ const presets = [
corejs: 3
}
],
'@babel/preset-react'
'@babel/preset-react',
'@babel/preset-flow'
];
const plugins = [

View file

@ -67,6 +67,7 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"@babel/preset-flow": "^7.7.4",
"@babel/preset-react": "^7.7.0",
"autoprefixer": "^9.7.1",
"babel-eslint": "^10.0.3",

View file

@ -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 };

View file

@ -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} onClick={onClick}>
{label}
<button className={s0.btn} ref={ref} onClick={onClick}>
{children || label}
</button>
);
});
}
Button.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func
};
function WithIcon({ text, icon, onClick = noop }, ref) {
return (
<button className={s0.btn} ref={ref} onClick={onClick}>
<div className={s0.withIconWrapper}>
{icon}
<span className={s0.txt}>{text}</span>
</div>
</button>
);
}
export default Button;
export const ButtonWithIcon = memo(forwardRef(WithIcon));
export default memo(forwardRef(Button));

View file

@ -25,3 +25,11 @@
padding: 6px 12px;
}
}
.withIconWrapper {
display: flex;
align-items: center;
.txt {
margin-left: 5px;
}
}

View file

@ -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
);

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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 (

View file

@ -8,10 +8,3 @@
padding: 10px 40px;
}
}
.fabgrp {
position: fixed;
z-index: 1;
right: 20px;
bottom: 20px;
}

View file

@ -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;
}

View file

@ -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>
);

View file

@ -1,5 +1 @@
.fabgrp {
position: fixed;
right: 20px;
bottom: 20px;
}
/* */

View file

@ -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}

View file

@ -336,6 +336,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-flow@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.7.4.tgz#6d91b59e1a0e4c17f36af2e10dd64ef220919d7b"
integrity sha512-2AMAWl5PsmM5KPkB22cvOkUyWk6MjUaqhHNU5nSPUl/ns3j5qLfw2SuYP5RbVZ0tfLvePr4zUScbICtDP2CUNw==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
@ -453,6 +460,14 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-flow-strip-types@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.7.4.tgz#cc73f85944782df1d77d80977bc097920a8bf31a"
integrity sha512-w9dRNlHY5ElNimyMYy0oQowvQpwt/PRHI0QS98ZJCTZU2bvSnKXo5zEiD5u76FBPigTm8TkqzmnUTg16T7qbkA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-flow" "^7.7.4"
"@babel/plugin-transform-for-of@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556"
@ -714,6 +729,14 @@
js-levenshtein "^1.1.3"
semver "^5.5.0"
"@babel/preset-flow@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.7.4.tgz#99c1349b6fd7132783196de181e6b32d0949427e"
integrity sha512-6LbUqcHD8BcRtXMOp5bc5nJeU8RlKh6q5U8TgZeCrf9ebBdW8Wyy5ujAUnbJfmzQ56Kkq5XtwErC/5+5RHyFYA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-flow-strip-types" "^7.7.4"
"@babel/preset-react@^7.7.0":
version "7.7.0"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.7.0.tgz#8ab0c4787d98cf1f5f22dabf115552bf9e4e406c"