diff options
| author | Zephyruso <[email protected]> | 2023-06-28 12:55:58 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-06-28 12:55:58 +0800 |
| commit | d497b15bedae37abb105d750ef1dfe16f6a7e05d (patch) | |
| tree | a3567f2bf7bf9f228fea42ff996154198d38b311 /src | |
| parent | f189d5b14f8c37c8d48a5c54fa52a0125f4d5306 (diff) | |
feat: close connection single or with filter (#64)
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ConnectionTable.scss | 23 | ||||
| -rw-r--r-- | src/components/ConnectionTable.tsx | 103 | ||||
| -rw-r--r-- | src/components/Connections.module.scss | 37 | ||||
| -rw-r--r-- | src/components/Connections.tsx | 230 | ||||
| -rw-r--r-- | src/components/ModalCloseAllConnections.tsx | 9 | ||||
| -rw-r--r-- | src/components/ModalManageConnectionColumns.module.scss | 18 | ||||
| -rw-r--r-- | src/components/ModalManageConnectionColumns.tsx | 102 | ||||
| -rw-r--r-- | src/components/ModalSourceIP.module.scss | 9 | ||||
| -rw-r--r-- | src/components/ModalSourceIP.tsx | 60 | ||||
| -rw-r--r-- | src/custom.d.ts | 3 | ||||
| -rw-r--r-- | src/i18n/en.ts | 4 | ||||
| -rw-r--r-- | src/i18n/zh-cn.ts | 4 | ||||
| -rw-r--r-- | src/i18n/zh-tw.ts | 4 |
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: '內部連線', }; |
