import React from 'react';
import ContentHeader from './ContentHeader';
import ConnectionTable from './ConnectionTable';
import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
import { getClashAPIConfig } from '../store/app';
import { X as IconClose, Pause, Play } from 'react-feather';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import SvgYacd from './SvgYacd';
import ModalCloseAllConnections from './ModalCloseAllConnections';
import { connect } from './StateProvider';
import * as connAPI from '../api/connections';
import { Fab, Action, position as fabPosition } from './shared/Fab';
import './Connections.css';
import s from './Connections.module.css';
const { useEffect, useState, useRef, useCallback } = React;
const paddingBottom = 30;
function arrayToIdKv(items) {
const o = {};
for (let i = 0; i < items.length; i++) {
const item = items[i];
o[item.id] = item;
}
return o;
}
function formatConnectionDataItem(i, prevKv) {
const { id, metadata, upload, download, start, chains, rule } = i;
// eslint-disable-next-line prefer-const
let { host, destinationPort, destinationIP } = metadata;
// host could be an empty string if it's direct IP connection
if (host === '') host = destinationIP;
const metadataNext = {
...metadata,
// merge host and destinationPort into one column
host: host + ':' + destinationPort,
};
// const started = formatDistance(new Date(start), now);
const ret = {
id,
upload,
download,
start: 0 - new Date(start),
chains: chains.reverse().join(' / '),
rule,
...metadataNext,
};
const prev = prevKv[id];
ret.downloadSpeedCurr = download - (prev ? prev.download : 0);
ret.uploadSpeedCurr = upload - (prev ? prev.upload : 0);
return ret;
}
function renderTableOrPlaceholder(conns) {
return conns.length > 0 ? (
) : (
);
}
function ConnQty({ qty }) {
return qty < 100 ? '' + qty : '99+';
}
function Conn({ apiConfig }) {
const [refContainer, containerHeight] = useRemainingViewPortHeight();
const [conns, setConns] = useState([]);
const [closedConns, setClosedConns] = useState([]);
const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false);
const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
const closeCloseAllModal = useCallback(
() => setIsCloseAllModalOpen(false),
[]
);
const [isRefreshPaused, setIsRefreshPaused] = useState(false);
const toggleIsRefreshPaused = useCallback(() => {
setIsRefreshPaused((x) => !x);
}, []);
const closeAllConnections = useCallback(() => {
connAPI.closeAllConnections(apiConfig);
closeCloseAllModal();
}, [apiConfig, closeCloseAllModal]);
const prevConnsRef = useRef(conns);
const read = useCallback(
({ connections }) => {
const prevConnsKv = arrayToIdKv(prevConnsRef.current);
const x = connections.map((c) =>
formatConnectionDataItem(c, prevConnsKv)
);
const closed = [];
for (const c of prevConnsRef.current) {
const idx = x.findIndex((conn) => conn.id === c.id);
if (idx < 0) closed.push(c);
}
setClosedConns((prev) => {
// keep max 100 entries
return [...closed, ...prev].slice(0, 101);
});
// if previous connections and current connections are both empty
// arrays, we wont update state to avaoid rerender
if (
x &&
(x.length !== 0 || prevConnsRef.current.length !== 0) &&
!isRefreshPaused
) {
prevConnsRef.current = x;
setConns(x);
} else {
prevConnsRef.current = x;
}
},
[setConns, isRefreshPaused]
);
useEffect(() => {
return connAPI.fetchData(apiConfig, read);
}, [apiConfig, read]);
return (
Active
Closed
<>{renderTableOrPlaceholder(conns)}>
:
}
mainButtonStyles={
isRefreshPaused
? {
background: '#e74c3c',
}
: {}
}
position={fabPosition}
text={isRefreshPaused ? 'Resume Refresh' : 'Pause Refresh'}
onClick={toggleIsRefreshPaused}
>
{renderTableOrPlaceholder(closedConns)}
);
}
const mapState = (s) => ({
apiConfig: getClashAPIConfig(s),
});
export default connect(mapState)(Conn);