feat: support closed connections (#5244)

* feat: support closed connections

* fix: clear closed connections

* feat: show footer

* feat: show closed connection detail

* docs: update Changelog.md

* chore: update
This commit is contained in:
oomeow
2025-11-13 19:50:23 +08:00
committed by GitHub
parent d73036a3b6
commit 1125dc0562
22 changed files with 195 additions and 120 deletions

View File

@@ -10,6 +10,7 @@
- **Mihomo(Meta) 内核升级至 v1.19.16** - **Mihomo(Meta) 内核升级至 v1.19.16**
- 支持连接页面各个项目的排序 - 支持连接页面各个项目的排序
- 实现可选的自动备份 - 实现可选的自动备份
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
</details> </details>

View File

@@ -8,19 +8,21 @@ import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
export interface ConnectionDetailRef { export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void; open: (detail: IConnectionsItem, closed: boolean) => void;
} }
export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) { export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [detail, setDetail] = useState<IConnectionsItem>(null!); const [detail, setDetail] = useState<IConnectionsItem>(null!);
const [closed, setClosed] = useState(false);
const theme = useTheme(); const theme = useTheme();
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: (detail: IConnectionsItem) => { open: (detail: IConnectionsItem, closed: boolean) => {
if (open) return; if (open) return;
setOpen(true); setOpen(true);
setDetail(detail); setDetail(detail);
setClosed(closed);
}, },
})); }));
@@ -42,7 +44,11 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
}} }}
message={ message={
detail ? ( detail ? (
<InnerConnectionDetail data={detail} onClose={onClose} /> <InnerConnectionDetail
data={detail}
closed={closed}
onClose={onClose}
/>
) : null ) : null
} }
/> />
@@ -51,10 +57,11 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
interface InnerProps { interface InnerProps {
data: IConnectionsItem; data: IConnectionsItem;
closed: boolean;
onClose?: () => void; onClose?: () => void;
} }
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { metadata, rulePayload } = data; const { metadata, rulePayload } = data;
const theme = useTheme(); const theme = useTheme();
@@ -134,18 +141,20 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
</div> </div>
))} ))}
<Box sx={{ textAlign: "right" }}> {!closed && (
<Button <Box sx={{ textAlign: "right" }}>
variant="contained" <Button
title={t("connections.components.actions.closeConnection")} variant="contained"
onClick={() => { title={t("connections.components.actions.closeConnection")}
onDelete(); onClick={() => {
onClose?.(); onDelete();
}} onClose?.();
> }}
{t("connections.components.actions.closeConnection")} >
</Button> {t("connections.components.actions.closeConnection")}
</Box> </Button>
</Box>
)}
</Box> </Box>
); );
}; };

View File

@@ -27,11 +27,12 @@ const Tag = styled("span")(({ theme }) => ({
interface Props { interface Props {
value: IConnectionsItem; value: IConnectionsItem;
closed: boolean;
onShowDetail?: () => void; onShowDetail?: () => void;
} }
export const ConnectionItem = (props: Props) => { export const ConnectionItem = (props: Props) => {
const { value, onShowDetail } = props; const { value, closed, onShowDetail } = props;
const { id, metadata, chains, start, curUpload, curDownload } = value; const { id, metadata, chains, start, curUpload, curDownload } = value;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -44,15 +45,17 @@ export const ConnectionItem = (props: Props) => {
dense dense
sx={{ borderBottom: "1px solid var(--divider-color)" }} sx={{ borderBottom: "1px solid var(--divider-color)" }}
secondaryAction={ secondaryAction={
<IconButton !closed && (
edge="end" <IconButton
color="inherit" edge="end"
onClick={onDelete} color="inherit"
title={t("connections.components.actions.closeConnection")} onClick={onDelete}
aria-label={t("connections.components.actions.closeConnection")} title={t("connections.components.actions.closeConnection")}
> aria-label={t("connections.components.actions.closeConnection")}
<CloseRounded /> >
</IconButton> <CloseRounded />
</IconButton>
)
} }
> >
<ListItemText <ListItemText

View File

@@ -516,7 +516,6 @@ export const ConnectionTable = (props: Props) => {
> >
<DataGrid <DataGrid
apiRef={apiRef} apiRef={apiRef}
hideFooter
rows={connRows} rows={connRows}
columns={columns} columns={columns}
onRowClick={(e) => onShowDetail(e.row.connectionData)} onRowClick={(e) => onShowDetail(e.row.connectionData)}

View File

@@ -187,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit, uploadTotalUnit,
downloadTotal, downloadTotal,
downloadTotalUnit, downloadTotalUnit,
connectionsCount: connections?.connections.length, connectionsCount: connections?.activeConnections.length,
}; };
}, [traffic, memory, connections]); }, [traffic, memory, connections]);

View File

@@ -4,12 +4,22 @@ import { mutate } from "swr";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export const initConnData: IConnections = { export const initConnData: ConnectionMonitorData = {
uploadTotal: 0, uploadTotal: 0,
downloadTotal: 0, downloadTotal: 0,
connections: [], activeConnections: [],
closedConnections: [],
}; };
export interface ConnectionMonitorData {
uploadTotal: number;
downloadTotal: number;
activeConnections: IConnectionsItem[];
closedConnections: IConnectionsItem[];
}
const MAX_CLOSED_CONNS_NUM = 500;
export const useConnectionData = () => { export const useConnectionData = () => {
const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now());
const subscriptKey = `getClashConnection-${date}`; const subscriptKey = `getClashConnection-${date}`;
@@ -18,7 +28,11 @@ export const useConnectionData = () => {
const wsFirstConnection = useRef<boolean>(true); const wsFirstConnection = useRef<boolean>(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null); const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<IConnections, any, string | null>( const response = useSWRSubscription<
ConnectionMonitorData,
any,
string | null
>(
subscriptKey, subscriptKey,
(_key, { next }) => { (_key, { next }) => {
const reconnect = async () => { const reconnect = async () => {
@@ -41,28 +55,44 @@ export const useConnectionData = () => {
} else { } else {
const data = JSON.parse(msg.data) as IConnections; const data = JSON.parse(msg.data) as IConnections;
next(null, (old = initConnData) => { next(null, (old = initConnData) => {
const oldConn = old.connections; const oldConn = old.activeConnections;
const maxLen = data.connections?.length; const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = []; const activeConns: IConnectionsItem[] = [];
const rest = (data.connections || []).filter((each) => { const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id); const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) { if (index >= 0 && index < maxLen) {
const old = oldConn[index]; const old = oldConn[index];
each.curUpload = each.upload - old.upload; each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download; each.curDownload = each.download - old.download;
connections[index] = each; activeConns[index] = each;
return false; return false;
} }
return true; return true;
}); });
for (let i = 0; i < maxLen; ++i) { for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) { if (!activeConns[i] && rest.length > 0) {
connections[i] = rest.shift()!; activeConns[i] = rest.shift()!;
connections[i].curUpload = 0; activeConns[i].curUpload = 0;
connections[i].curDownload = 0; activeConns[i].curDownload = 0;
} }
} }
return { ...data, connections }; const currentClosedConns = oldConn.filter((each) => {
const index = activeConns.findIndex(
(o) => o.id === each.id,
);
return index < 0;
});
let closedConns =
old.closedConnections.concat(currentClosedConns);
if (closedConns.length > 500) {
closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM);
}
return {
uploadTotal: data.uploadTotal,
downloadTotal: data.downloadTotal,
activeConnections: activeConns,
closedConnections: closedConns,
};
}); });
} }
} }
@@ -109,5 +139,14 @@ export const useConnectionData = () => {
setDate(Date.now()); setDate(Date.now());
}; };
return { response, refreshGetClashConnection }; const clearClosedConnections = () => {
mutate(`$sub$${subscriptKey}`, {
uploadTotal: response.data?.uploadTotal ?? 0,
downloadTotal: response.data?.downloadTotal ?? 0,
activeConnections: response.data?.activeConnections ?? [],
closedConnections: [],
});
};
return { response, refreshGetClashConnection, clearClosedConnections };
}; };

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "سرعة التنزيل" "downloadSpeed": "سرعة التنزيل"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "إغلاق الاتصال" "closeConnection": "إغلاق الاتصال"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Download-Geschwindigkeit" "downloadSpeed": "Download-Geschwindigkeit"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Verbindung schließen" "closeConnection": "Verbindung schließen"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Download Speed" "downloadSpeed": "Download Speed"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Close Connection" "closeConnection": "Close Connection"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Velocidad de descarga" "downloadSpeed": "Velocidad de descarga"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Cerrar conexión" "closeConnection": "Cerrar conexión"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "سرعت دانلود" "downloadSpeed": "سرعت دانلود"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "بستن اتصال" "closeConnection": "بستن اتصال"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Kecepatan Unduh" "downloadSpeed": "Kecepatan Unduh"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Tutup Koneksi" "closeConnection": "Tutup Koneksi"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "ダウンロード速度" "downloadSpeed": "ダウンロード速度"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "接続を閉じる" "closeConnection": "接続を閉じる"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "다운로드 속도" "downloadSpeed": "다운로드 속도"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "연결 닫기" "closeConnection": "연결 닫기"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Скорость скачивания" "downloadSpeed": "Скорость скачивания"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Закрыть соединение" "closeConnection": "Закрыть соединение"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "İndirme Hızı" "downloadSpeed": "İndirme Hızı"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Bağlantıyı Kapat" "closeConnection": "Bağlantıyı Kapat"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "Йөкләү тизлеге" "downloadSpeed": "Йөкләү тизлеге"
}, },
"actions": { "actions": {
"active": "Active",
"closed": "Closed",
"closeConnection": "Тоташуны ябу" "closeConnection": "Тоташуны ябу"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "下载速度" "downloadSpeed": "下载速度"
}, },
"actions": { "actions": {
"active": "活跃",
"closed": "已关闭",
"closeConnection": "关闭连接" "closeConnection": "关闭连接"
}, },
"columnManager": { "columnManager": {

View File

@@ -22,6 +22,8 @@
"downloadSpeed": "下載速度" "downloadSpeed": "下載速度"
}, },
"actions": { "actions": {
"active": "活躍",
"closed": "已關閉",
"closeConnection": "關閉連線" "closeConnection": "關閉連線"
}, },
"columnManager": { "columnManager": {

View File

@@ -1,10 +1,17 @@
import { import {
PauseCircleOutlineRounded, DeleteForeverRounded,
PlayCircleOutlineRounded,
TableChartRounded, TableChartRounded,
TableRowsRounded, TableRowsRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Button, IconButton, MenuItem } from "@mui/material"; import {
Box,
Button,
ButtonGroup,
Fab,
IconButton,
MenuItem,
Zoom,
} from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -21,16 +28,9 @@ import {
import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table"; import { ConnectionTable } from "@/components/connection/connection-table";
import { useConnectionData } from "@/hooks/use-connection-data"; import { useConnectionData } from "@/hooks/use-connection-data";
import { useVisibility } from "@/hooks/use-visibility";
import { useConnectionSetting } from "@/services/states"; import { useConnectionSetting } from "@/services/states";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
const initConn: IConnections = {
uploadTotal: 0,
downloadTotal: 0,
connections: [],
};
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
const ORDER_OPTIONS = [ const ORDER_OPTIONS = [
@@ -70,61 +70,42 @@ const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
const ConnectionsPage = () => { const ConnectionsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const pageVisible = useVisibility();
const [match, setMatch] = useState<(input: string) => boolean>( const [match, setMatch] = useState<(input: string) => boolean>(
() => () => true, () => () => true,
); );
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>("default"); const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>("default");
const [connectionsType, setConnectionsType] = useState<"active" | "closed">(
"active",
);
const { const {
response: { data: connections }, response: { data: connections },
clearClosedConnections,
} = useConnectionData(); } = useConnectionData();
const [setting, setSetting] = useConnectionSetting(); const [setting, setSetting] = useConnectionSetting();
const isTableLayout = setting.layout === "table"; const isTableLayout = setting.layout === "table";
const [isPaused, setIsPaused] = useState(false);
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false); const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false);
// 使用全局连接数据
const displayData = useMemo(() => {
if (!pageVisible) return initConn;
if (isPaused) {
return (
frozenData ?? {
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
}
);
}
return {
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
};
}, [isPaused, frozenData, connections, pageVisible]);
const [filterConn] = useMemo(() => { const [filterConn] = useMemo(() => {
const orderFunc = orderFunctionMap[curOrderOpt]; const orderFunc = orderFunctionMap[curOrderOpt];
let conns: IConnectionsItem[] = (displayData.connections ?? []).filter( const conns =
(conn) => { (connectionsType === "active"
const { host, destinationIP, process } = conn.metadata; ? connections?.activeConnections
return ( : connections?.closedConnections) ?? [];
match(host || "") || let matchConns = conns.filter((conn) => {
match(destinationIP || "") || const { host, destinationIP, process } = conn.metadata;
match(process || "") return (
); match(host || "") || match(destinationIP || "") || match(process || "")
}, );
); });
if (orderFunc) conns = orderFunc(conns);
return [conns]; if (orderFunc) matchConns = orderFunc(matchConns ?? []);
}, [displayData, match, curOrderOpt]);
return [matchConns];
}, [connections, connectionsType, match, curOrderOpt]);
const onCloseAll = useLockFn(closeAllConnections); const onCloseAll = useLockFn(closeAllConnections);
@@ -134,21 +115,6 @@ const ConnectionsPage = () => {
setMatch(() => match); setMatch(() => match);
}, []); }, []);
const handlePauseToggle = useCallback(() => {
setIsPaused((prev) => {
if (!prev) {
setFrozenData({
uploadTotal: connections?.uploadTotal ?? 0,
downloadTotal: connections?.downloadTotal ?? 0,
connections: connections?.connections ?? [],
});
} else {
setFrozenData(null);
}
return !prev;
});
}, [connections]);
const hasTableData = filterConn.length > 0; const hasTableData = filterConn.length > 0;
return ( return (
@@ -170,11 +136,11 @@ const ConnectionsPage = () => {
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("shared.labels.downloaded")}:{" "} {t("shared.labels.downloaded")}:{" "}
{parseTraffic(displayData.downloadTotal)} {parseTraffic(connections?.downloadTotal)}
</Box> </Box>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("shared.labels.uploaded")}:{" "} {t("shared.labels.uploaded")}:{" "}
{parseTraffic(displayData.uploadTotal)} {parseTraffic(connections?.uploadTotal)}
</Box> </Box>
<IconButton <IconButton
color="inherit" color="inherit"
@@ -193,20 +159,6 @@ const ConnectionsPage = () => {
<TableChartRounded titleAccess={t("shared.actions.tableView")} /> <TableChartRounded titleAccess={t("shared.actions.tableView")} />
)} )}
</IconButton> </IconButton>
<IconButton
color="inherit"
size="small"
onClick={handlePauseToggle}
title={
isPaused ? t("shared.actions.resume") : t("shared.actions.pause")
}
>
{isPaused ? (
<PlayCircleOutlineRounded />
) : (
<PauseCircleOutlineRounded />
)}
</IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}> <Button size="small" variant="contained" onClick={onCloseAll}>
<span style={{ whiteSpace: "nowrap" }}> <span style={{ whiteSpace: "nowrap" }}>
{t("shared.actions.closeAll")} {t("shared.actions.closeAll")}
@@ -230,6 +182,24 @@ const ConnectionsPage = () => {
zIndex: 2, zIndex: 2,
}} }}
> >
<ButtonGroup sx={{ mr: 1, flexBasis: "content" }}>
<Button
size="small"
variant={connectionsType === "active" ? "contained" : "outlined"}
onClick={() => setConnectionsType("active")}
>
{t("connections.components.actions.active")}{" "}
{connections?.activeConnections.length}
</Button>
<Button
size="small"
variant={connectionsType === "closed" ? "contained" : "outlined"}
onClick={() => setConnectionsType("closed")}
>
{t("connections.components.actions.closed")}{" "}
{connections?.closedConnections.length}
</Button>
</ButtonGroup>
{!isTableLayout && ( {!isTableLayout && (
<BaseStyledSelect <BaseStyledSelect
value={curOrderOpt} value={curOrderOpt}
@@ -261,7 +231,9 @@ const ConnectionsPage = () => {
) : isTableLayout ? ( ) : isTableLayout ? (
<ConnectionTable <ConnectionTable
connections={filterConn} connections={filterConn}
onShowDetail={(detail) => detailRef.current?.open(detail)} onShowDetail={(detail) =>
detailRef.current?.open(detail, connectionsType === "closed")
}
columnManagerOpen={isTableLayout && isColumnManagerOpen} columnManagerOpen={isTableLayout && isColumnManagerOpen}
onOpenColumnManager={() => setIsColumnManagerOpen(true)} onOpenColumnManager={() => setIsColumnManagerOpen(true)}
onCloseColumnManager={() => setIsColumnManagerOpen(false)} onCloseColumnManager={() => setIsColumnManagerOpen(false)}
@@ -276,12 +248,34 @@ const ConnectionsPage = () => {
itemContent={(_, item) => ( itemContent={(_, item) => (
<ConnectionItem <ConnectionItem
value={item} value={item}
onShowDetail={() => detailRef.current?.open(item)} closed={connectionsType === "closed"}
onShowDetail={() =>
detailRef.current?.open(item, connectionsType === "closed")
}
/> />
)} )}
/> />
)} )}
<ConnectionDetail ref={detailRef} /> <ConnectionDetail ref={detailRef} />
<Zoom
in={connectionsType === "closed" && filterConn.length > 0}
unmountOnExit
>
<Fab
size="medium"
variant="extended"
sx={{
position: "absolute",
right: 16,
bottom: isTableLayout ? 70 : 16,
}}
color="primary"
onClick={() => clearClosedConnections()}
>
<DeleteForeverRounded sx={{ mr: 1 }} fontSize="small" />
{t("shared.actions.clear")}
</Fab>
</Zoom>
</BasePage> </BasePage>
); );
}; };

View File

@@ -17,6 +17,8 @@ export const translationKeys = [
"connections.components.order.default", "connections.components.order.default",
"connections.components.order.uploadSpeed", "connections.components.order.uploadSpeed",
"connections.components.order.downloadSpeed", "connections.components.order.downloadSpeed",
"connections.components.actions.active",
"connections.components.actions.closed",
"connections.components.actions.closeConnection", "connections.components.actions.closeConnection",
"connections.components.columnManager.title", "connections.components.columnManager.title",
"connections.components.columnManager.dragHandle", "connections.components.columnManager.dragHandle",

View File

@@ -6,7 +6,9 @@ export interface TranslationResources {
connections: { connections: {
components: { components: {
actions: { actions: {
active: string;
closeConnection: string; closeConnection: string;
closed: string;
}; };
columnManager: { columnManager: {
dragHandle: string; dragHandle: string;