feat: support close all connections
for https://github.com/haishanh/yacd/issues/338
This commit is contained in:
parent
19ecf435de
commit
8b5ecb3f18
16 changed files with 229 additions and 40 deletions
|
@ -10,7 +10,8 @@ const presets = [
|
|||
corejs: 3
|
||||
}
|
||||
],
|
||||
'@babel/preset-react'
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-flow'
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -25,3 +25,11 @@
|
|||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.withIconWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.txt {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
44
src/components/ModalCloseAllConnections.js
Normal file
44
src/components/ModalCloseAllConnections.js
Normal 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>
|
||||
);
|
||||
}
|
23
src/components/ModalCloseAllConnections.module.css
Normal file
23
src/components/ModalCloseAllConnections.module.css
Normal 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;
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -8,10 +8,3 @@
|
|||
padding: 10px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.fabgrp {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -1,5 +1 @@
|
|||
.fabgrp {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
/* */
|
||||
|
|
|
@ -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}
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue