update github
This commit is contained in:
parent
b658f11737
commit
c9770c14d2
38 changed files with 5975 additions and 2807 deletions
|
@ -28,4 +28,3 @@ steps:
|
||||||
when:
|
when:
|
||||||
event: tag
|
event: tag
|
||||||
tag: v*
|
tag: v*
|
||||||
|
|
||||||
|
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
public
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
|
@ -1,5 +1,7 @@
|
||||||
|
[中文](./README_CN.md)
|
||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/1166872/47954055-97e6cb80-dfc0-11e8-991f-230fd40481e5.png" alt="yacd">
|
<img src="https://user-images.githubusercontent.com/78135608/232244383-5e1389db-ce56-4c83-9627-4f3d1a489c6e.png" alt="yacd">
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
> Yet Another [Clash](https://github.com/yaling888/clash) [Dashboard](https://github.com/yaling888/clash-dashboard)
|
> Yet Another [Clash](https://github.com/yaling888/clash) [Dashboard](https://github.com/yaling888/clash-dashboard)
|
||||||
|
@ -8,8 +10,7 @@
|
||||||
|
|
||||||
Install [twemoji](https://github.com/mozilla/twemoji-colr/releases) to display emoji better on Windows system.
|
Install [twemoji](https://github.com/mozilla/twemoji-colr/releases) to display emoji better on Windows system.
|
||||||
|
|
||||||
|
The site http://yacd.metacubex.one is served with HTTP not HTTPS is because many browsers block requests to HTTP resources from a HTTPS website. If you think it's not safe, you could just download the [zip of the gh-pages](https://github.com/MetaCubeX/yacd/archive/gh-pages.zip), unzip and serve those static files with a web server(like Nginx).
|
||||||
The site http://yacd.metacubex.one is served with HTTP not HTTPS is because many browsers block requests to HTTP resources from a HTTPS website. If you think it's not safe, you could just download the [zip of the gh-pages](https://github.com/yaling888/yacd/archive/gh-pages.zip), unzip and serve those static files with a web server(like Nginx).
|
|
||||||
|
|
||||||
**Supported URL query params**
|
**Supported URL query params**
|
||||||
|
|
||||||
|
|
36
README_CN.md
Normal file
36
README_CN.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<h1 align="center">
|
||||||
|
<img src="https://user-images.githubusercontent.com/78135608/232244383-5e1389db-ce56-4c83-9627-4f3d1a489c6e.png" alt="yacd">
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
> Yet Another [Clash](https://github.com/yaling888/clash) [Dashboard](https://github.com/yaling888/clash-dashboard)
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
安装[twemoji](https://github.com/mozilla/twemoji-colr/releases)以在 Windows 系统上更好地显示表情符号。
|
||||||
|
|
||||||
|
网站 http://yacd.metacubex.one 是通过 HTTP 提供服务的,而不是 HTTPS,因为许多浏览器会阻止从 HTTPS 网站请求 HTTP 资源。如果认为这不安全,可以下载[gh-pages 的 zip 文件](https://github.com/MetaCubeX/yacd/archive/gh-pages.zip),解压缩并使用 Web 服务器(如 Nginx)提供这些静态文件。
|
||||||
|
|
||||||
|
**支持的 URL 查询参数**
|
||||||
|
|
||||||
|
| 参数 | 描述 |
|
||||||
|
| -------- | ------------------------------------------------------------------ |
|
||||||
|
| hostname | Clash 后端 API 的主机名(通常是`external-controller`的 host 部分) |
|
||||||
|
| port | Clash 后端 API 的端口号(通常是`external-controller`的 port 部分) |
|
||||||
|
| secret | Clash API 密钥(`config.yaml`中的"secret") |
|
||||||
|
| theme | UI 颜色方案(dark、light、auto) |
|
||||||
|
|
||||||
|
## 开发部署
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 安装依赖库
|
||||||
|
# 你可以使用 `npm i -g pnpm` 安装 pnpm
|
||||||
|
pnpm i
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
# 然后访问 http://127.0.0.1:3000
|
||||||
|
pnpm start
|
||||||
|
|
||||||
|
# 构建优化资源
|
||||||
|
# 准备好部署的资源将在目录 `public` 中
|
||||||
|
pnpm build
|
||||||
|
```
|
BIN
assets/Twemoji_Mozilla.ttf
Normal file
BIN
assets/Twemoji_Mozilla.ttf
Normal file
Binary file not shown.
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="shortcut icon" href="yacd.ico" />
|
<link rel="shortcut icon" href="yacd.ico" />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "yacd",
|
"name": "yacd",
|
||||||
"version": "0.3.5",
|
"version": "0.3.6",
|
||||||
"description": "Yet another Clash dashboard",
|
"description": "Yet another Clash dashboard",
|
||||||
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
|
"author": "MetaCubeX (https://github.com/MetaCubeX)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
8043
pnpm-lock.yaml
8043
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,8 @@ import { ClashAPIConfig } from '~/types';
|
||||||
const endpoint = '/configs';
|
const endpoint = '/configs';
|
||||||
const updateGeoDatabasesFileEndpoint = '/configs/geo';
|
const updateGeoDatabasesFileEndpoint = '/configs/geo';
|
||||||
const flushFakeIPPoolEndpoint = '/cache/fakeip/flush';
|
const flushFakeIPPoolEndpoint = '/cache/fakeip/flush';
|
||||||
|
const restartCoreEndpoint = '/restart';
|
||||||
|
const upgradeCoreEndpoint = '/upgrade';
|
||||||
|
|
||||||
export async function fetchConfigs(apiConfig: ClashAPIConfig) {
|
export async function fetchConfigs(apiConfig: ClashAPIConfig) {
|
||||||
const { url, init } = getURLAndInit(apiConfig);
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
|
@ -42,6 +44,18 @@ export async function updateGeoDatabasesFile(apiConfig: ClashAPIConfig) {
|
||||||
return await fetch(url + updateGeoDatabasesFileEndpoint, { ...init, body, method: 'POST' });
|
return await fetch(url + updateGeoDatabasesFileEndpoint, { ...init, body, method: 'POST' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartCore(apiConfig: ClashAPIConfig) {
|
||||||
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
|
const body = '{"path": "", "payload": ""}';
|
||||||
|
return await fetch(url + restartCoreEndpoint, { ...init, body, method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upgradeCore(apiConfig: ClashAPIConfig) {
|
||||||
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
|
const body = '{"path": "", "payload": ""}';
|
||||||
|
return await fetch(url + upgradeCoreEndpoint, { ...init, body, method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
export async function flushFakeIPPool(apiConfig: ClashAPIConfig) {
|
export async function flushFakeIPPool(apiConfig: ClashAPIConfig) {
|
||||||
const { url, init } = getURLAndInit(apiConfig);
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
return await fetch(url + flushFakeIPPoolEndpoint, { ...init, method: 'POST' });
|
return await fetch(url + flushFakeIPPoolEndpoint, { ...init, method: 'POST' });
|
||||||
|
|
|
@ -5,6 +5,12 @@ import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
|
||||||
const endpoint = '/connections';
|
const endpoint = '/connections';
|
||||||
|
|
||||||
const fetched = false;
|
const fetched = false;
|
||||||
|
|
||||||
|
interface Subscriber {
|
||||||
|
listner: unknown; // on data received, listener will be called with data
|
||||||
|
onClose: () => void; // on stream closed, onClose will be called
|
||||||
|
}
|
||||||
|
|
||||||
const subscribers = [];
|
const subscribers = [];
|
||||||
|
|
||||||
// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41
|
// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41
|
||||||
|
@ -48,28 +54,49 @@ function appendData(s: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('JSON.parse error', JSON.parse(s));
|
console.log('JSON.parse error', JSON.parse(s));
|
||||||
}
|
}
|
||||||
subscribers.forEach((f) => f(o));
|
subscribers.forEach((s) => s.listner(o));
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnsubscribeFn = () => void;
|
type UnsubscribeFn = () => void;
|
||||||
|
|
||||||
let wsState: number;
|
let wsState: number;
|
||||||
export function fetchData(apiConfig: ClashAPIConfig, listener: unknown): UnsubscribeFn | void {
|
export function fetchData(
|
||||||
|
apiConfig: ClashAPIConfig,
|
||||||
|
listener: unknown,
|
||||||
|
onClose: () => void
|
||||||
|
): UnsubscribeFn | void {
|
||||||
if (fetched || wsState === 1) {
|
if (fetched || wsState === 1) {
|
||||||
if (listener) return subscribe(listener);
|
if (listener)
|
||||||
|
return subscribe({
|
||||||
|
listner: listener,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
wsState = 1;
|
wsState = 1;
|
||||||
const url = buildWebSocketURL(apiConfig, endpoint);
|
const url = buildWebSocketURL(apiConfig, endpoint);
|
||||||
const ws = new WebSocket(url);
|
const ws = new WebSocket(url);
|
||||||
ws.addEventListener('error', () => (wsState = 3));
|
ws.addEventListener('error', () => {
|
||||||
|
wsState = 3;
|
||||||
|
subscribers.forEach((s) => s.onClose());
|
||||||
|
subscribers.length = 0;
|
||||||
|
});
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
wsState = 3;
|
||||||
|
subscribers.forEach((s) => s.onClose());
|
||||||
|
subscribers.length = 0;
|
||||||
|
});
|
||||||
ws.addEventListener('message', (event) => appendData(event.data));
|
ws.addEventListener('message', (event) => appendData(event.data));
|
||||||
if (listener) return subscribe(listener);
|
if (listener)
|
||||||
|
return subscribe({
|
||||||
|
listner: listener,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribe(listener: unknown): UnsubscribeFn {
|
function subscribe(subscriber: Subscriber): UnsubscribeFn {
|
||||||
subscribers.push(listener);
|
subscribers.push(subscriber);
|
||||||
return function unsubscribe() {
|
return function unsubscribe() {
|
||||||
const idx = subscribers.indexOf(listener);
|
const idx = subscribers.indexOf(subscriber);
|
||||||
subscribers.splice(idx, 1);
|
subscribers.splice(idx, 1);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
119
src/api/memory.ts
Normal file
119
src/api/memory.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import { ClashAPIConfig } from '~/types';
|
||||||
|
|
||||||
|
import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
|
||||||
|
|
||||||
|
const endpoint = '/memory';
|
||||||
|
const textDecoder = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
const Size = 150;
|
||||||
|
|
||||||
|
const memory = {
|
||||||
|
labels: Array(Size).fill(0),
|
||||||
|
inuse: Array(Size),
|
||||||
|
oslimit: Array(Size),
|
||||||
|
|
||||||
|
size: Size,
|
||||||
|
subscribers: [],
|
||||||
|
appendData(o: { inuse: number; oslimit: number }) {
|
||||||
|
this.inuse.shift();
|
||||||
|
this.oslimit.shift();
|
||||||
|
this.labels.shift();
|
||||||
|
|
||||||
|
const l = Date.now();
|
||||||
|
this.inuse.push(o.inuse);
|
||||||
|
this.oslimit.push(o.oslimit);
|
||||||
|
this.labels.push(l);
|
||||||
|
|
||||||
|
this.subscribers.forEach((f) => f(o));
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe(listener: (x: any) => void) {
|
||||||
|
this.subscribers.push(listener);
|
||||||
|
return () => {
|
||||||
|
const idx = this.subscribers.indexOf(listener);
|
||||||
|
this.subscribers.splice(idx, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let fetched = false;
|
||||||
|
let decoded = '';
|
||||||
|
|
||||||
|
function parseAndAppend(x: string) {
|
||||||
|
memory.appendData(JSON.parse(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pump(reader: ReadableStreamDefaultReader) {
|
||||||
|
return reader.read().then(({ done, value }) => {
|
||||||
|
const str = textDecoder.decode(value, { stream: !done });
|
||||||
|
decoded += str;
|
||||||
|
|
||||||
|
const splits = decoded.split('\n');
|
||||||
|
|
||||||
|
const lastSplit = splits[splits.length - 1];
|
||||||
|
|
||||||
|
for (let i = 0; i < splits.length - 1; i++) {
|
||||||
|
parseAndAppend(splits[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
parseAndAppend(lastSplit);
|
||||||
|
decoded = '';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('GET /memory streaming done');
|
||||||
|
fetched = false;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
decoded = lastSplit;
|
||||||
|
}
|
||||||
|
return pump(reader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 OPEN
|
||||||
|
// other value CLOSED
|
||||||
|
// similar to ws readyState but not the same
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
||||||
|
let wsState: number;
|
||||||
|
function fetchData(apiConfig: ClashAPIConfig) {
|
||||||
|
if (fetched || wsState === 1) return memory;
|
||||||
|
wsState = 1;
|
||||||
|
const url = buildWebSocketURL(apiConfig, endpoint);
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
ws.addEventListener('error', function (_ev) {
|
||||||
|
wsState = 3;
|
||||||
|
});
|
||||||
|
ws.addEventListener('close', function (_ev) {
|
||||||
|
wsState = 3;
|
||||||
|
fetchDataWithFetch(apiConfig);
|
||||||
|
});
|
||||||
|
ws.addEventListener('message', function (event) {
|
||||||
|
parseAndAppend(event.data);
|
||||||
|
});
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDataWithFetch(apiConfig: ClashAPIConfig) {
|
||||||
|
if (fetched) return memory;
|
||||||
|
fetched = true;
|
||||||
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
|
fetch(url + endpoint, init).then(
|
||||||
|
(response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
pump(reader);
|
||||||
|
} else {
|
||||||
|
fetched = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('fetch /memory error', err);
|
||||||
|
fetched = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchData };
|
|
@ -34,7 +34,7 @@ export async function requestToSwitchProxy(apiConfig, name1, name2) {
|
||||||
export async function requestDelayForProxy(
|
export async function requestDelayForProxy(
|
||||||
apiConfig,
|
apiConfig,
|
||||||
name,
|
name,
|
||||||
latencyTestUrl = 'http://www.gstatic.com/generate_204'
|
latencyTestUrl = 'https://www.gstatic.com/generate_204'
|
||||||
) {
|
) {
|
||||||
const { url, init } = getURLAndInit(apiConfig);
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
const qs = `timeout=5000&url=${encodeURIComponent(latencyTestUrl)}`;
|
const qs = `timeout=5000&url=${encodeURIComponent(latencyTestUrl)}`;
|
||||||
|
|
|
@ -15,8 +15,10 @@ import {
|
||||||
flushFakeIPPool,
|
flushFakeIPPool,
|
||||||
getConfigs,
|
getConfigs,
|
||||||
reloadConfigFile,
|
reloadConfigFile,
|
||||||
|
restartCore,
|
||||||
updateConfigs,
|
updateConfigs,
|
||||||
updateGeoDatabasesFile,
|
updateGeoDatabasesFile,
|
||||||
|
upgradeCore,
|
||||||
} from '../store/configs';
|
} from '../store/configs';
|
||||||
import { openModal } from '../store/modals';
|
import { openModal } from '../store/modals';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
@ -206,6 +208,14 @@ function ConfigImpl({
|
||||||
dispatch(reloadConfigFile(apiConfig));
|
dispatch(reloadConfigFile(apiConfig));
|
||||||
}, [apiConfig, dispatch]);
|
}, [apiConfig, dispatch]);
|
||||||
|
|
||||||
|
const handleRestartCore = useCallback(() => {
|
||||||
|
dispatch(restartCore(apiConfig));
|
||||||
|
}, [apiConfig, dispatch]);
|
||||||
|
|
||||||
|
const handleUpgradeCore = useCallback(() => {
|
||||||
|
dispatch(upgradeCore(apiConfig));
|
||||||
|
}, [apiConfig, dispatch]);
|
||||||
|
|
||||||
const handleUpdateGeoDatabasesFile = useCallback(() => {
|
const handleUpdateGeoDatabasesFile = useCallback(() => {
|
||||||
dispatch(updateGeoDatabasesFile(apiConfig));
|
dispatch(updateGeoDatabasesFile(apiConfig));
|
||||||
}, [apiConfig, dispatch]);
|
}, [apiConfig, dispatch]);
|
||||||
|
@ -351,6 +361,22 @@ function ConfigImpl({
|
||||||
onClick={handleFlushFakeIPPool}
|
onClick={handleFlushFakeIPPool}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>Restart</div>
|
||||||
|
<Button
|
||||||
|
start={<RotateCw size={16} />}
|
||||||
|
label={t('restart_core')}
|
||||||
|
onClick={handleRestartCore}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={s0.label}>⚠️ Upgrade ⚠️</div>
|
||||||
|
<Button
|
||||||
|
start={<RotateCw size={16} />}
|
||||||
|
label={t('upgrade_core')}
|
||||||
|
onClick={handleUpgradeCore}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={s0.sep}>
|
<div className={s0.sep}>
|
||||||
<div />
|
<div />
|
||||||
|
|
|
@ -5,15 +5,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.th {
|
.th {
|
||||||
padding: 8px 10px;
|
padding: 3% 2px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
font-size: 0.9em;
|
font-size: 1em;
|
||||||
text-align: center;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -23,10 +23,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thdu {
|
||||||
|
padding: 3% 2px;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--color-background);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
user-select: none;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text-highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.break {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.td {
|
.td {
|
||||||
padding: 8px 13px;
|
/* 上边下边 | 左边右边 */
|
||||||
|
padding: 10px 2px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
// max-width: 14em;
|
||||||
|
min-width: 9em;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-text-highlight);
|
color: var(--color-text-highlight);
|
||||||
}
|
}
|
||||||
|
@ -38,15 +70,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* download upload td cells */
|
/* download upload td cells */
|
||||||
.du {
|
.center {
|
||||||
text-align: right;
|
min-width: 7em;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortIconContainer {
|
.sortIconContainer {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: 10px;
|
margin-left: 0.5em;
|
||||||
width: 16px;
|
width: 1em;
|
||||||
height: 16px;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate180 {
|
.rotate180 {
|
||||||
|
|
|
@ -13,19 +13,19 @@ const sortDescFirst = true;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ accessor: 'id', show: false },
|
{ accessor: 'id', show: false },
|
||||||
{ Header: 'c_host', accessor: 'host' },
|
{ Header: 'c_type', accessor: 'type' },
|
||||||
{ Header: 'c_sni', accessor: 'sniffHost' },
|
|
||||||
{ Header: 'c_process', accessor: 'process' },
|
{ Header: 'c_process', accessor: 'process' },
|
||||||
{ Header: 'c_dl', accessor: 'download', sortDescFirst },
|
{ Header: 'c_host', accessor: 'host' },
|
||||||
{ Header: 'c_ul', accessor: 'upload', sortDescFirst },
|
{ Header: 'c_rule', accessor: 'rule' },
|
||||||
|
{ Header: 'c_chains', accessor: 'chains' },
|
||||||
|
{ Header: 'c_time', accessor: 'start' },
|
||||||
{ Header: 'c_dl_speed', accessor: 'downloadSpeedCurr', sortDescFirst },
|
{ Header: 'c_dl_speed', accessor: 'downloadSpeedCurr', sortDescFirst },
|
||||||
{ Header: 'c_ul_speed', accessor: 'uploadSpeedCurr', sortDescFirst },
|
{ Header: 'c_ul_speed', accessor: 'uploadSpeedCurr', sortDescFirst },
|
||||||
{ Header: 'c_chains', accessor: 'chains' },
|
{ Header: 'c_dl', accessor: 'download', sortDescFirst },
|
||||||
{ Header: 'c_rule', accessor: 'rule' },
|
{ Header: 'c_ul', accessor: 'upload', sortDescFirst },
|
||||||
{ Header: 'c_time', accessor: 'start', sortDescFirst },
|
|
||||||
{ Header: 'c_source', accessor: 'source' },
|
{ Header: 'c_source', accessor: 'source' },
|
||||||
{ Header: 'c_destination_ip', accessor: 'destinationIP' },
|
{ Header: 'c_destination_ip', accessor: 'destinationIP' },
|
||||||
{ Header: 'c_type', accessor: 'type' },
|
{ Header: 'c_sni', accessor: 'sniffHost' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderCell(cell: { column: { id: string }; value: number }, locale: Locale) {
|
function renderCell(cell: { column: { id: string }; value: number }, locale: Locale) {
|
||||||
|
@ -71,8 +71,11 @@ function Table({ data }) {
|
||||||
{headerGroups.map((headerGroup) => {
|
{headerGroups.map((headerGroup) => {
|
||||||
return (
|
return (
|
||||||
<div {...headerGroup.getHeaderGroupProps()} className={s.tr}>
|
<div {...headerGroup.getHeaderGroupProps()} className={s.tr}>
|
||||||
{headerGroup.headers.map((column) => (
|
{headerGroup.headers.map((column, index) => (
|
||||||
<div {...column.getHeaderProps(column.getSortByToggleProps())} className={s.th}>
|
<div
|
||||||
|
{...column.getHeaderProps(column.getSortByToggleProps())}
|
||||||
|
className={index == 0 || (index >= 5 && index < 10) ? s.thdu : s.th}
|
||||||
|
>
|
||||||
<span>{t(column.render('Header'))}</span>
|
<span>{t(column.render('Header'))}</span>
|
||||||
<span className={s.sortIconContainer}>
|
<span className={s.sortIconContainer}>
|
||||||
{column.isSorted ? (
|
{column.isSorted ? (
|
||||||
|
@ -93,7 +96,8 @@ function Table({ data }) {
|
||||||
className={cx(
|
className={cx(
|
||||||
s.td,
|
s.td,
|
||||||
i % 2 === 0 ? s.odd : false,
|
i % 2 === 0 ? s.odd : false,
|
||||||
j >= 2 && j <= 5 ? s.du : false
|
j == 0 || (j >= 5 && j < 10) ? s.center : true
|
||||||
|
// j ==1 ? s.break : true
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderCell(cell, locale)}
|
{renderCell(cell, locale)}
|
||||||
|
|
|
@ -47,3 +47,7 @@
|
||||||
.react-tabs__tab-panel--selected {
|
.react-tabs__tab-panel--selected {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._btn_lzu00_1 {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { State } from '~/store/types';
|
||||||
import * as connAPI from '../api/connections';
|
import * as connAPI from '../api/connections';
|
||||||
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
|
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
|
||||||
import { getClashAPIConfig } from '../store/app';
|
import { getClashAPIConfig } from '../store/app';
|
||||||
|
import Button from './Button';
|
||||||
import s from './Connections.module.scss';
|
import s from './Connections.module.scss';
|
||||||
import ConnectionTable from './ConnectionTable';
|
import ConnectionTable from './ConnectionTable';
|
||||||
import ContentHeader from './ContentHeader';
|
import ContentHeader from './ContentHeader';
|
||||||
|
@ -58,22 +59,38 @@ function hasSubstring(s: string, pat: string) {
|
||||||
return s.toLowerCase().includes(pat.toLowerCase());
|
return s.toLowerCase().includes(pat.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterConns(conns: FormattedConn[], keyword: string) {
|
function filterConnIps(conns: FormattedConn[], ipStr: string) {
|
||||||
return !keyword
|
return conns.filter((each) => each.sourceIP === ipStr);
|
||||||
? conns
|
}
|
||||||
: conns.filter((conn) =>
|
|
||||||
[
|
function filterConns(conns: FormattedConn[], keyword: string, sourceIp: string) {
|
||||||
conn.host,
|
let result = conns;
|
||||||
conn.sourceIP,
|
if (keyword !== '') {
|
||||||
conn.sourcePort,
|
result = conns.filter((conn) =>
|
||||||
conn.destinationIP,
|
[
|
||||||
conn.chains,
|
conn.host,
|
||||||
conn.rule,
|
conn.sourceIP,
|
||||||
conn.type,
|
conn.sourcePort,
|
||||||
conn.network,
|
conn.destinationIP,
|
||||||
conn.process,
|
conn.chains,
|
||||||
].some((field) => hasSubstring(field, keyword))
|
conn.rule,
|
||||||
|
conn.type,
|
||||||
|
conn.network,
|
||||||
|
conn.process,
|
||||||
|
].some((field) => {
|
||||||
|
return hasSubstring(field, keyword);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (sourceIp !== '') {
|
||||||
|
result = filterConnIps(result, sourceIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConnIpList(conns: FormattedConn[]) {
|
||||||
|
return Array.from(new Set(conns.map((x) => x.sourceIP))).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatConnectionDataItem(
|
function formatConnectionDataItem(
|
||||||
|
@ -103,7 +120,7 @@ function formatConnectionDataItem(
|
||||||
upload,
|
upload,
|
||||||
download,
|
download,
|
||||||
start: now - new Date(start).valueOf(),
|
start: now - new Date(start).valueOf(),
|
||||||
chains: chains.reverse().join(' / '),
|
chains: modifyChains(chains),
|
||||||
rule: !rulePayload ? rule : `${rule} :: ${rulePayload}`,
|
rule: !rulePayload ? rule : `${rule} :: ${rulePayload}`,
|
||||||
...metadata,
|
...metadata,
|
||||||
host: `${host2}:${destinationPort}`,
|
host: `${host2}:${destinationPort}`,
|
||||||
|
@ -117,6 +134,24 @@ function formatConnectionDataItem(
|
||||||
};
|
};
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
function modifyChains(chains: string[]): string {
|
||||||
|
if (!Array.isArray(chains) || chains.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chains.length === 1) {
|
||||||
|
return chains[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
//倒序
|
||||||
|
if (chains.length === 2) {
|
||||||
|
return chains[1] + ' / ' + chains[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = chains.pop();
|
||||||
|
const last = chains.shift();
|
||||||
|
return `${first} / ${last}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTableOrPlaceholder(conns: FormattedConn[]) {
|
function renderTableOrPlaceholder(conns: FormattedConn[]) {
|
||||||
return conns.length > 0 ? (
|
return conns.length > 0 ? (
|
||||||
|
@ -134,11 +169,19 @@ function ConnQty({ qty }) {
|
||||||
|
|
||||||
function Conn({ apiConfig }) {
|
function Conn({ apiConfig }) {
|
||||||
const [refContainer, containerHeight] = useRemainingViewPortHeight();
|
const [refContainer, containerHeight] = useRemainingViewPortHeight();
|
||||||
|
|
||||||
const [conns, setConns] = useState([]);
|
const [conns, setConns] = useState([]);
|
||||||
const [closedConns, setClosedConns] = useState([]);
|
const [closedConns, setClosedConns] = useState([]);
|
||||||
|
|
||||||
const [filterKeyword, setFilterKeyword] = useState('');
|
const [filterKeyword, setFilterKeyword] = useState('');
|
||||||
const filteredConns = filterConns(conns, filterKeyword);
|
const [filterSourceIpStr, setFilterSourceIpStr] = useState('');
|
||||||
const filteredClosedConns = filterConns(closedConns, filterKeyword);
|
|
||||||
|
const filteredConns = filterConns(conns, filterKeyword, filterSourceIpStr);
|
||||||
|
const filteredClosedConns = filterConns(closedConns, filterKeyword, filterSourceIpStr);
|
||||||
|
|
||||||
|
const connIpSet = getConnIpList(conns);
|
||||||
|
const ClosedConnIpSet = getConnIpList(closedConns);
|
||||||
|
|
||||||
const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false);
|
const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false);
|
||||||
const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
|
const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
|
||||||
const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []);
|
const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []);
|
||||||
|
@ -178,9 +221,15 @@ function Conn({ apiConfig }) {
|
||||||
},
|
},
|
||||||
[setConns, isRefreshPaused]
|
[setConns, isRefreshPaused]
|
||||||
);
|
);
|
||||||
|
const [reConnectCount, setReConnectCount] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return connAPI.fetchData(apiConfig, read);
|
return connAPI.fetchData(apiConfig, read, () => {
|
||||||
}, [apiConfig, read]);
|
setTimeout(() => {
|
||||||
|
setReConnectCount((prev) => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}, [apiConfig, read, reConnectCount, setReConnectCount]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
@ -230,7 +279,17 @@ function Conn({ apiConfig }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<>{renderTableOrPlaceholder(filteredConns)}</>
|
<Button onClick={() => setFilterSourceIpStr('')} kind="minimal">
|
||||||
|
{t('All')}
|
||||||
|
</Button>
|
||||||
|
{connIpSet.map((value, k) => {
|
||||||
|
return (
|
||||||
|
<Button key={k} onClick={() => setFilterSourceIpStr(value)} kind="minimal">
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderTableOrPlaceholder(filteredConns)}
|
||||||
<Fab
|
<Fab
|
||||||
icon={isRefreshPaused ? <Play size={16} /> : <Pause size={16} />}
|
icon={isRefreshPaused ? <Play size={16} /> : <Pause size={16} />}
|
||||||
mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}}
|
mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}}
|
||||||
|
@ -243,7 +302,19 @@ function Conn({ apiConfig }) {
|
||||||
</Action>
|
</Action>
|
||||||
</Fab>
|
</Fab>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>{renderTableOrPlaceholder(filteredClosedConns)}</TabPanel>
|
<TabPanel>
|
||||||
|
<Button onClick={() => setFilterSourceIpStr('')} kind="minimal">
|
||||||
|
{t('All')}
|
||||||
|
</Button>
|
||||||
|
{ClosedConnIpSet.map((value, k) => {
|
||||||
|
return (
|
||||||
|
<Button key={k} onClick={() => setFilterSourceIpStr(value)} kind="minimal">
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderTableOrPlaceholder(filteredClosedConns)}
|
||||||
|
</TabPanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModalCloseAllConnections
|
<ModalCloseAllConnections
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
padding: 6px 15px;
|
padding: 6px 15px;
|
||||||
@media (--breakpoint-not-small) {
|
@media screen and (min-width: 30em) {
|
||||||
padding: 10px 40px;
|
padding: 10px 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chart {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import ContentHeader from './ContentHeader';
|
import ContentHeader from './ContentHeader';
|
||||||
import s0 from './Home.module.scss';
|
import s0 from './Home.module.scss';
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
|
import MemoryChart from './MemoryChart';
|
||||||
import TrafficChart from './TrafficChart';
|
import TrafficChart from './TrafficChart';
|
||||||
import TrafficNow from './TrafficNow';
|
import TrafficNow from './TrafficNow';
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ export default function Home() {
|
||||||
<div className={s0.chart}>
|
<div className={s0.chart}>
|
||||||
<Suspense fallback={<Loading height="200px" />}>
|
<Suspense fallback={<Loading height="200px" />}>
|
||||||
<TrafficChart />
|
<TrafficChart />
|
||||||
|
<MemoryChart />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
61
src/components/MemoryChart.tsx
Normal file
61
src/components/MemoryChart.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { State } from '~/store/types';
|
||||||
|
|
||||||
|
import { fetchData } from '../api/memory';
|
||||||
|
import { useLineChartMemory } from '../hooks/useLineChart';
|
||||||
|
import {
|
||||||
|
chartJSResource,
|
||||||
|
chartStyles,
|
||||||
|
commonDataSetProps,
|
||||||
|
memoryChartOptions,
|
||||||
|
} from '../misc/chart-memory';
|
||||||
|
import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app';
|
||||||
|
import { connect } from './StateProvider';
|
||||||
|
|
||||||
|
const { useMemo } = React;
|
||||||
|
|
||||||
|
const chartWrapperStyle = {
|
||||||
|
// make chartjs chart responsive
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: 1000,
|
||||||
|
marginTop: '1em',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapState = (s: State) => ({
|
||||||
|
apiConfig: getClashAPIConfig(s),
|
||||||
|
selectedChartStyleIndex: getSelectedChartStyleIndex(s),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapState)(MemoryChart);
|
||||||
|
|
||||||
|
function MemoryChart({ apiConfig, selectedChartStyleIndex }) {
|
||||||
|
const ChartMod = chartJSResource.read();
|
||||||
|
const memory = fetchData(apiConfig);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const data = useMemo(
|
||||||
|
() => ({
|
||||||
|
labels: memory.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...commonDataSetProps,
|
||||||
|
...memoryChartOptions,
|
||||||
|
...chartStyles[selectedChartStyleIndex].inuse,
|
||||||
|
label: t('Memory'),
|
||||||
|
data: memory.inuse,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[memory, selectedChartStyleIndex, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
useLineChartMemory(ChartMod.Chart, 'MemoryChart', data, memory);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message
|
||||||
|
<div style={chartWrapperStyle}>
|
||||||
|
<canvas id="MemoryChart" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,11 +11,10 @@ import { connect } from './StateProvider';
|
||||||
|
|
||||||
const { useMemo } = React;
|
const { useMemo } = React;
|
||||||
|
|
||||||
const chartWrapperStyle = {
|
const chartWrapperStyle: React.CSSProperties = {
|
||||||
// make chartjs chart responsive
|
// make chartjs chart responsive
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxWidth: 1000,
|
maxWidth: 1000,
|
||||||
marginTop: '1em',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapState = (s: State) => ({
|
const mapState = (s: State) => ({
|
||||||
|
@ -53,7 +52,6 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
|
||||||
useLineChart(ChartMod.Chart, 'trafficChart', data, traffic);
|
useLineChart(ChartMod.Chart, 'trafficChart', data, traffic);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message
|
|
||||||
<div style={chartWrapperStyle}>
|
<div style={chartWrapperStyle}>
|
||||||
<canvas id="trafficChart" />
|
<canvas id="trafficChart" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,26 +3,25 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, 140px);
|
||||||
|
grid-gap: 20px;
|
||||||
|
justify-content: space-around;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
|
|
||||||
.sec {
|
.sec {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 19%;
|
|
||||||
margin: 3px;
|
|
||||||
background-color: var(--color-bg-card);
|
background-color: var(--color-bg-card);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
|
||||||
div:nth-child(1) {
|
div:nth-child(1) {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 0.65em;
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
div:nth-child(2) {
|
div:nth-child(2) {
|
||||||
padding: 10px 0 0;
|
padding: 10px 0 0;
|
||||||
font-size: 1em;
|
font-size: 1.3em;
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
width: 48%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default connect(mapState)(TrafficNow);
|
||||||
function TrafficNow({ apiConfig }) {
|
function TrafficNow({ apiConfig }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { upStr, downStr } = useSpeed(apiConfig);
|
const { upStr, downStr } = useSpeed(apiConfig);
|
||||||
const { upTotal, dlTotal, connNumber } = useConnection(apiConfig);
|
const { upTotal, dlTotal, connNumber, mUsage } = useConnection(apiConfig);
|
||||||
return (
|
return (
|
||||||
<div className={s0.TrafficNow}>
|
<div className={s0.TrafficNow}>
|
||||||
<div className={s0.sec}>
|
<div className={s0.sec}>
|
||||||
|
@ -41,6 +41,10 @@ function TrafficNow({ apiConfig }) {
|
||||||
<div>{t('Active Connections')}</div>
|
<div>{t('Active Connections')}</div>
|
||||||
<div>{connNumber}</div>
|
<div>{connNumber}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={s0.sec}>
|
||||||
|
<div>{t('Memory Usage')}</div>
|
||||||
|
<div>{mUsage}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -63,13 +67,15 @@ function useConnection(apiConfig) {
|
||||||
upTotal: '0 B',
|
upTotal: '0 B',
|
||||||
dlTotal: '0 B',
|
dlTotal: '0 B',
|
||||||
connNumber: 0,
|
connNumber: 0,
|
||||||
|
mUsage: '0 B',
|
||||||
});
|
});
|
||||||
const read = useCallback(
|
const read = useCallback(
|
||||||
({ downloadTotal, uploadTotal, connections }) => {
|
({ downloadTotal, uploadTotal, connections, memory }) => {
|
||||||
setState({
|
setState({
|
||||||
upTotal: prettyBytes(uploadTotal),
|
upTotal: prettyBytes(uploadTotal),
|
||||||
dlTotal: prettyBytes(downloadTotal),
|
dlTotal: prettyBytes(downloadTotal),
|
||||||
connNumber: connections.length,
|
connNumber: connections.length,
|
||||||
|
mUsage: prettyBytes(memory),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setState]
|
[setState]
|
||||||
|
|
|
@ -76,8 +76,8 @@
|
||||||
|
|
||||||
.proxySmall {
|
.proxySmall {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
.now {
|
.now {
|
||||||
|
|
|
@ -50,7 +50,7 @@ function ProxyGroupImpl({
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelectable = useMemo(
|
const isSelectable = useMemo(
|
||||||
() => ['Selector', version.meta && 'Fallback'].includes(type),
|
() => ['Selector', version.meta && 'Fallback', version.meta && 'URLTest'].includes(type),
|
||||||
[type, version.meta]
|
[type, version.meta]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
.proxyLatency {
|
.proxyLatency {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
font-size: 0.6em;
|
font-size: 1em;
|
||||||
@media (--breakpoint-not-small) {
|
@media (--breakpoint-not-small) {
|
||||||
font-size: 0.7em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ function ProxyProviderImpl({
|
||||||
const getYear = expire.getFullYear() + '-';
|
const getYear = expire.getFullYear() + '-';
|
||||||
const getMonth =
|
const getMonth =
|
||||||
(expire.getMonth() + 1 < 10 ? '0' + (expire.getMonth() + 1) : expire.getMonth() + 1) + '-';
|
(expire.getMonth() + 1 < 10 ? '0' + (expire.getMonth() + 1) : expire.getMonth() + 1) + '-';
|
||||||
const getDate = expire.getDate() + ' ';
|
const getDate = (expire.getDate() < 10 ? '0' + expire.getDate() : expire.getDate()) + ' ';
|
||||||
return getYear + getMonth + getDate;
|
return getYear + getMonth + getDate;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,7 @@ h2.sectionNameType {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@media (--breakpoint-not-small) {
|
@media (--breakpoint-not-small) {
|
||||||
font-size: 1.1em;
|
font-size: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
span:nth-child(2) {
|
span:nth-child(2) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { ChartConfiguration } from 'chart.js';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { commonChartOptions } from '~/misc/chart';
|
import { commonChartOptions } from '~/misc/chart';
|
||||||
|
import { memoryChartOptions } from '~/misc/chart-memory';
|
||||||
|
|
||||||
const { useEffect } = React;
|
const { useEffect } = React;
|
||||||
|
|
||||||
|
@ -23,3 +24,22 @@ export default function useLineChart(
|
||||||
};
|
};
|
||||||
}, [chart, elementId, data, subscription, extraChartOptions]);
|
}, [chart, elementId, data, subscription, extraChartOptions]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useLineChartMemory(
|
||||||
|
chart: typeof import('chart.js').Chart,
|
||||||
|
elementId: string,
|
||||||
|
data: ChartConfiguration['data'],
|
||||||
|
subscription: any,
|
||||||
|
extraChartOptions = {}
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const ctx = (document.getElementById(elementId) as HTMLCanvasElement).getContext('2d');
|
||||||
|
const options = { ...memoryChartOptions, ...extraChartOptions };
|
||||||
|
const c = new chart(ctx, { type: 'line', data, options });
|
||||||
|
const unsubscribe = subscription && subscription.subscribe(() => c.update());
|
||||||
|
return () => {
|
||||||
|
unsubscribe && unsubscribe();
|
||||||
|
c.destroy();
|
||||||
|
};
|
||||||
|
}, [chart, elementId, data, subscription, extraChartOptions]);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const data = {
|
export const data = {
|
||||||
|
All: 'All',
|
||||||
Overview: 'Overview',
|
Overview: 'Overview',
|
||||||
Proxies: 'Proxies',
|
Proxies: 'Proxies',
|
||||||
Rules: 'Rules',
|
Rules: 'Rules',
|
||||||
|
@ -10,6 +11,7 @@ export const data = {
|
||||||
'Upload Total': 'Upload Total',
|
'Upload Total': 'Upload Total',
|
||||||
'Download Total': 'Download Total',
|
'Download Total': 'Download Total',
|
||||||
'Active Connections': 'Active Connections',
|
'Active Connections': 'Active Connections',
|
||||||
|
'Memory Usage': 'Memory Usage',
|
||||||
'Pause Refresh': 'Pause Refresh',
|
'Pause Refresh': 'Pause Refresh',
|
||||||
'Resume Refresh': 'Resume Refresh',
|
'Resume Refresh': 'Resume Refresh',
|
||||||
close_all_connections: 'Close All Connections',
|
close_all_connections: 'Close All Connections',
|
||||||
|
@ -39,6 +41,8 @@ export const data = {
|
||||||
update_all_rule_provider: 'Update all rule providers',
|
update_all_rule_provider: 'Update all rule providers',
|
||||||
update_all_proxy_provider: 'Update all proxy providers',
|
update_all_proxy_provider: 'Update all proxy providers',
|
||||||
reload_config_file: 'Reload config file',
|
reload_config_file: 'Reload config file',
|
||||||
|
restart_core: 'Restart clash core',
|
||||||
|
upgrade_core: 'Upgrade Alpha core',
|
||||||
update_geo_databases_file: 'Update GEO Databases ',
|
update_geo_databases_file: 'Update GEO Databases ',
|
||||||
flush_fake_ip_pool: 'Flush fake-ip data',
|
flush_fake_ip_pool: 'Flush fake-ip data',
|
||||||
enable_tun_device: 'Enable TUN Device',
|
enable_tun_device: 'Enable TUN Device',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const data = {
|
export const data = {
|
||||||
|
All: '所有',
|
||||||
Overview: '概览',
|
Overview: '概览',
|
||||||
Proxies: '代理',
|
Proxies: '代理',
|
||||||
Rules: '规则',
|
Rules: '规则',
|
||||||
|
@ -10,6 +11,8 @@ export const data = {
|
||||||
'Upload Total': '上传总量',
|
'Upload Total': '上传总量',
|
||||||
'Download Total': '下载总量',
|
'Download Total': '下载总量',
|
||||||
'Active Connections': '活动连接',
|
'Active Connections': '活动连接',
|
||||||
|
'Memory Usage': '内存使用情况',
|
||||||
|
Memory: '内存',
|
||||||
'Pause Refresh': '暂停刷新',
|
'Pause Refresh': '暂停刷新',
|
||||||
'Resume Refresh': '继续刷新',
|
'Resume Refresh': '继续刷新',
|
||||||
close_all_connections: '关闭所有连接',
|
close_all_connections: '关闭所有连接',
|
||||||
|
@ -57,4 +60,6 @@ export const data = {
|
||||||
c_source: '来源',
|
c_source: '来源',
|
||||||
c_destination_ip: '目标IP',
|
c_destination_ip: '目标IP',
|
||||||
c_type: '类型',
|
c_type: '类型',
|
||||||
|
restart_core: '重启 clash 核心',
|
||||||
|
upgrade_core: '更新 Alpha 核心',
|
||||||
};
|
};
|
||||||
|
|
64
src/misc/chart-memory.ts
Normal file
64
src/misc/chart-memory.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { createAsset } from 'use-asset';
|
||||||
|
|
||||||
|
import prettyBytes from './pretty-bytes';
|
||||||
|
export const chartJSResource = createAsset(() => {
|
||||||
|
return import('~/misc/chart-lib');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const commonDataSetProps = { borderWidth: 1, pointRadius: 0, tension: 0.2, fill: true };
|
||||||
|
|
||||||
|
export const memoryChartOptions: import('chart.js').ChartOptions<'line'> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { labels: { boxWidth: 20 } },
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { display: false, type: 'category' },
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: '#555',
|
||||||
|
drawTicks: false,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
dash: [3, 6],
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 3,
|
||||||
|
callback(value: number) {
|
||||||
|
return prettyBytes(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chartStyles = [
|
||||||
|
{
|
||||||
|
inuse: {
|
||||||
|
backgroundColor: 'rgba(81, 168, 221, 0.5)',
|
||||||
|
borderColor: 'rgb(81, 168, 221)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inuse: {
|
||||||
|
backgroundColor: 'rgba(245,78,162,0.6)',
|
||||||
|
borderColor: 'rgba(245,78,162,1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inuse: {
|
||||||
|
backgroundColor: 'rgba(94, 175, 223, 0.3)',
|
||||||
|
borderColor: 'rgb(94, 175, 223)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inuse: {
|
||||||
|
backgroundColor: 'rgba(242, 174, 62, 0.3)',
|
||||||
|
borderColor: 'rgb(242, 174, 62)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -27,6 +27,7 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = {
|
||||||
dash: [3, 6],
|
dash: [3, 6],
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
callback(value: number) {
|
callback(value: number) {
|
||||||
return prettyBytes(value) + '/s ';
|
return prettyBytes(value) + '/s ';
|
||||||
},
|
},
|
||||||
|
@ -38,22 +39,22 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = {
|
||||||
export const chartStyles = [
|
export const chartStyles = [
|
||||||
{
|
{
|
||||||
down: {
|
down: {
|
||||||
backgroundColor: 'rgba(176, 209, 132, 0.8)',
|
backgroundColor: 'rgba(81, 168, 221, 0.5)',
|
||||||
borderColor: 'rgb(176, 209, 132)',
|
borderColor: 'rgb(81, 168, 221)',
|
||||||
},
|
},
|
||||||
up: {
|
up: {
|
||||||
backgroundColor: 'rgba(181, 220, 231, 0.8)',
|
backgroundColor: 'rgba(219, 77, 109, 0.5)',
|
||||||
borderColor: 'rgb(181, 220, 231)',
|
borderColor: 'rgb(219, 77, 109)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: {
|
up: {
|
||||||
backgroundColor: 'rgb(98, 190, 100)',
|
backgroundColor: 'rgba(245,78,162,0.6)',
|
||||||
borderColor: 'rgb(78,146,79)',
|
borderColor: 'rgba(245,78,162,1)',
|
||||||
},
|
},
|
||||||
down: {
|
down: {
|
||||||
backgroundColor: 'rgb(160, 230, 66)',
|
backgroundColor: 'rgba(123,59,140,0.6)',
|
||||||
borderColor: 'rgb(110, 156, 44)',
|
borderColor: 'rgba(66,33,142,1)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -47,7 +47,7 @@ i18next
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
supportedLngs: ['en', 'zh'],
|
supportedLngs: ['zh', 'en'],
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
|
|
|
@ -95,7 +95,7 @@ export function updateClashAPIConfig({ baseURL, secret }) {
|
||||||
const rootEl = document.querySelector('html');
|
const rootEl = document.querySelector('html');
|
||||||
type ThemeType = 'dark' | 'light' | 'auto';
|
type ThemeType = 'dark' | 'light' | 'auto';
|
||||||
|
|
||||||
function setTheme(theme: ThemeType = 'dark') {
|
function setTheme(theme: ThemeType = 'light') {
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
rootEl.setAttribute('data-theme', 'auto');
|
rootEl.setAttribute('data-theme', 'auto');
|
||||||
} else if (theme === 'dark') {
|
} else if (theme === 'dark') {
|
||||||
|
@ -159,7 +159,7 @@ const defaultState: StateApp = {
|
||||||
selectedClashAPIConfigIndex: 0,
|
selectedClashAPIConfigIndex: 0,
|
||||||
clashAPIConfigs: [defaultClashAPIConfig],
|
clashAPIConfigs: [defaultClashAPIConfig],
|
||||||
|
|
||||||
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
latencyTestUrl: 'https://www.gstatic.com/generate_204',
|
||||||
selectedChartStyleIndex: 0,
|
selectedChartStyleIndex: 0,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,51 @@ export function reloadConfigFile(apiConfig: ClashAPIConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function restartCore(apiConfig: ClashAPIConfig) {
|
||||||
|
return async (dispatch: DispatchFn) => {
|
||||||
|
configsAPI
|
||||||
|
.restartCore(apiConfig)
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
|
if (res.ok === false) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error restart core', res.statusText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error restart core', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchConfigs(apiConfig));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upgradeCore(apiConfig: ClashAPIConfig) {
|
||||||
|
return async (dispatch: DispatchFn) => {
|
||||||
|
configsAPI
|
||||||
|
.upgradeCore(apiConfig)
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
|
if (res.ok === false) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error upgrade core', res.statusText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Error upgrade core', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(fetchConfigs(apiConfig));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
export function updateGeoDatabasesFile(apiConfig: ClashAPIConfig) {
|
export function updateGeoDatabasesFile(apiConfig: ClashAPIConfig) {
|
||||||
return async (dispatch: DispatchFn) => {
|
return async (dispatch: DispatchFn) => {
|
||||||
configsAPI
|
configsAPI
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
@import '@fontsource/roboto-mono/latin-400.css';
|
@import '@fontsource/roboto-mono/latin-400.css';
|
||||||
@import 'modern-normalize/modern-normalize.css';
|
@import 'modern-normalize/modern-normalize.css';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: '_Twemoji Mozilla';
|
||||||
|
src: url('../../assets/Twemoji_Mozilla.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +64,7 @@
|
||||||
:root {
|
:root {
|
||||||
--font-mono: 'Roboto Mono', Menlo, monospace;
|
--font-mono: 'Roboto Mono', Menlo, monospace;
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
--font-normal: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, "Twemoji Mozilla", Segoe UI Emoji, Segoe UI Symbol, 'PingFang SC', 'Microsoft YaHei', '\5fae\8f6f\96c5\9ed1', Arial;
|
--font-normal: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Source Han Sans", "PingFang SC", "Microsoft YaHei" , "微软雅黑", Arial,"Twemoji Mozilla", "_Twemoji Mozilla","Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
--color-focus-blue: #1a73e8;
|
--color-focus-blue: #1a73e8;
|
||||||
--btn-bg: #387cec;
|
--btn-bg: #387cec;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue