summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorZephyruso <[email protected]>2023-06-28 12:55:58 +0800
committerGitHub <[email protected]>2023-06-28 12:55:58 +0800
commitd497b15bedae37abb105d750ef1dfe16f6a7e05d (patch)
treea3567f2bf7bf9f228fea42ff996154198d38b311 /src
parentf189d5b14f8c37c8d48a5c54fa52a0125f4d5306 (diff)
feat: close connection single or with filter (#64)
Diffstat (limited to 'src')
-rw-r--r--src/components/ConnectionTable.scss23
-rw-r--r--src/components/ConnectionTable.tsx103
-rw-r--r--src/components/Connections.module.scss37
-rw-r--r--src/components/Connections.tsx230
-rw-r--r--src/components/ModalCloseAllConnections.tsx9
-rw-r--r--src/components/ModalManageConnectionColumns.module.scss18
-rw-r--r--src/components/ModalManageConnectionColumns.tsx102
-rw-r--r--src/components/ModalSourceIP.module.scss9
-rw-r--r--src/components/ModalSourceIP.tsx60
-rw-r--r--src/custom.d.ts3
-rw-r--r--src/i18n/en.ts4
-rw-r--r--src/i18n/zh-cn.ts4
-rw-r--r--src/i18n/zh-tw.ts4
13 files changed, 378 insertions, 228 deletions
diff --git a/src/components/ConnectionTable.scss b/src/components/ConnectionTable.scss
new file mode 100644
index 0000000..22324db
--- /dev/null
+++ b/src/components/ConnectionTable.scss
@@ -0,0 +1,23 @@
+.connections-table {
+ td.ctrl {
+ min-width: 4em;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ svg {
+ height: 16px;
+ }
+ }
+
+ td.type,
+ td.start,
+ td.downloadSpeedCurr,
+ td.uploadSpeedCurr,
+ td.download,
+ td.upload {
+ min-width: 7em;
+ text-align: center;
+ }
+}
diff --git a/src/components/ConnectionTable.tsx b/src/components/ConnectionTable.tsx
index 4c20598..d5c50a7 100644
--- a/src/components/ConnectionTable.tsx
+++ b/src/components/ConnectionTable.tsx
@@ -1,32 +1,28 @@
+import './ConnectionTable.scss';
+
import cx from 'clsx';
import { formatDistance, Locale } from 'date-fns';
import { enUS, zhCN, zhTW } from 'date-fns/locale';
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { ChevronDown } from 'react-feather';
+import { XCircle } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { useSortBy, useTable } from 'react-table';
+import { State } from '~/store/types';
+
+import * as connAPI from '../api/connections';
import prettyBytes from '../misc/pretty-bytes';
+import { getClashAPIConfig } from '../store/app';
import s from './ConnectionTable.module.scss';
-
-function renderCell(cell: { column: { id: string }; value: number }, locale: Locale) {
- switch (cell.column.id) {
- case 'start':
- return formatDistance(cell.value, 0, { locale: locale });
- case 'download':
- case 'upload':
- return prettyBytes(cell.value);
- case 'downloadSpeedCurr':
- case 'uploadSpeedCurr':
- return prettyBytes(cell.value) + '/s';
- default:
- return cell.value;
- }
-}
+import MOdalCloseConnection from './ModalCloseAllConnections';
+import { connect } from './StateProvider';
const sortById = { id: 'id', desc: true };
-function Table({ data, columns, hiddenColumns }) {
+function Table({ data, columns, hiddenColumns, apiConfig }) {
+ const [operationId, setOperationId] = useState('');
+ const [showModalDisconnect, setShowModalDisconnect] = useState(false);
const tableState = {
sortBy: [
// maintain a more stable order
@@ -61,9 +57,44 @@ function Table({ data, columns, hiddenColumns }) {
locale = enUS;
}
+ const disconnectOperation = () => {
+ connAPI.closeConnById(apiConfig, operationId);
+ setShowModalDisconnect(false);
+ };
+
+ const handlerDisconnect = (id) => {
+ setOperationId(id);
+ setShowModalDisconnect(true);
+ };
+
+ const renderCell = (
+ cell: { column: { id: string }; row: { original: { id: string } }; value: number },
+ locale: Locale
+ ) => {
+ switch (cell.column.id) {
+ case 'ctrl':
+ return (
+ <XCircle
+ style={{ cursor: 'pointer' }}
+ onClick={() => handlerDisconnect(cell.row.original.id)}
+ ></XCircle>
+ );
+ case 'start':
+ return formatDistance(cell.value, 0, { locale: locale });
+ case 'download':
+ case 'upload':
+ return prettyBytes(cell.value);
+ case 'downloadSpeedCurr':
+ case 'uploadSpeedCurr':
+ return prettyBytes(cell.value) + '/s';
+ default:
+ return cell.value;
+ }
+ };
+
return (
<div style={{ marginTop: '5px' }}>
- <table {...getTableProps()} className={s.table}>
+ <table {...getTableProps()} className={cx(s.table, 'connections-table')}>
<thead>
{headerGroups.map((headerGroup, trindex) => {
return (
@@ -71,11 +102,16 @@ function Table({ data, columns, hiddenColumns }) {
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} className={s.th}>
<span>{t(column.render('Header'))}</span>
- <span className={s.sortIconContainer}>
- {column.isSorted ? (
- <ChevronDown size={16} className={column.isSortedDesc ? '' : s.rotate180} />
- ) : null}
- </span>
+ {column.id !== 'ctrl' ? (
+ <span className={s.sortIconContainer}>
+ {column.isSorted ? (
+ <ChevronDown
+ size={16}
+ className={column.isSortedDesc ? '' : s.rotate180}
+ />
+ ) : null}
+ </span>
+ ) : null}
</th>
))}
</tr>
@@ -87,16 +123,11 @@ function Table({ data, columns, hiddenColumns }) {
prepareRow(row);
return (
<tr className={s.tr} key={i}>
- {row.cells.map((cell, j) => {
+ {row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
- className={cx(
- s.td,
- i % 2 === 0 ? s.odd : false,
- j == 0 || (j >= 5 && j < 10) ? s.center : true
- // j ==1 ? s.break : true
- )}
+ className={cx(s.td, i % 2 === 0 ? s.odd : false, cell.column.id)}
>
{renderCell(cell, locale)}
</td>
@@ -107,8 +138,18 @@ function Table({ data, columns, hiddenColumns }) {
})}
</tbody>
</table>
+ <MOdalCloseConnection
+ confirm={'disconnect'}
+ isOpen={showModalDisconnect}
+ onRequestClose={() => setShowModalDisconnect(false)}
+ primaryButtonOnTap={disconnectOperation}
+ ></MOdalCloseConnection>
</div>
);
}
-export default Table;
+const mapState = (s: State) => ({
+ apiConfig: getClashAPIConfig(s),
+});
+
+export default connect(mapState)(Table);
diff --git a/src/components/Connections.module.scss b/src/components/Connections.module.scss
index 8d290f4..4fcdb45 100644
--- a/src/components/Connections.module.scss
+++ b/src/components/Connections.module.scss
@@ -68,40 +68,3 @@
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
}
-
-.sourceipContainer {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- flex-direction: row;
- justify-content: space-evenly;
-}
-
-.sourceipTable {
- input {
- width: 120px;
- }
-}
-
-.iptableTipContainer {
- width: 300px;
-}
-
-.columnManagerRow {
- width: 200px;
- display: flex;
- margin: 5px 0;
- align-items: center;
-
- .columnManageLabel {
- flex: 1;
- margin-left: 10px;
- }
-
- .columnManageSwitch {
- transform: scale(0.7);
- height: 20px;
- display: flex;
- align-items: center;
- }
-}
diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx
index 2d3f365..9cf4b8b 100644
--- a/src/components/Connections.tsx
+++ b/src/components/Connections.tsx
@@ -1,29 +1,27 @@
import './Connections.css';
import React from 'react';
-import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
-import { List, Pause, Play, RefreshCcw, Settings, Tag, X as IconClose } from 'react-feather';
+import { Pause, Play, RefreshCcw, Settings, Tag, X as IconClose } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { ConnectionItem } from '~/api/connections';
-import BaseModal from '~/components/shared/BaseModal';
import Select from '~/components/shared/Select';
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';
import Input from './Input';
import ModalCloseAllConnections from './ModalCloseAllConnections';
+import ModalManageConnectionColumns from './ModalManageConnectionColumns';
+import ModalSourceIP from './ModalSourceIP';
import { Action, Fab, position as fabPosition } from './shared/Fab';
import { connect } from './StateProvider';
import SvgYacd from './SvgYacd';
-import Switch from './SwitchThemed';
const { useEffect, useState, useRef, useCallback } = React;
@@ -31,16 +29,6 @@ const sourceMapInit = localStorage.getItem('sourceMap')
? JSON.parse(localStorage.getItem('sourceMap'))
: [];
-const getItemStyle = (isDragging, draggableStyle) => {
- return {
- ...draggableStyle,
- ...(isDragging && {
- background: 'transparent',
- transform: draggableStyle.transform, // modal基于transform会造成偏移
- }),
- };
-};
-
const paddingBottom = 30;
function arrayToIdKv<T extends { id: string }>(items: T[]) {
@@ -108,17 +96,6 @@ function filterConns(conns: FormattedConn[], keyword: string, sourceIp: string)
return result;
}
-function getConnIpList(conns: FormattedConn[], sourceMap: { reg: string; name: string }[]) {
- return [
- ['', '全部'],
- ...Array.from(new Set(conns.map((x) => x.sourceIP)))
- .sort()
- .map((value) => {
- return [value, getNameFromSource(value, sourceMap)];
- }),
- ];
-}
-
function getNameFromSource(
source: string,
sourceMap: { reg: string; name: string }[],
@@ -223,9 +200,8 @@ function ConnQty({ qty }) {
}
const sortDescFirst = true;
-
-const hiddenColumnsOrigin = JSON.stringify(['id']);
-const columnsOrigin = JSON.stringify([
+const hiddenColumnsOrigin = ['id'];
+const columnsOrigin = [
{ accessor: 'id', show: false },
{ Header: 'c_type', accessor: 'type' },
{ Header: 'c_process', accessor: 'process' },
@@ -240,17 +216,35 @@ const columnsOrigin = JSON.stringify([
{ Header: 'c_source', accessor: 'source' },
{ Header: 'c_destination_ip', accessor: 'destinationIP' },
{ Header: 'c_sni', accessor: 'sniffHost' },
-]);
+ { Header: 'c_ctrl', accessor: 'ctrl' },
+];
const savedHiddenColumns = localStorage.getItem('hiddenColumns');
const savedColumns = localStorage.getItem('columns');
const hiddenColumnsInit = savedHiddenColumns
? JSON.parse(savedHiddenColumns)
- : JSON.parse(hiddenColumnsOrigin);
-const columnsInit = savedColumns ? JSON.parse(savedColumns) : JSON.parse(columnsOrigin);
+ : [...hiddenColumnsOrigin];
+
+const columnOrder = savedColumns ? JSON.parse(savedColumns) : null;
+const columnsInit = columnOrder
+ ? [...columnsOrigin].sort((pre, next) => {
+ const preIdx = columnOrder.findIndex((column) => column.accessor === pre.accessor);
+ const nextIdx = columnOrder.findIndex((column) => column.accessor === next.accessor);
+
+ if (preIdx === -1) {
+ return 1;
+ }
+
+ if (nextIdx === -1) {
+ return -1;
+ }
+ return preIdx - nextIdx;
+ })
+ : [...columnsOrigin];
function Conn({ apiConfig }) {
+ const { t } = useTranslation();
const [showModalColumn, setModalColumn] = useState(false);
const [hiddenColumns, setHiddenColumns] = useState(hiddenColumnsInit);
const [columns, setColumns] = useState(columnsInit);
@@ -259,39 +253,13 @@ function Conn({ apiConfig }) {
setModalColumn(false);
};
- const onShowChange = (column, val) => {
- if (!val) {
- hiddenColumns.push(column.accessor);
- } else {
- const idx = hiddenColumns.indexOf(column.accessor);
-
- hiddenColumns.splice(idx, 1);
- }
- setHiddenColumns(Array.from(hiddenColumns));
- localStorage.setItem('hiddenColumns', JSON.stringify(hiddenColumns));
- };
-
const resetColumns = () => {
- hiddenColumns.splice(0, hiddenColumns.length);
- hiddenColumns.push('id');
- setHiddenColumns(hiddenColumns);
- setColumns(JSON.parse(columnsOrigin));
+ setHiddenColumns([...hiddenColumnsOrigin]);
+ setColumns([...columnsOrigin]);
localStorage.removeItem('hiddenColumns');
localStorage.removeItem('columns');
};
- const onDragEnd = (result) => {
- if (!result.destination) {
- return;
- }
-
- const items = Array.from(columns);
- const [removed] = items.splice(result.source.index, 1);
- items.splice(result.destination.index, 0, removed);
- setColumns(items);
- localStorage.setItem('columns', JSON.stringify(items));
- };
-
const [sourceMapModal, setSourceMapModal] = useState(false);
const [sourceMap, setSourceMap] = useState(sourceMapInit);
const [refContainer, containerHeight] = useRemainingViewPortHeight();
@@ -305,9 +273,29 @@ function Conn({ apiConfig }) {
const filteredConns = filterConns(conns, filterKeyword, filterSourceIpStr);
const filteredClosedConns = filterConns(closedConns, filterKeyword, filterSourceIpStr);
- const connIpSet = getConnIpList(conns, sourceMap);
+ const getConnIpList = (conns: FormattedConn[]) => {
+ return [
+ ['', t('All')],
+ ...Array.from(new Set(conns.map((x) => x.sourceIP)))
+ .sort()
+ .map((value) => {
+ return [value, getNameFromSource(value, sourceMap).trim() || t('internel')];
+ }),
+ ];
+ };
+ const connIpSet = getConnIpList(conns);
// const ClosedConnIpSet = getConnIpList(closedConns);
+ const [isCloseFilterModalOpen, setIsCloseFilterModalOpen] = useState(false);
+ const openCloseFilterModal = useCallback(() => setIsCloseFilterModalOpen(true), []);
+ const closeCloseFilterModal = useCallback(() => setIsCloseFilterModalOpen(false), []);
+
+ const closeFilterConnections = useCallback(async () => {
+ for (const connection of filteredConns) {
+ await connAPI.closeConnById(apiConfig, connection.id);
+ }
+ closeCloseFilterModal();
+ }, [apiConfig, filteredConns, closeCloseFilterModal]);
const [isCloseAllModalOpen, setIsCloseAllModalOpen] = useState(false);
const openCloseAllModal = useCallback(() => setIsCloseAllModalOpen(true), []);
const closeCloseAllModal = useCallback(() => setIsCloseAllModalOpen(false), []);
@@ -345,7 +333,7 @@ function Conn({ apiConfig }) {
prevConnsRef.current = x;
}
},
- [setConns, isRefreshPaused]
+ [setConns, sourceMap, isRefreshPaused]
);
const [reConnectCount, setReConnectCount] = useState(0);
@@ -357,7 +345,6 @@ function Conn({ apiConfig }) {
});
}, [apiConfig, read, reConnectCount, setReConnectCount]);
- const { t } = useTranslation();
const openModalSource = () => {
if (sourceMap.length === 0) {
sourceMap.push({
@@ -372,103 +359,9 @@ function Conn({ apiConfig }) {
localStorage.setItem('sourceMap', JSON.stringify(sourceMap));
setSourceMapModal(false);
};
- const setSource = (key, index, val) => {
- sourceMap[index][key] = val;
- setSourceMap(Array.from(sourceMap));
- };
return (
<div>
- <BaseModal isOpen={showModalColumn} onRequestClose={closeModalColumn}>
- <div>
- <DragDropContext onDragEnd={onDragEnd}>
- <Droppable droppableId="droppable-modal">
- {(provided) => (
- <div {...provided.droppableProps} ref={provided.innerRef}>
- {columns
- .filter((i) => i.accessor !== 'id')
- .map((column) => {
- const show = !hiddenColumns.includes(column.accessor);
-
- return (
- <Draggable
- key={column.accessor}
- draggableId={column.accessor}
- index={columns.findIndex((a) => a.accessor === column.accessor)}
- >
- {(provided, snapshot) => (
- <div
- ref={provided.innerRef}
- {...provided.draggableProps}
- {...provided.dragHandleProps}
- className={s.columnManagerRow}
- style={getItemStyle(
- snapshot.isDragging,
- provided.draggableProps.style
- )}
- >
- <List />
- <span className={s.columnManageLabel}>{t(column.Header)}</span>
- <div className={s.columnManageSwitch}>
- <Switch
- size="mini"
- checked={show}
- onChange={(val) => onShowChange(column, val)}
- />
- </div>
- </div>
- )}
- </Draggable>
- );
- })}
- {provided.placeholder}
- </div>
- )}
- </Droppable>
- </DragDropContext>
- </div>
- </BaseModal>
- <BaseModal isOpen={sourceMapModal} onRequestClose={closeModalSource}>
- <table className={s.sourceipTable}>
- <thead>
- <tr>
- <th>{t('c_source')}</th>
- <th>{t('device_name')}</th>
- </tr>
- </thead>
- <tbody>
- {sourceMap.map((source, index) => (
- <tr key={`${index}`}>
- <td>
- <Input
- type="text"
- name="reg"
- autoComplete="off"
- value={source.reg}
- onChange={(e) => setSource('reg', index, e.target.value)}
- />
- </td>
- <td>
- <Input
- type="text"
- name="name"
- autoComplete="off"
- value={source.name}
- onChange={(e) => setSource('name', index, e.target.value)}
- />
- </td>
- <td>
- <Button onClick={() => sourceMap.splice(index, 1)}>{t('delete')}</Button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- <div>
- <div className={s.iptableTipContainer}>{t('sourceip_tip')}</div>
- <Button onClick={() => sourceMap.push({ reg: '', name: '' })}>{t('add_tag')}</Button>
- </div>
- </BaseModal>
<div className={s.header}>
<ContentHeader title={t('Connections')} />
<div className={s.inputWrapper}>
@@ -537,6 +430,9 @@ function Conn({ apiConfig }) {
<Action text={t('close_all_connections')} onClick={openCloseAllModal}>
<IconClose size={10} />
</Action>
+ <Action text={t('close_filter_connections')} onClick={openCloseFilterModal}>
+ <IconClose size={10} />
+ </Action>
<Action text={t('manage_column')} onClick={() => setModalColumn(true)}>
<Settings size={10} />
</Action>
@@ -571,6 +467,26 @@ function Conn({ apiConfig }) {
primaryButtonOnTap={closeAllConnections}
onRequestClose={closeCloseAllModal}
/>
+ <ModalCloseAllConnections
+ confirm={'close_filter_connections'}
+ isOpen={isCloseFilterModalOpen}
+ primaryButtonOnTap={closeFilterConnections}
+ onRequestClose={closeCloseFilterModal}
+ />
+ <ModalManageConnectionColumns
+ isOpen={showModalColumn}
+ onRequestClose={closeModalColumn}
+ columns={columns}
+ hiddenColumns={hiddenColumns}
+ setColumns={setColumns}
+ setHiddenColumns={setHiddenColumns}
+ />
+ <ModalSourceIP
+ isOpen={sourceMapModal}
+ onRequestClose={closeModalSource}
+ sourceMap={sourceMap}
+ setSourceMap={setSourceMap}
+ />
</Tabs>
</div>
);
diff --git a/src/components/ModalCloseAllConnections.tsx b/src/components/ModalCloseAllConnections.tsx
index 505c0cf..77bcb59 100644
--- a/src/components/ModalCloseAllConnections.tsx
+++ b/src/components/ModalCloseAllConnections.tsx
@@ -9,7 +9,12 @@ import s from './ModalCloseAllConnections.module.scss';
const { useRef, useCallback, useMemo } = React;
-export default function Comp({ isOpen, onRequestClose, primaryButtonOnTap }) {
+export default function Comp({
+ confirm = 'close_all_confirm',
+ isOpen,
+ onRequestClose,
+ primaryButtonOnTap,
+}) {
const { t } = useTranslation();
const primaryButtonRef = useRef(null);
const onAfterOpen = useCallback(() => {
@@ -31,7 +36,7 @@ export default function Comp({ isOpen, onRequestClose, primaryButtonOnTap }) {
className={className}
overlayClassName={cx(modalStyle.overlay, s.overlay)}
>
- <p>{t('close_all_confirm')}</p>
+ <p>{t(confirm)}</p>
<div className={s.btngrp}>
<Button onClick={primaryButtonOnTap} ref={primaryButtonRef}>
{t('close_all_confirm_yes')}
diff --git a/src/components/ModalManageConnectionColumns.module.scss b/src/components/ModalManageConnectionColumns.module.scss
new file mode 100644
index 0000000..9247acd
--- /dev/null
+++ b/src/components/ModalManageConnectionColumns.module.scss
@@ -0,0 +1,18 @@
+.columnManagerRow {
+ width: 200px;
+ display: flex;
+ margin: 5px 0;
+ align-items: center;
+
+ .columnManageLabel {
+ flex: 1;
+ margin-left: 10px;
+ }
+
+ .columnManageSwitch {
+ transform: scale(0.7);
+ height: 20px;
+ display: flex;
+ align-items: center;
+ }
+}
diff --git a/src/components/ModalManageConnectionColumns.tsx b/src/components/ModalManageConnectionColumns.tsx
new file mode 100644
index 0000000..e7a25fb
--- /dev/null
+++ b/src/components/ModalManageConnectionColumns.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
+import { Menu } from 'react-feather';
+import { useTranslation } from 'react-i18next';
+
+import BaseModal from '~/components/shared/BaseModal';
+
+import s from './ModalManageConnectionColumns.module.scss';
+import Switch from './SwitchThemed';
+
+const getItemStyle = (isDragging, draggableStyle) => {
+ return {
+ ...draggableStyle,
+ ...(isDragging && {
+ background: 'transparent',
+ }),
+ };
+};
+
+export default function ModalManageConnectionColumns({
+ isOpen,
+ onRequestClose,
+ columns,
+ hiddenColumns,
+ setColumns,
+ setHiddenColumns,
+}) {
+ const { t } = useTranslation();
+
+ const onDragEnd = (result) => {
+ if (!result.destination) {
+ return;
+ }
+
+ const items = Array.from(columns);
+ const [removed] = items.splice(result.source.index, 1);
+ items.splice(result.destination.index, 0, removed);
+ setColumns(items);
+ localStorage.setItem('columns', JSON.stringify(items));
+ };
+
+ const onShowChange = (column, val) => {
+ if (!val) {
+ hiddenColumns.push(column.accessor);
+ } else {
+ const idx = hiddenColumns.indexOf(column.accessor);
+
+ hiddenColumns.splice(idx, 1);
+ }
+ setHiddenColumns(Array.from(hiddenColumns));
+ localStorage.setItem('hiddenColumns', JSON.stringify(hiddenColumns));
+ };
+
+ return (
+ <BaseModal isOpen={isOpen} onRequestClose={onRequestClose}>
+ <div>
+ <DragDropContext onDragEnd={onDragEnd}>
+ <Droppable droppableId="droppable-modal">
+ {(provided) => (
+ <div {...provided.droppableProps} ref={provided.innerRef}>
+ {columns
+ .filter((i) => i.accessor !== 'id')
+ .map((column) => {
+ const show = !hiddenColumns.includes(column.accessor);
+
+ return (
+ <Draggable
+ key={column.accessor}
+ draggableId={column.accessor}
+ index={columns.findIndex((a) => a.accessor === column.accessor)}
+ >
+ {(provided, snapshot) => (
+ <div
+ ref={provided.innerRef}
+ {...provided.draggableProps}
+ {...provided.dragHandleProps}
+ className={s.columnManagerRow}
+ style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
+ >
+ <Menu />
+ <span className={s.columnManageLabel}>{t(column.Header)}</span>
+ <div className={s.columnManageSwitch}>
+ <Switch
+ size="mini"
+ checked={show}
+ onChange={(val) => onShowChange(column, val)}
+ />
+ </div>
+ </div>
+ )}
+ </Draggable>
+ );
+ })}
+ {provided.placeholder}
+ </div>
+ )}
+ </Droppable>
+ </DragDropContext>
+ </div>
+ </BaseModal>
+ );
+}
diff --git a/src/components/ModalSourceIP.module.scss b/src/components/ModalSourceIP.module.scss
new file mode 100644
index 0000000..7a60dcf
--- /dev/null
+++ b/src/components/ModalSourceIP.module.scss
@@ -0,0 +1,9 @@
+.sourceipTable {
+ input {
+ width: 120px;
+ }
+}
+
+.iptableTipContainer {
+ width: 300px;
+}
diff --git a/src/components/ModalSourceIP.tsx b/src/components/ModalSourceIP.tsx
new file mode 100644
index 0000000..a406601
--- /dev/null
+++ b/src/components/ModalSourceIP.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import BaseModal from '~/components/shared/BaseModal';
+
+import Button from './Button';
+import Input from './Input';
+import s from './ModalSourceIP.module.scss';
+
+export default function ModalSourceIP({ isOpen, onRequestClose, sourceMap, setSourceMap }) {
+ const { t } = useTranslation();
+ const setSource = (key, index, val) => {
+ sourceMap[index][key] = val;
+ setSourceMap(Array.from(sourceMap));
+ };
+
+ return (
+ <BaseModal isOpen={isOpen} onRequestClose={onRequestClose}>
+ <table className={s.sourceipTable}>
+ <thead>
+ <tr>
+ <th>{t('c_source')}</th>
+ <th>{t('device_name')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {sourceMap.map((source, index) => (
+ <tr key={`${index}`}>
+ <td>
+ <Input
+ type="text"
+ name="reg"
+ autoComplete="off"
+ value={source.reg}
+ onChange={(e) => setSource('reg', index, e.target.value)}
+ />
+ </td>
+ <td>
+ <Input
+ type="text"
+ name="name"
+ autoComplete="off"
+ value={source.name}
+ onChange={(e) => setSource('name', index, e.target.value)}
+ />
+ </td>
+ <td>
+ <Button onClick={() => sourceMap.splice(index, 1)}>{t('delete')}</Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ <div>
+ <div className={s.iptableTipContainer}>{t('sourceip_tip')}</div>
+ <Button onClick={() => sourceMap.push({ reg: '', name: '' })}>{t('add_tag')}</Button>
+ </div>
+ </BaseModal>
+ );
+}
diff --git a/src/custom.d.ts b/src/custom.d.ts
index 2bf9b7d..6eb527a 100644
--- a/src/custom.d.ts
+++ b/src/custom.d.ts
@@ -35,6 +35,7 @@ declare module 'react-table' {
getHeaderProps(p: SortByToggleProps): { role?: string };
getSortByToggleProps(): SortByToggleProps;
render(x: string): string;
+ id: string;
isSorted: boolean;
isSortedDesc: boolean;
}
@@ -46,7 +47,7 @@ declare module 'react-table' {
interface Cell {
getCellProps(): { role?: string };
-
+ row: { original: { id: string } };
column: { id: string };
value: number;
}
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 7963aae..f35840c 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -15,6 +15,7 @@ export const data = {
'Pause Refresh': 'Pause Refresh',
'Resume Refresh': 'Resume Refresh',
close_all_connections: 'Close All Connections',
+ close_filter_connections: 'Close all connections after filtering',
Search: 'Search',
Up: 'Up',
Down: 'Down',
@@ -63,6 +64,7 @@ export const data = {
c_source: 'Source',
c_destination_ip: 'Destination IP',
c_type: 'Type',
+ c_ctrl: 'Operation',
close_all_confirm: 'Are you sure you want to close all connections?',
close_all_confirm_yes: "I'm sure",
close_all_confirm_no: 'No',
@@ -73,4 +75,6 @@ export const data = {
add_tag: 'Add tag',
client_tag: 'Client tags',
sourceip_tip: "Prefix with / for regular expressions, otherwise it's a complete match",
+ disconnect: 'Close Connection',
+ internel: 'Internal Connection',
};
diff --git a/src/i18n/zh-cn.ts b/src/i18n/zh-cn.ts
index 3fa27c4..40faf24 100644
--- a/src/i18n/zh-cn.ts
+++ b/src/i18n/zh-cn.ts
@@ -16,6 +16,7 @@ export const data = {
'Pause Refresh': '暂停刷新',
'Resume Refresh': '继续刷新',
close_all_connections: '关闭所有连接',
+ close_filter_connections: '关闭所有过滤后的连接',
Search: '查找',
Up: '上传',
Down: '下载',
@@ -62,6 +63,7 @@ export const data = {
c_source: '来源',
c_destination_ip: '目标IP',
c_type: '类型',
+ c_ctrl: '操作',
restart_core: '重启 clash 核心',
upgrade_core: '更新 Alpha 核心',
close_all_confirm: '确定关闭所有连接?',
@@ -74,4 +76,6 @@ export const data = {
add_tag: '添加标签',
client_tag: '客户端标签',
sourceip_tip: '/开头为正则,否则为全匹配',
+ disconnect: '断开连接',
+ internel: '内部链接',
};
diff --git a/src/i18n/zh-tw.ts b/src/i18n/zh-tw.ts
index 7a1dca0..9d4cf5f 100644
--- a/src/i18n/zh-tw.ts
+++ b/src/i18n/zh-tw.ts
@@ -16,6 +16,7 @@ export const data = {
'Pause Refresh': '暫停重整',
'Resume Refresh': '繼續重整',
close_all_connections: '斷開所有連線',
+ close_filter_connections: '斷開所有過濾後的連線',
Search: '搜尋',
Up: '上傳',
Down: '下載',
@@ -62,6 +63,7 @@ export const data = {
c_source: '來源',
c_destination_ip: '目標 IP',
c_type: '類型',
+ c_ctrl: '操作',
restart_core: '重新啟動 clash 核心',
upgrade_core: '更新 Alpha 核心',
close_all_confirm: '確定關閉所有連接?',
@@ -74,4 +76,6 @@ export const data = {
add_tag: '新增標籤',
client_tag: '客戶端標籤',
sourceip_tip: '/開頭為正規表達式,否則為全面配對',
+ disconnect: '斷開連線',
+ internel: '內部連線',
};