diff --git a/src/components/ConnectionTable.module.scss b/src/components/ConnectionTable.module.scss
index 59764ec..d25eb7d 100644
--- a/src/components/ConnectionTable.module.scss
+++ b/src/components/ConnectionTable.module.scss
@@ -5,15 +5,15 @@
}
.th {
- padding: 8px 10px;
+ padding: 3% 2px;
height: 50px;
background: var(--color-background);
position: sticky;
top: 0;
- font-size: 0.9em;
- text-align: center;
+ font-size: 1em;
user-select: none;
-
+ text-align: center;
+ vertical-align: middle;
display: flex;
align-items: center;
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 {
- padding: 8px 13px;
+ /* 上边下边 | 左边右边 */
+ padding: 10px 2px;
font-size: 0.9em;
+ // max-width: 14em;
+ min-width: 9em;
cursor: default;
+ align-items: center;
+ text-align: left;
+ vertical-align: middle;
&:hover {
color: var(--color-text-highlight);
}
@@ -38,15 +70,16 @@
}
/* download upload td cells */
-.du {
- text-align: right;
+.center {
+ min-width: 7em;
+ text-align: center;
}
.sortIconContainer {
display: inline-flex;
- margin-left: 10px;
- width: 16px;
- height: 16px;
+ margin-left: 0.5em;
+ width: 1em;
+ height: 1em;
}
.rotate180 {
diff --git a/src/components/ConnectionTable.tsx b/src/components/ConnectionTable.tsx
index 2c3ae13..2b0a12a 100644
--- a/src/components/ConnectionTable.tsx
+++ b/src/components/ConnectionTable.tsx
@@ -13,19 +13,19 @@ const sortDescFirst = true;
const columns = [
{ accessor: 'id', show: false },
- { Header: 'c_host', accessor: 'host' },
- { Header: 'c_sni', accessor: 'sniffHost' },
+ { Header: 'c_type', accessor: 'type' },
{ Header: 'c_process', accessor: 'process' },
- { Header: 'c_dl', accessor: 'download', sortDescFirst },
- { Header: 'c_ul', accessor: 'upload', sortDescFirst },
+ { Header: 'c_host', accessor: 'host' },
+ { Header: 'c_rule', accessor: 'rule' },
+ { Header: 'c_chains', accessor: 'chains' },
+ { Header: 'c_time', accessor: 'start' },
{ Header: 'c_dl_speed', accessor: 'downloadSpeedCurr', sortDescFirst },
{ Header: 'c_ul_speed', accessor: 'uploadSpeedCurr', sortDescFirst },
- { Header: 'c_chains', accessor: 'chains' },
- { Header: 'c_rule', accessor: 'rule' },
- { Header: 'c_time', accessor: 'start', sortDescFirst },
+ { Header: 'c_dl', accessor: 'download', sortDescFirst },
+ { Header: 'c_ul', accessor: 'upload', sortDescFirst },
{ Header: 'c_source', accessor: 'source' },
{ 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) {
@@ -71,8 +71,11 @@ function Table({ data }) {
{headerGroups.map((headerGroup) => {
return (
- {headerGroup.headers.map((column) => (
-
+ {headerGroup.headers.map((column, index) => (
+
= 5 && index < 10) ? s.thdu : s.th}
+ >
{t(column.render('Header'))}
{column.isSorted ? (
@@ -93,7 +96,8 @@ function Table({ data }) {
className={cx(
s.td,
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)}
diff --git a/src/components/Connections.css b/src/components/Connections.css
index bc69a62..55dd869 100644
--- a/src/components/Connections.css
+++ b/src/components/Connections.css
@@ -47,3 +47,7 @@
.react-tabs__tab-panel--selected {
display: block;
}
+
+._btn_lzu00_1 {
+ margin-right: 10px;
+}
diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx
index fd29390..80efdfe 100644
--- a/src/components/Connections.tsx
+++ b/src/components/Connections.tsx
@@ -11,6 +11,7 @@ import { State } from '~/store/types';
import * as connAPI from '../api/connections';
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
import { getClashAPIConfig } from '../store/app';
+import Button from './Button';
import s from './Connections.module.scss';
import ConnectionTable from './ConnectionTable';
import ContentHeader from './ContentHeader';
@@ -58,22 +59,38 @@ function hasSubstring(s: string, pat: string) {
return s.toLowerCase().includes(pat.toLowerCase());
}
-function filterConns(conns: FormattedConn[], keyword: string) {
- return !keyword
- ? conns
- : conns.filter((conn) =>
- [
- conn.host,
- conn.sourceIP,
- conn.sourcePort,
- conn.destinationIP,
- conn.chains,
- conn.rule,
- conn.type,
- conn.network,
- conn.process,
- ].some((field) => hasSubstring(field, keyword))
+function filterConnIps(conns: FormattedConn[], ipStr: string) {
+ return conns.filter((each) => each.sourceIP === ipStr);
+}
+
+function filterConns(conns: FormattedConn[], keyword: string, sourceIp: string) {
+ let result = conns;
+ if (keyword !== '') {
+ result = conns.filter((conn) =>
+ [
+ conn.host,
+ conn.sourceIP,
+ conn.sourcePort,
+ conn.destinationIP,
+ conn.chains,
+ 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(
@@ -103,7 +120,7 @@ function formatConnectionDataItem(
upload,
download,
start: now - new Date(start).valueOf(),
- chains: chains.reverse().join(' / '),
+ chains: modifyChains(chains),
rule: !rulePayload ? rule : `${rule} :: ${rulePayload}`,
...metadata,
host: `${host2}:${destinationPort}`,
@@ -117,6 +134,24 @@ function formatConnectionDataItem(
};
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[]) {
return conns.length > 0 ? (
@@ -134,11 +169,19 @@ function ConnQty({ qty }) {
function Conn({ apiConfig }) {
const [refContainer, containerHeight] = useRemainingViewPortHeight();
+
const [conns, setConns] = useState([]);
const [closedConns, setClosedConns] = useState([]);
+
const [filterKeyword, setFilterKeyword] = useState('');
- const filteredConns = filterConns(conns, filterKeyword);
- const filteredClosedConns = filterConns(closedConns, filterKeyword);
+ const [filterSourceIpStr, setFilterSourceIpStr] = useState('');
+
+ 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 openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []);
@@ -178,9 +221,15 @@ function Conn({ apiConfig }) {
},
[setConns, isRefreshPaused]
);
+ const [reConnectCount, setReConnectCount] = useState(0);
+
useEffect(() => {
- return connAPI.fetchData(apiConfig, read);
- }, [apiConfig, read]);
+ return connAPI.fetchData(apiConfig, read, () => {
+ setTimeout(() => {
+ setReConnectCount((prev) => prev + 1);
+ }, 1000);
+ });
+ }, [apiConfig, read, reConnectCount, setReConnectCount]);
const { t } = useTranslation();
@@ -230,7 +279,17 @@ function Conn({ apiConfig }) {
}}
>
- <>{renderTableOrPlaceholder(filteredConns)}>
+ setFilterSourceIpStr('')} kind="minimal">
+ {t('All')}
+
+ {connIpSet.map((value, k) => {
+ return (
+ setFilterSourceIpStr(value)} kind="minimal">
+ {value}
+
+ );
+ })}
+ {renderTableOrPlaceholder(filteredConns)}
: }
mainButtonStyles={isRefreshPaused ? { background: '#e74c3c' } : {}}
@@ -243,7 +302,19 @@ function Conn({ apiConfig }) {
- {renderTableOrPlaceholder(filteredClosedConns)}
+
+ setFilterSourceIpStr('')} kind="minimal">
+ {t('All')}
+
+ {ClosedConnIpSet.map((value, k) => {
+ return (
+ setFilterSourceIpStr(value)} kind="minimal">
+ {value}
+
+ );
+ })}
+ {renderTableOrPlaceholder(filteredClosedConns)}
+
}>
+
diff --git a/src/components/MemoryChart.tsx b/src/components/MemoryChart.tsx
new file mode 100644
index 0000000..9f6febb
--- /dev/null
+++ b/src/components/MemoryChart.tsx
@@ -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
+