diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index f121f2720..ed1d396a2 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -97,6 +97,7 @@ const getConnectionCellValue = (field: ColumnField, each: IConnectionsItem) => { interface Props { connections: IConnectionsItem[]; + paused?: boolean; onShowDetail: (data: IConnectionsItem) => void; columnManagerOpen: boolean; onOpenColumnManager: () => void; @@ -106,6 +107,7 @@ interface Props { export const ConnectionTable = (props: Props) => { const { connections, + paused = false, onShowDetail, columnManagerOpen, onOpenColumnManager, @@ -337,14 +339,15 @@ export const ConnectionTable = (props: Props) => { }, [baseColumns, relativeNow]); useEffect(() => { - if (typeof window === "undefined") return undefined; + if (paused || typeof window === "undefined") return undefined; + setRelativeNow(Date.now()); const timer = window.setInterval(() => { setRelativeNow(Date.now()); }, 5000); return () => window.clearInterval(timer); - }, []); + }, [paused]); const handleColumnSizingChange = useCallback( (updater: Updater) => { diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index 15b41d25f..acecb1581 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -1,3 +1,4 @@ +import { useEffect, useRef } from "react"; import { mutate } from "swr"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; @@ -74,7 +75,21 @@ const mergeConnectionSnapshot = ( }; }; -export const useConnectionData = () => { +export const useConnectionData = ( + options: { + paused?: boolean; + } = {}, +) => { + const { paused = false } = options; + const pausedRef = useRef(paused); + const resetSpeedRef = useRef(false); + useEffect(() => { + if (pausedRef.current && !paused) { + resetSpeedRef.current = true; + } + pausedRef.current = paused; + }, [paused]); + const { response, refresh, subscriptionCacheKey } = useMihomoWsSubscription({ storageKey: "mihomo_connection_date", @@ -89,11 +104,20 @@ export const useConnectionData = () => { return; } + if (pausedRef.current) return; + try { const parsed = JSON.parse(data) as IConnections; - next(null, (old = initConnData) => - mergeConnectionSnapshot(parsed, old), - ); + next(null, (old = initConnData) => { + if (resetSpeedRef.current) { + resetSpeedRef.current = false; + return mergeConnectionSnapshot(parsed, { + ...old, + activeConnections: [], + }); + } + return mergeConnectionSnapshot(parsed, old); + }); } catch (error) { next(error); } diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index c5bcd0057..5003aa262 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "المضيف", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "ميناء الوجهة", + "network": "Protocol", "dlSpeed": "سرعة التنزيل", "ulSpeed": "سرعة الرفع", "chains": "السلاسل", @@ -13,18 +18,23 @@ "time": "الوقت", "source": "المصدر", "destination": "عنوان IP الوجهة", - "destinationPort": "ميناء الوجهة", "type": "النوع" }, "order": { "default": "Default", "uploadSpeed": "سرعة الرفع", - "downloadSpeed": "سرعة التنزيل" + "downloadSpeed": "سرعة التنزيل", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "إغلاق الاتصال" + "closeConnection": "إغلاق الاتصال", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "الأعمدة", diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index 57f8bc1bb..328555e60 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Host", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Zielport", + "network": "Protocol", "dlSpeed": "Download-Geschwindigkeit", "ulSpeed": "Upload-Geschwindigkeit", "chains": "Ketten", @@ -13,18 +18,23 @@ "time": "Verbindungszeit", "source": "Quelladresse", "destination": "Zieladresse", - "destinationPort": "Zielport", "type": "Typ" }, "order": { "default": "Default", "uploadSpeed": "Upload-Geschwindigkeit", - "downloadSpeed": "Download-Geschwindigkeit" + "downloadSpeed": "Download-Geschwindigkeit", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Verbindung schließen" + "closeConnection": "Verbindung schließen", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Spalten", diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 82b3cebac..3d5a943e8 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Host", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Destination Port", + "network": "Protocol", "dlSpeed": "DL Speed", "ulSpeed": "UL Speed", "chains": "Chains", @@ -13,18 +18,23 @@ "time": "Time", "source": "Source", "destination": "Destination", - "destinationPort": "Destination Port", "type": "Type" }, "order": { "default": "Default", "uploadSpeed": "Upload Speed", - "downloadSpeed": "Download Speed" + "downloadSpeed": "Download Speed", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Close Connection" + "closeConnection": "Close Connection", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Columns", diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index 1b49bc17c..70694f541 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Host", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Puerto de destino", + "network": "Protocol", "dlSpeed": "Velocidad de descarga", "ulSpeed": "Velocidad de subida", "chains": "Cadenas", @@ -13,18 +18,23 @@ "time": "Tiempo de conexión", "source": "Dirección de origen", "destination": "Dirección de destino", - "destinationPort": "Puerto de destino", "type": "Tipo" }, "order": { "default": "Default", "uploadSpeed": "Velocidad de subida", - "downloadSpeed": "Velocidad de descarga" + "downloadSpeed": "Velocidad de descarga", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Cerrar conexión" + "closeConnection": "Cerrar conexión", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Columnas", diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index a8c8687e7..0da7fa7b7 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "میزبان", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "بندر هدف", + "network": "Protocol", "dlSpeed": "سرعت دانلود", "ulSpeed": "سرعت بارگذاری", "chains": "زنجیره‌ها", @@ -13,18 +18,23 @@ "time": "زمان", "source": "منبع", "destination": "آدرس IP مقصد", - "destinationPort": "بندر هدف", "type": "نوع" }, "order": { "default": "Default", "uploadSpeed": "سرعت بارگذاری", - "downloadSpeed": "سرعت دانلود" + "downloadSpeed": "سرعت دانلود", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "بستن اتصال" + "closeConnection": "بستن اتصال", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "ستون‌ها", diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 29748ccf9..db82eb8a1 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Host", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Port Tujuan", + "network": "Protocol", "dlSpeed": "Kecepatan Unduh", "ulSpeed": "Kecepatan Unggah", "chains": "Rantai", @@ -13,18 +18,23 @@ "time": "Waktu", "source": "Sumber", "destination": "IP Tujuan", - "destinationPort": "Port Tujuan", "type": "Jenis" }, "order": { "default": "Default", "uploadSpeed": "Kecepatan Unggah", - "downloadSpeed": "Kecepatan Unduh" + "downloadSpeed": "Kecepatan Unduh", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Tutup Koneksi" + "closeConnection": "Tutup Koneksi", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Kolom", diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index 25fb84e8a..bf96d0698 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "ホスト", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "宛先ポート", + "network": "Protocol", "dlSpeed": "ダウンロード速度", "ulSpeed": "アップロード速度", "chains": "チェーン", @@ -13,18 +18,23 @@ "time": "接続時間", "source": "送信元アドレス", "destination": "宛先アドレス", - "destinationPort": "宛先ポート", "type": "タイプ" }, "order": { "default": "Default", "uploadSpeed": "アップロード速度", - "downloadSpeed": "ダウンロード速度" + "downloadSpeed": "ダウンロード速度", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "接続を閉じる" + "closeConnection": "接続を閉じる", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "列", diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index 88173f6ac..acde46e07 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "호스트", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "목적지 포트", + "network": "Protocol", "dlSpeed": "다운로드 속도", "ulSpeed": "업로드 속도", "chains": "체인", @@ -13,18 +18,23 @@ "time": "시간", "source": "소스", "destination": "목적지", - "destinationPort": "목적지 포트", "type": "유형" }, "order": { "default": "기본", "uploadSpeed": "업로드 속도", - "downloadSpeed": "다운로드 속도" + "downloadSpeed": "다운로드 속도", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "연결 닫기" + "closeConnection": "연결 닫기", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "열", diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index 6aadd293d..47bea298d 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Хост", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Целевой порт", + "network": "Protocol", "dlSpeed": "Скорость скачивания", "ulSpeed": "Скорость загрузки", "chains": "Цепочки", @@ -13,18 +18,23 @@ "time": "Время подключения", "source": "Исходный адрес", "destination": "IP-адрес назначения", - "destinationPort": "Целевой порт", "type": "Тип" }, "order": { "default": "Default", "uploadSpeed": "Скорость загрузки", - "downloadSpeed": "Скорость скачивания" + "downloadSpeed": "Скорость скачивания", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Закрыть соединение" + "closeConnection": "Закрыть соединение", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Столбцы", diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index 9bcd349de..cc52ba59a 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Ana Bilgisayar", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Hedef Port", + "network": "Protocol", "dlSpeed": "İndirme Hızı", "ulSpeed": "Yükleme Hızı", "chains": "Zincirler", @@ -13,18 +18,23 @@ "time": "Zaman", "source": "Kaynak", "destination": "Hedef", - "destinationPort": "Hedef Port", "type": "Tip" }, "order": { "default": "Default", "uploadSpeed": "Yükleme Hızı", - "downloadSpeed": "İndirme Hızı" + "downloadSpeed": "İndirme Hızı", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Bağlantıyı Kapat" + "closeConnection": "Bağlantıyı Kapat", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Sütunlar", diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index 48a28d4b8..3b6701d54 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "Хост", + "sourceIP": "Source IP", + "destinationIP": "Destination IP", + "sourcePort": "Source Port", + "destinationPort": "Барасы порты", + "network": "Protocol", "dlSpeed": "Йөкләү тизл.", "ulSpeed": "Чыгару тизл.", "chains": "Чылбырлар", @@ -13,18 +18,23 @@ "time": "Тоташу вакыты", "source": "Чыганак адресы", "destination": "Максат IP-адресы", - "destinationPort": "Барасы порты", "type": "Төр" }, "order": { "default": "Default", "uploadSpeed": "Йөкләү (чыгару) тизлеге", - "downloadSpeed": "Йөкләү тизлеге" + "downloadSpeed": "Йөкләү тизлеге", + "uploadTotal": "Upload Total", + "downloadTotal": "Download Total", + "duration": "Duration" }, "actions": { "active": "Active", "closed": "Closed", - "closeConnection": "Тоташуны ябу" + "closeConnection": "Тоташуны ябу", + "filter": "Filter", + "clearFilters": "Clear Filters", + "closeFiltered": "Close Filtered" }, "columnManager": { "title": "Баганалар", diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 33a447813..3489ca117 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "主机", + "sourceIP": "源 IP", + "destinationIP": "目标 IP", + "sourcePort": "源端口", + "destinationPort": "目标端口", + "network": "协议", "dlSpeed": "下载速度", "ulSpeed": "上传速度", "chains": "链路", @@ -13,18 +18,23 @@ "time": "连接时间", "source": "源地址", "destination": "目标地址", - "destinationPort": "目标端口", "type": "类型" }, "order": { "default": "默认", "uploadSpeed": "上传速度", - "downloadSpeed": "下载速度" + "downloadSpeed": "下载速度", + "uploadTotal": "上传总量", + "downloadTotal": "下载总量", + "duration": "连接时长" }, "actions": { "active": "活跃", "closed": "已关闭", - "closeConnection": "关闭连接" + "closeConnection": "关闭连接", + "filter": "筛选", + "clearFilters": "清除筛选", + "closeFiltered": "关闭筛选连接" }, "columnManager": { "title": "列设置", diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 520f3a855..97f8df6fa 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -5,6 +5,11 @@ "components": { "fields": { "host": "主機", + "sourceIP": "來源 IP", + "destinationIP": "目標 IP", + "sourcePort": "來源連接埠", + "destinationPort": "目標連接埠", + "network": "協定", "dlSpeed": "下載速度", "ulSpeed": "上傳速度", "chains": "鏈路", @@ -13,18 +18,23 @@ "time": "連線時間", "source": "來源位址", "destination": "目標位址", - "destinationPort": "目標連接埠", "type": "類型" }, "order": { "default": "預設", "uploadSpeed": "上傳速度", - "downloadSpeed": "下載速度" + "downloadSpeed": "下載速度", + "uploadTotal": "上傳總量", + "downloadTotal": "下載總量", + "duration": "連線時長" }, "actions": { "active": "活躍", "closed": "已關閉", - "closeConnection": "關閉連線" + "closeConnection": "關閉連線", + "filter": "篩選", + "clearFilters": "清除篩選", + "closeFiltered": "關閉篩選連線" }, "columnManager": { "title": "欄位設定", diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 03cdc3614..fa1b8c2a4 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,28 +1,38 @@ import { DeleteForeverRounded, + FilterListRounded, + PauseCircleOutlineRounded, + PlayCircleOutlineRounded, TableChartRounded, TableRowsRounded, } from "@mui/icons-material"; import { + Autocomplete, + Badge, Box, Button, ButtonGroup, Fab, IconButton, MenuItem, + Popover, + Stack, + TextField, + Tooltip, Zoom, } from "@mui/material"; import { useLockFn } from "ahooks"; import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso } from "react-virtuoso"; -import { closeAllConnections } from "tauri-plugin-mihomo-api"; +import { closeAllConnections, closeConnection } from "tauri-plugin-mihomo-api"; import { BaseEmpty, BasePage, BaseSearchBox, BaseStyledSelect, + type SearchState, } from "@/components/base"; import { ConnectionDetail, @@ -59,6 +69,27 @@ const ORDER_OPTIONS = [ fn: (list: IConnectionsItem[]) => list.sort((a, b) => b.curDownload! - a.curDownload!), }, + { + id: "uploadTotal", + labelKey: "connections.components.order.uploadTotal", + fn: (list: IConnectionsItem[]) => list.sort((a, b) => b.upload - a.upload), + }, + { + id: "downloadTotal", + labelKey: "connections.components.order.downloadTotal", + fn: (list: IConnectionsItem[]) => + list.sort((a, b) => b.download - a.download), + }, + { + id: "duration", + labelKey: "connections.components.order.duration", + fn: (list: IConnectionsItem[]) => + list.sort( + (a, b) => + new Date(a.start || "0").getTime()! - + new Date(b.start || "0").getTime()!, + ), + }, ] as const; type OrderKey = (typeof ORDER_OPTIONS)[number]["id"]; @@ -71,20 +102,53 @@ const orderFunctionMap = ORDER_OPTIONS.reduce>( {} as Record, ); +type ConnectionFilters = { + host: string[]; + sourceIP: string[]; + destinationIP: string[]; + network: string[]; + sourcePort: string[]; + destinationPort: string[]; +}; + +const EMPTY_FILTERS: ConnectionFilters = { + host: [], + sourceIP: [], + destinationIP: [], + network: [], + sourcePort: [], + destinationPort: [], +}; + +const getUniqueValues = (values: Array) => { + const set = new Set(); + values.forEach((value) => { + const nextValue = value?.trim(); + if (nextValue) set.add(nextValue); + }); + return [...set]; +}; + const ConnectionsPage = () => { const { t } = useTranslation(); const [match, setMatch] = useState<(input: string) => boolean>( () => () => true, ); + const [searchState, setSearchState] = useState(); const [curOrderOpt, setCurOrderOpt] = useState("default"); const [connectionsType, setConnectionsType] = useState<"active" | "closed">( "active", ); + const [filters, setFilters] = useState(EMPTY_FILTERS); + const [filterAnchorEl, setFilterAnchorEl] = useState( + null, + ); + const [paused, setPaused] = useState(false); const { response: { data: connections }, clearClosedConnections, - } = useConnectionData(); + } = useConnectionData({ paused }); const [setting, setSetting] = useConnectionSetting(); @@ -92,34 +156,196 @@ const ConnectionsPage = () => { const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false); - const [filterConn] = useMemo(() => { - const orderFunc = orderFunctionMap[curOrderOpt]; - const conns = + const baseConnections = useMemo( + () => (connectionsType === "active" ? connections?.activeConnections - : connections?.closedConnections) ?? []; - let matchConns = conns.filter((conn) => { - const { host, destinationIP, process } = conn.metadata; - return ( - match(host || "") || match(destinationIP || "") || match(process || "") - ); + : connections?.closedConnections) ?? [], + [connections, connectionsType], + ); + + const filterOptions = useMemo(() => { + const hosts = getUniqueValues( + baseConnections.map( + (conn) => conn.metadata.host || conn.metadata.remoteDestination, + ), + ); + const sourceIPs = getUniqueValues( + baseConnections.map((conn) => conn.metadata.sourceIP), + ); + const destinationIPs = getUniqueValues( + baseConnections.map((conn) => conn.metadata.destinationIP), + ); + const networks = getUniqueValues( + baseConnections.map((conn) => conn.metadata.network), + ); + const sourcePorts = getUniqueValues( + baseConnections.map((conn) => conn.metadata.sourcePort), + ); + const destinationPorts = getUniqueValues( + baseConnections.map((conn) => conn.metadata.destinationPort), + ); + + return { + host: hosts.sort((a, b) => a.localeCompare(b)), + sourceIP: sourceIPs.sort((a, b) => a.localeCompare(b)), + destinationIP: destinationIPs.sort((a, b) => a.localeCompare(b)), + network: networks.sort((a, b) => a.localeCompare(b)), + sourcePort: sourcePorts.sort((a, b) => Number(a) - Number(b)), + destinationPort: destinationPorts.sort((a, b) => Number(a) - Number(b)), + }; + }, [baseConnections]); + + const normalizedFilters = useMemo( + () => ({ + host: new Set( + filters.host.map((value) => value.trim().toLowerCase()).filter(Boolean), + ), + sourceIP: new Set( + filters.sourceIP.map((value) => value.trim()).filter(Boolean), + ), + destinationIP: new Set( + filters.destinationIP.map((value) => value.trim()).filter(Boolean), + ), + network: new Set( + filters.network + .map((value) => value.trim().toLowerCase()) + .filter(Boolean), + ), + sourcePort: new Set( + filters.sourcePort.map((value) => value.trim()).filter(Boolean), + ), + destinationPort: new Set( + filters.destinationPort.map((value) => value.trim()).filter(Boolean), + ), + }), + [filters], + ); + + const [filterConn] = useMemo(() => { + const orderFunc = orderFunctionMap[curOrderOpt]; + let matchConns = baseConnections.filter((conn) => { + const { metadata } = conn; + const searchTarget = [ + metadata.host, + metadata.destinationIP, + metadata.remoteDestination, + metadata.sourceIP, + metadata.sourcePort, + metadata.destinationPort, + metadata.process, + metadata.processPath, + metadata.type, + metadata.network, + ] + .filter(Boolean) + .join(" "); + + if (!match(searchTarget)) return false; + + const hostValue = ( + metadata.host || + metadata.remoteDestination || + "" + ).toLowerCase(); + const networkValue = (metadata.network || "").toLowerCase(); + const sourceIPValue = metadata.sourceIP || ""; + const destinationIPValue = metadata.destinationIP || ""; + const sourcePortValue = metadata.sourcePort || ""; + const destinationPortValue = metadata.destinationPort || ""; + + if ( + normalizedFilters.host.size > 0 && + !normalizedFilters.host.has(hostValue) + ) { + return false; + } + if ( + normalizedFilters.network.size > 0 && + !normalizedFilters.network.has(networkValue) + ) { + return false; + } + if ( + normalizedFilters.sourceIP.size > 0 && + !normalizedFilters.sourceIP.has(sourceIPValue) + ) { + return false; + } + if ( + normalizedFilters.destinationIP.size > 0 && + !normalizedFilters.destinationIP.has(destinationIPValue) + ) { + return false; + } + if ( + normalizedFilters.sourcePort.size > 0 && + !normalizedFilters.sourcePort.has(sourcePortValue) + ) { + return false; + } + if ( + normalizedFilters.destinationPort.size > 0 && + !normalizedFilters.destinationPort.has(destinationPortValue) + ) { + return false; + } + + return true; }); if (orderFunc) matchConns = orderFunc(matchConns ?? []); return [matchConns]; - }, [connections, connectionsType, match, curOrderOpt]); + }, [baseConnections, curOrderOpt, match, normalizedFilters]); + + const hasActiveFilters = useMemo( + () => Object.values(filters).some((values) => values.length > 0), + [filters], + ); + const hasSearchText = Boolean(searchState?.text?.trim()); + const hasFilterCriteria = hasActiveFilters || hasSearchText; const onCloseAll = useLockFn(closeAllConnections); + const onCloseFiltered = useLockFn(async () => { + if (connectionsType !== "active" || filterConn.length === 0) return; + if (!hasFilterCriteria) return; + await Promise.allSettled( + filterConn.map((conn) => closeConnection(conn.id)), + ); + }); + + const shouldCloseFiltered = connectionsType === "active" && hasFilterCriteria; + const closeActionLabel = shouldCloseFiltered + ? t("connections.components.actions.closeFiltered") + : t("shared.actions.closeAll"); const detailRef = useRef(null!); - const handleSearch = useCallback((match: (content: string) => boolean) => { - setMatch(() => match); - }, []); + const handleSearch = useCallback( + (matcher: (content: string) => boolean, state: SearchState) => { + setMatch(() => matcher); + setSearchState(state); + }, + [], + ); const hasTableData = filterConn.length > 0; + const handleFilterChange = useCallback( + (key: keyof ConnectionFilters) => (_: unknown, values: string[]) => { + const nextValues = Array.from( + new Set(values.map((value) => value.trim()).filter(Boolean)), + ); + setFilters((prev) => ({ ...prev, [key]: nextValues })); + }, + [], + ); + + const handleClearFilters = useCallback(() => { + setFilters({ ...EMPTY_FILTERS }); + }, []); + return ( { {t("shared.labels.uploaded")}:{" "} {parseTraffic(connections?.uploadTotal)} + setPaused((prev) => !prev)} + > + {paused ? ( + + ) : ( + + )} + { )} - } @@ -216,6 +460,147 @@ const ConnectionsPage = () => { ))} )} + + + + setFilterAnchorEl((prev) => (prev ? null : event.currentTarget)) + } + aria-label={t("connections.components.actions.filter")} + > + + + + + setFilterAnchorEl(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} + transformOrigin={{ vertical: "top", horizontal: "left" }} + > + + + + {t("connections.components.actions.filter")} + + + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + { ) : isTableLayout ? ( detailRef.current?.open(detail, connectionsType === "closed") } diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 80450dfc6..1bebef104 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -4,6 +4,11 @@ export const translationKeys = [ "connections.page.title", "connections.components.fields.host", + "connections.components.fields.sourceIP", + "connections.components.fields.destinationIP", + "connections.components.fields.sourcePort", + "connections.components.fields.destinationPort", + "connections.components.fields.network", "connections.components.fields.dlSpeed", "connections.components.fields.ulSpeed", "connections.components.fields.chains", @@ -12,14 +17,19 @@ export const translationKeys = [ "connections.components.fields.time", "connections.components.fields.source", "connections.components.fields.destination", - "connections.components.fields.destinationPort", "connections.components.fields.type", "connections.components.order.default", "connections.components.order.uploadSpeed", "connections.components.order.downloadSpeed", + "connections.components.order.uploadTotal", + "connections.components.order.downloadTotal", + "connections.components.order.duration", "connections.components.actions.active", "connections.components.actions.closed", "connections.components.actions.closeConnection", + "connections.components.actions.filter", + "connections.components.actions.clearFilters", + "connections.components.actions.closeFiltered", "connections.components.columnManager.title", "connections.components.columnManager.dragHandle", "home.page.tooltips.lightweightMode", diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 90cb8b838..f133ee58c 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -7,8 +7,11 @@ export interface TranslationResources { components: { actions: { active: string; + clearFilters: string; closeConnection: string; closed: string; + closeFiltered: string; + filter: string; }; columnManager: { dragHandle: string; @@ -17,12 +20,16 @@ export interface TranslationResources { fields: { chains: string; destination: string; + destinationIP: string; destinationPort: string; dlSpeed: string; host: string; + network: string; process: string; rule: string; source: string; + sourceIP: string; + sourcePort: string; time: string; type: string; ulSpeed: string; @@ -30,7 +37,10 @@ export interface TranslationResources { order: { default: string; downloadSpeed: string; + downloadTotal: string; + duration: string; uploadSpeed: string; + uploadTotal: string; }; }; page: {