mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-14 22:10:32 +08:00
refactor: replace @mui/x-data-grid with @tanstack/react-table (#5544)
This commit is contained in:
parent
7e373ff39c
commit
bf06cbc87d
@ -44,7 +44,8 @@
|
|||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/lab": "7.0.0-beta.17",
|
"@mui/lab": "7.0.0-beta.17",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
"@mui/x-data-grid": "^8.18.0",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@tanstack/react-virtual": "^3.13.12",
|
||||||
"@tauri-apps/api": "2.9.0",
|
"@tauri-apps/api": "2.9.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
|
|||||||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@ -38,9 +38,12 @@ importers:
|
|||||||
'@mui/material':
|
'@mui/material':
|
||||||
specifier: ^7.3.5
|
specifier: ^7.3.5
|
||||||
version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@mui/x-data-grid':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.18.0
|
specifier: ^8.21.3
|
||||||
version: 8.18.0(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@tanstack/react-virtual':
|
||||||
|
specifier: ^3.13.12
|
||||||
|
version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: 2.9.0
|
specifier: 2.9.0
|
||||||
version: 2.9.0
|
version: 2.9.0
|
||||||
@ -1245,22 +1248,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@mui/x-data-grid@8.18.0':
|
|
||||||
resolution: {integrity: sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@emotion/react': ^11.9.0
|
|
||||||
'@emotion/styled': ^11.8.1
|
|
||||||
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
|
|
||||||
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
|
|
||||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@emotion/react':
|
|
||||||
optional: true
|
|
||||||
'@emotion/styled':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mui/x-internals@8.18.0':
|
'@mui/x-internals@8.18.0':
|
||||||
resolution: {integrity: sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==}
|
resolution: {integrity: sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@ -1687,6 +1674,26 @@ packages:
|
|||||||
'@swc/types@0.1.25':
|
'@swc/types@0.1.25':
|
||||||
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.3':
|
||||||
|
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
|
'@tanstack/react-virtual@3.13.12':
|
||||||
|
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.3':
|
||||||
|
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12':
|
||||||
|
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||||
|
|
||||||
'@tauri-apps/api@2.9.0':
|
'@tauri-apps/api@2.9.0':
|
||||||
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
||||||
|
|
||||||
@ -5436,25 +5443,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.6
|
'@types/react': 19.2.6
|
||||||
|
|
||||||
'@mui/x-data-grid@8.18.0(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.28.4
|
|
||||||
'@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0)
|
|
||||||
'@mui/utils': 7.3.5(@types/react@19.2.6)(react@19.2.0)
|
|
||||||
'@mui/x-internals': 8.18.0(@types/react@19.2.6)(react@19.2.0)
|
|
||||||
'@mui/x-virtualizer': 0.2.8(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
clsx: 2.1.1
|
|
||||||
prop-types: 15.8.1
|
|
||||||
react: 19.2.0
|
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
|
||||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@emotion/react': 11.14.0(@types/react@19.2.6)(react@19.2.0)
|
|
||||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@types/react'
|
|
||||||
|
|
||||||
'@mui/x-internals@8.18.0(@types/react@19.2.6)(react@19.2.0)':
|
'@mui/x-internals@8.18.0(@types/react@19.2.6)(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
@ -5814,6 +5802,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/table-core': 8.21.3
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
|
'@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.13.12
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.3': {}
|
||||||
|
|
||||||
|
'@tanstack/virtual-core@3.13.12': {}
|
||||||
|
|
||||||
'@tauri-apps/api@2.9.0': {}
|
'@tauri-apps/api@2.9.0': {}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.9.4':
|
'@tauri-apps/cli-darwin-arm64@2.9.4':
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
import { Box } from "@mui/material";
|
import { ViewColumnRounded } from "@mui/icons-material";
|
||||||
|
import { Box, IconButton, Tooltip } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
ColumnDef,
|
||||||
GridColDef,
|
ColumnSizingState,
|
||||||
GridColumnOrderChangeParams,
|
SortingState,
|
||||||
GridColumnResizeParams,
|
VisibilityState,
|
||||||
GridColumnVisibilityModel,
|
flexRender,
|
||||||
useGridApiRef,
|
getCoreRowModel,
|
||||||
GridColumnMenuItemProps,
|
getSortedRowModel,
|
||||||
GridColumnMenuHideItem,
|
Updater,
|
||||||
useGridRootProps,
|
useReactTable,
|
||||||
} from "@mui/x-data-grid";
|
} from "@tanstack/react-table";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useLocalStorage } from "foxact/use-local-storage";
|
import { useLocalStorage } from "foxact/use-local-storage";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
createContext,
|
useRef,
|
||||||
use,
|
useState,
|
||||||
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { MouseEvent } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
@ -28,7 +29,7 @@ import { truncateStr } from "@/utils/truncate-str";
|
|||||||
|
|
||||||
import { ConnectionColumnManager } from "./connection-column-manager";
|
import { ConnectionColumnManager } from "./connection-column-manager";
|
||||||
|
|
||||||
const ColumnManagerContext = createContext<() => void>(() => {});
|
const ROW_HEIGHT = 40;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconcile stored column order with base columns to handle added/removed fields
|
* Reconcile stored column order with base columns to handle added/removed fields
|
||||||
@ -59,131 +60,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
onCloseColumnManager,
|
onCloseColumnManager,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const apiRef = useGridApiRef();
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const PATCH_FLAG_KEY = "__clashPatchedPublishEvent" as const;
|
|
||||||
const ORIGINAL_KEY = "__clashOriginalPublishEvent" as const;
|
|
||||||
let isUnmounted = false;
|
|
||||||
let retryHandle: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let cleanupOriginal: (() => void) | null = null;
|
|
||||||
|
|
||||||
const scheduleRetry = () => {
|
|
||||||
if (isUnmounted || retryHandle !== null) return;
|
|
||||||
retryHandle = setTimeout(() => {
|
|
||||||
retryHandle = null;
|
|
||||||
ensurePatched();
|
|
||||||
}, 16);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Safari occasionally emits grid events without an event object,
|
|
||||||
// and MUI expects `defaultMuiPrevented` to exist. Normalize here to avoid crashes.
|
|
||||||
const createFallbackEvent = () => {
|
|
||||||
const fallback = {
|
|
||||||
defaultMuiPrevented: false,
|
|
||||||
preventDefault() {
|
|
||||||
fallback.defaultMuiPrevented = true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureMuiEvent = (
|
|
||||||
value: unknown,
|
|
||||||
): {
|
|
||||||
defaultMuiPrevented: boolean;
|
|
||||||
preventDefault: () => void;
|
|
||||||
[key: string]: unknown;
|
|
||||||
} => {
|
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return createFallbackEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventObject = value as {
|
|
||||||
defaultMuiPrevented?: unknown;
|
|
||||||
preventDefault?: () => void;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof eventObject.defaultMuiPrevented !== "boolean") {
|
|
||||||
eventObject.defaultMuiPrevented = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof eventObject.preventDefault !== "function") {
|
|
||||||
eventObject.preventDefault = () => {
|
|
||||||
eventObject.defaultMuiPrevented = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventObject as {
|
|
||||||
defaultMuiPrevented: boolean;
|
|
||||||
preventDefault: () => void;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensurePatched = () => {
|
|
||||||
if (isUnmounted) return;
|
|
||||||
const api = apiRef.current;
|
|
||||||
|
|
||||||
if (!api?.publishEvent) {
|
|
||||||
scheduleRetry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataApi = api as unknown as typeof api &
|
|
||||||
Record<string, unknown>;
|
|
||||||
if (metadataApi[PATCH_FLAG_KEY] === true) return;
|
|
||||||
|
|
||||||
const originalPublishEvent = api.publishEvent;
|
|
||||||
|
|
||||||
// Use Proxy to create a more resilient wrapper that always normalizes events
|
|
||||||
const patchedPublishEvent = new Proxy(originalPublishEvent, {
|
|
||||||
apply(target, thisArg, rawArgs: unknown[]) {
|
|
||||||
rawArgs[2] = ensureMuiEvent(rawArgs[2]);
|
|
||||||
|
|
||||||
return Reflect.apply(
|
|
||||||
target as (...args: unknown[]) => unknown,
|
|
||||||
thisArg,
|
|
||||||
rawArgs,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}) as typeof originalPublishEvent;
|
|
||||||
|
|
||||||
api.publishEvent = patchedPublishEvent;
|
|
||||||
metadataApi[PATCH_FLAG_KEY] = true;
|
|
||||||
metadataApi[ORIGINAL_KEY] = originalPublishEvent;
|
|
||||||
|
|
||||||
cleanupOriginal = () => {
|
|
||||||
const storedOriginal = metadataApi[ORIGINAL_KEY] as
|
|
||||||
| typeof originalPublishEvent
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
api.publishEvent = (
|
|
||||||
typeof storedOriginal === "function"
|
|
||||||
? storedOriginal
|
|
||||||
: originalPublishEvent
|
|
||||||
) as typeof originalPublishEvent;
|
|
||||||
|
|
||||||
delete metadataApi[PATCH_FLAG_KEY];
|
|
||||||
delete metadataApi[ORIGINAL_KEY];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ensurePatched();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isUnmounted = true;
|
|
||||||
if (retryHandle !== null) {
|
|
||||||
clearTimeout(retryHandle);
|
|
||||||
retryHandle = null;
|
|
||||||
}
|
|
||||||
if (cleanupOriginal) {
|
|
||||||
cleanupOriginal();
|
|
||||||
cleanupOriginal = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [apiRef]);
|
|
||||||
|
|
||||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||||
Record<string, number>
|
Record<string, number>
|
||||||
>(
|
>(
|
||||||
@ -229,95 +105,132 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseColumns = useMemo<GridColDef[]>(() => {
|
const createConnectionRow = (each: IConnectionsItem) => {
|
||||||
|
const { metadata, rulePayload } = each;
|
||||||
|
const chains = [...each.chains].reverse().join(" / ");
|
||||||
|
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||||
|
const Destination = metadata.destinationIP
|
||||||
|
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||||
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||||
|
return {
|
||||||
|
id: each.id,
|
||||||
|
host: metadata.host
|
||||||
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
|
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||||
|
download: each.download,
|
||||||
|
upload: each.upload,
|
||||||
|
dlSpeed: each.curDownload,
|
||||||
|
ulSpeed: each.curUpload,
|
||||||
|
chains,
|
||||||
|
rule,
|
||||||
|
process: truncateStr(metadata.process || metadata.processPath),
|
||||||
|
time: each.start,
|
||||||
|
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
|
remoteDestination: Destination,
|
||||||
|
type: `${metadata.type}(${metadata.network})`,
|
||||||
|
connectionData: each,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConnectionRow = ReturnType<typeof createConnectionRow>;
|
||||||
|
|
||||||
|
type ColumnField = Exclude<keyof ConnectionRow, "connectionData">;
|
||||||
|
|
||||||
|
interface BaseColumn {
|
||||||
|
field: ColumnField;
|
||||||
|
headerName: string;
|
||||||
|
width?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
align?: "left" | "right";
|
||||||
|
cell?: (row: ConnectionRow) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseColumns = useMemo<BaseColumn[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: "host",
|
field: "host",
|
||||||
headerName: t("connections.components.fields.host"),
|
headerName: t("connections.components.fields.host"),
|
||||||
width: columnWidths["host"] || 220,
|
width: 180,
|
||||||
minWidth: 180,
|
minWidth: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "download",
|
field: "download",
|
||||||
headerName: t("shared.labels.downloaded"),
|
headerName: t("shared.labels.downloaded"),
|
||||||
width: columnWidths["download"] || 88,
|
width: 76,
|
||||||
|
minWidth: 60,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
cell: (row) => parseTraffic(row.download).join(" "),
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "upload",
|
field: "upload",
|
||||||
headerName: t("shared.labels.uploaded"),
|
headerName: t("shared.labels.uploaded"),
|
||||||
width: columnWidths["upload"] || 88,
|
width: 76,
|
||||||
|
minWidth: 60,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
cell: (row) => parseTraffic(row.upload).join(" "),
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: "dlSpeed",
|
||||||
headerName: t("connections.components.fields.dlSpeed"),
|
headerName: t("connections.components.fields.dlSpeed"),
|
||||||
width: columnWidths["dlSpeed"] || 88,
|
width: 76,
|
||||||
|
minWidth: 60,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
cell: (row) => `${parseTraffic(row.dlSpeed).join(" ")}/s`,
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: "ulSpeed",
|
||||||
headerName: t("connections.components.fields.ulSpeed"),
|
headerName: t("connections.components.fields.ulSpeed"),
|
||||||
width: columnWidths["ulSpeed"] || 88,
|
width: 76,
|
||||||
|
minWidth: 60,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
cell: (row) => `${parseTraffic(row.ulSpeed).join(" ")}/s`,
|
||||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "chains",
|
field: "chains",
|
||||||
headerName: t("connections.components.fields.chains"),
|
headerName: t("connections.components.fields.chains"),
|
||||||
width: columnWidths["chains"] || 340,
|
width: 280,
|
||||||
minWidth: 180,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "rule",
|
field: "rule",
|
||||||
headerName: t("connections.components.fields.rule"),
|
headerName: t("connections.components.fields.rule"),
|
||||||
width: columnWidths["rule"] || 280,
|
width: 220,
|
||||||
minWidth: 180,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "process",
|
field: "process",
|
||||||
headerName: t("connections.components.fields.process"),
|
headerName: t("connections.components.fields.process"),
|
||||||
width: columnWidths["process"] || 220,
|
width: 180,
|
||||||
minWidth: 180,
|
minWidth: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "time",
|
field: "time",
|
||||||
headerName: t("connections.components.fields.time"),
|
headerName: t("connections.components.fields.time"),
|
||||||
width: columnWidths["time"] || 120,
|
width: 100,
|
||||||
minWidth: 100,
|
minWidth: 80,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
cell: (row) => dayjs(row.time).fromNow(),
|
||||||
sortComparator: (v1: string, v2: string) =>
|
|
||||||
new Date(v2).getTime() - new Date(v1).getTime(),
|
|
||||||
valueFormatter: (value: number) => dayjs(value).fromNow(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "source",
|
field: "source",
|
||||||
headerName: t("connections.components.fields.source"),
|
headerName: t("connections.components.fields.source"),
|
||||||
width: columnWidths["source"] || 200,
|
width: 160,
|
||||||
minWidth: 130,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "remoteDestination",
|
field: "remoteDestination",
|
||||||
headerName: t("connections.components.fields.destination"),
|
headerName: t("connections.components.fields.destination"),
|
||||||
width: columnWidths["remoteDestination"] || 200,
|
width: 160,
|
||||||
minWidth: 130,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "type",
|
field: "type",
|
||||||
headerName: t("connections.components.fields.type"),
|
headerName: t("connections.components.fields.type"),
|
||||||
width: columnWidths["type"] || 160,
|
width: 120,
|
||||||
minWidth: 100,
|
minWidth: 80,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [columnWidths, t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumnOrder((prevValue) => {
|
setColumnOrder((prevValue) => {
|
||||||
@ -334,7 +247,7 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}, [baseColumns, setColumnOrder]);
|
}, [baseColumns, setColumnOrder]);
|
||||||
|
|
||||||
const columns = useMemo<GridColDef[]>(() => {
|
const columns = useMemo<BaseColumn[]>(() => {
|
||||||
const order = Array.isArray(columnOrder) ? columnOrder : [];
|
const order = Array.isArray(columnOrder) ? columnOrder : [];
|
||||||
const orderMap = new Map(order.map((field, index) => [field, index]));
|
const orderMap = new Map(order.map((field, index) => [field, index]));
|
||||||
|
|
||||||
@ -362,42 +275,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [columns, columnVisibilityModel]);
|
}, [columns, columnVisibilityModel]);
|
||||||
|
|
||||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
|
||||||
const { colDef, width } = params;
|
|
||||||
setColumnWidths((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[colDef.field]: width,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnVisibilityChange = useCallback(
|
|
||||||
(model: GridColumnVisibilityModel) => {
|
|
||||||
const hiddenFields = new Set<string>();
|
|
||||||
Object.entries(model).forEach(([field, value]) => {
|
|
||||||
if (value === false) {
|
|
||||||
hiddenFields.add(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextVisibleCount = columns.reduce((count, column) => {
|
|
||||||
return hiddenFields.has(column.field) ? count : count + 1;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
if (nextVisibleCount === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setColumnVisibilityModel(() => {
|
|
||||||
const sanitized: Partial<Record<string, boolean>> = {};
|
|
||||||
hiddenFields.forEach((field) => {
|
|
||||||
sanitized[field] = false;
|
|
||||||
});
|
|
||||||
return sanitized;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[columns, setColumnVisibilityModel],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleColumn = useCallback(
|
const handleToggleColumn = useCallback(
|
||||||
(field: string, visible: boolean) => {
|
(field: string, visible: boolean) => {
|
||||||
if (!visible && visibleColumnsCount <= 1) {
|
if (!visible && visibleColumnsCount <= 1) {
|
||||||
@ -417,30 +294,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
[setColumnVisibilityModel, visibleColumnsCount],
|
[setColumnVisibilityModel, visibleColumnsCount],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleColumnOrderChange = useCallback(
|
|
||||||
(params: GridColumnOrderChangeParams) => {
|
|
||||||
setColumnOrder((prevValue) => {
|
|
||||||
const baseFields = baseColumns.map((col) => col.field);
|
|
||||||
const currentOrder = Array.isArray(prevValue)
|
|
||||||
? [...prevValue]
|
|
||||||
: [...baseFields];
|
|
||||||
const field = params.column.field;
|
|
||||||
const currentIndex = currentOrder.indexOf(field);
|
|
||||||
if (currentIndex === -1) return currentOrder;
|
|
||||||
|
|
||||||
currentOrder.splice(currentIndex, 1);
|
|
||||||
const targetIndex = Math.min(
|
|
||||||
Math.max(params.targetIndex, 0),
|
|
||||||
currentOrder.length,
|
|
||||||
);
|
|
||||||
currentOrder.splice(targetIndex, 0, field);
|
|
||||||
|
|
||||||
return currentOrder;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[baseColumns, setColumnOrder],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleManagerOrderChange = useCallback(
|
const handleManagerOrderChange = useCallback(
|
||||||
(order: string[]) => {
|
(order: string[]) => {
|
||||||
setColumnOrder(() => {
|
setColumnOrder(() => {
|
||||||
@ -456,16 +309,54 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
setColumnOrder(baseColumns.map((col) => col.field));
|
setColumnOrder(baseColumns.map((col) => col.field));
|
||||||
}, [baseColumns, setColumnOrder, setColumnVisibilityModel]);
|
}, [baseColumns, setColumnOrder, setColumnVisibilityModel]);
|
||||||
|
|
||||||
const gridVisibilityModel = useMemo(() => {
|
const handleColumnVisibilityChange = useCallback(
|
||||||
const result: GridColumnVisibilityModel = {};
|
(update: Updater<VisibilityState>) => {
|
||||||
if (!columnVisibilityModel) return result;
|
setColumnVisibilityModel((prev) => {
|
||||||
Object.entries(columnVisibilityModel).forEach(([field, value]) => {
|
const current = prev ?? {};
|
||||||
if (typeof value === "boolean") {
|
const baseState: VisibilityState = {};
|
||||||
result[field] = value;
|
columns.forEach((column) => {
|
||||||
}
|
baseState[column.field] = (current[column.field] ?? true) !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedState =
|
||||||
|
typeof update === "function"
|
||||||
|
? update(baseState)
|
||||||
|
: { ...baseState, ...update };
|
||||||
|
|
||||||
|
const hiddenFields = columns
|
||||||
|
.filter((column) => mergedState[column.field] === false)
|
||||||
|
.map((column) => column.field);
|
||||||
|
|
||||||
|
if (columns.length - hiddenFields.length === 0) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized: Partial<Record<string, boolean>> = {};
|
||||||
|
hiddenFields.forEach((field) => {
|
||||||
|
sanitized[field] = false;
|
||||||
|
});
|
||||||
|
return sanitized;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[columns, setColumnVisibilityModel],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnVisibilityState = useMemo<VisibilityState>(() => {
|
||||||
|
const result: VisibilityState = {};
|
||||||
|
if (!columnVisibilityModel) {
|
||||||
|
columns.forEach((column) => {
|
||||||
|
result[column.field] = true;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.forEach((column) => {
|
||||||
|
result[column.field] =
|
||||||
|
(columnVisibilityModel?.[column.field] ?? true) !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [columnVisibilityModel]);
|
}, [columnVisibilityModel, columns]);
|
||||||
|
|
||||||
const columnOptions = useMemo(() => {
|
const columnOptions = useMemo(() => {
|
||||||
return columns.map((column) => ({
|
return columns.map((column) => ({
|
||||||
@ -475,72 +366,293 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
}));
|
}));
|
||||||
}, [columns, columnVisibilityModel]);
|
}, [columns, columnVisibilityModel]);
|
||||||
|
|
||||||
const connRows = useMemo(() => {
|
const connRows = useMemo<ConnectionRow[]>(
|
||||||
return connections.map((each) => {
|
() => connections.map((each) => createConnectionRow(each)),
|
||||||
const { metadata, rulePayload } = each;
|
[connections],
|
||||||
const chains = [...each.chains].reverse().join(" / ");
|
);
|
||||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
|
||||||
const Destination = metadata.destinationIP
|
const columnDefs = useMemo<ColumnDef<ConnectionRow>[]>(() => {
|
||||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
return columns.map((column) => ({
|
||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
id: column.field,
|
||||||
return {
|
accessorKey: column.field,
|
||||||
id: each.id,
|
header: column.headerName,
|
||||||
host: metadata.host
|
size: column.width,
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
minSize: column.minWidth ?? 80,
|
||||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
enableResizing: true,
|
||||||
download: each.download,
|
meta: {
|
||||||
upload: each.upload,
|
align: column.align ?? "left",
|
||||||
dlSpeed: each.curDownload,
|
field: column.field,
|
||||||
ulSpeed: each.curUpload,
|
},
|
||||||
chains,
|
cell: column.cell
|
||||||
rule,
|
? ({ row }) => column.cell?.(row.original)
|
||||||
process: truncateStr(metadata.process || metadata.processPath),
|
: (info) => info.getValue(),
|
||||||
time: each.start,
|
}));
|
||||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
}, [columns]);
|
||||||
remoteDestination: Destination,
|
|
||||||
type: `${metadata.type}(${metadata.network})`,
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
connectionData: each,
|
|
||||||
};
|
const handleColumnSizingChange = useCallback(
|
||||||
});
|
(updater: Updater<ColumnSizingState>) => {
|
||||||
}, [connections]);
|
setColumnWidths((prev) => {
|
||||||
|
const prevState = prev ?? {};
|
||||||
|
const nextState =
|
||||||
|
typeof updater === "function" ? updater(prevState) : updater;
|
||||||
|
const sanitized: Record<string, number> = {};
|
||||||
|
Object.entries(nextState).forEach(([key, size]) => {
|
||||||
|
if (typeof size === "number" && Number.isFinite(size)) {
|
||||||
|
sanitized[key] = size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sanitized;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setColumnWidths],
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: connRows,
|
||||||
|
columns: columnDefs,
|
||||||
|
state: {
|
||||||
|
columnVisibility: columnVisibilityState,
|
||||||
|
columnSizing: columnWidths,
|
||||||
|
sorting,
|
||||||
|
},
|
||||||
|
columnResizeMode: "onChange",
|
||||||
|
enableSortingRemoval: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnSizingChange: handleColumnSizingChange,
|
||||||
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = table.getRowModel().rows;
|
||||||
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: rows.length,
|
||||||
|
getScrollElement: () => tableContainerRef.current,
|
||||||
|
estimateSize: () => ROW_HEIGHT,
|
||||||
|
overscan: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||||
|
const totalSize = rowVirtualizer.getTotalSize();
|
||||||
|
const tableWidth = table.getTotalSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnManagerContext value={onOpenColumnManager}>
|
<>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
|
position: "relative",
|
||||||
|
fontFamily: (theme) => theme.typography.fontFamily,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DataGrid
|
<Tooltip title={t("connections.components.columnManager.title")}>
|
||||||
apiRef={apiRef}
|
<IconButton
|
||||||
rows={connRows}
|
size="small"
|
||||||
columns={columns}
|
onClick={onOpenColumnManager}
|
||||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
sx={{
|
||||||
density="compact"
|
position: "absolute",
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
zIndex: 3,
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? theme.palette.background.default
|
||||||
|
: theme.palette.background.paper,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ViewColumnRounded fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Box
|
||||||
|
ref={tableContainerRef}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
border: "none",
|
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
"div:focus": { outline: "none !important" },
|
overflow: "auto",
|
||||||
"& .MuiDataGrid-columnHeader": {
|
borderRadius: 1,
|
||||||
userSelect: "none",
|
border: "none",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
height: 8,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
columnVisibilityModel={gridVisibilityModel}
|
>
|
||||||
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
<Box
|
||||||
onColumnResize={handleColumnResize}
|
sx={{
|
||||||
onColumnOrderChange={handleColumnOrderChange}
|
minWidth: "100%",
|
||||||
slotProps={{
|
width: tableWidth,
|
||||||
columnMenu: {
|
}}
|
||||||
slots: {
|
>
|
||||||
columnMenuColumnsItem: ConnectionColumnMenuColumnsItem,
|
<Box
|
||||||
},
|
sx={{
|
||||||
},
|
position: "sticky",
|
||||||
}}
|
top: 0,
|
||||||
/>
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<Box
|
||||||
|
key={headerGroup.id}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
borderBottom: (theme) =>
|
||||||
|
`1px solid ${theme.palette.divider}`,
|
||||||
|
backgroundColor: (theme) => theme.palette.background.paper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
if (header.isPlaceholder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const meta = header.column.columnDef.meta as {
|
||||||
|
align?: "left" | "right";
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={header.id}
|
||||||
|
sx={{
|
||||||
|
flex: `0 0 ${header.getSize()}px`,
|
||||||
|
minWidth: header.column.columnDef.minSize || 80,
|
||||||
|
maxWidth: header.column.columnDef.maxSize,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "relative",
|
||||||
|
px: 1,
|
||||||
|
py: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "text.secondary",
|
||||||
|
userSelect: "none",
|
||||||
|
justifyContent:
|
||||||
|
meta?.align === "right" ? "flex-end" : "flex-start",
|
||||||
|
gap: 0.25,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.5,
|
||||||
|
cursor: header.column.getCanSort()
|
||||||
|
? "pointer"
|
||||||
|
: "default",
|
||||||
|
}}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
{{
|
||||||
|
asc: "▲",
|
||||||
|
desc: "▼",
|
||||||
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
|
</Box>
|
||||||
|
{header.column.getCanResize() && (
|
||||||
|
<Box
|
||||||
|
onMouseDown={header.getResizeHandler()}
|
||||||
|
onTouchStart={header.getResizeHandler()}
|
||||||
|
sx={{
|
||||||
|
cursor: "col-resize",
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 4,
|
||||||
|
height: "100%",
|
||||||
|
transform: "translateX(50%)",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.action.active,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
height: totalSize,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualRows.map((virtualRow) => {
|
||||||
|
const row = rows[virtualRow.index];
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={row.id}
|
||||||
|
onClick={() => onShowDetail(row.original.connectionData)}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: virtualRow.size,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
borderBottom: (theme) =>
|
||||||
|
`1px solid ${theme.palette.divider}`,
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: (theme) => theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
const meta = cell.column.columnDef.meta as {
|
||||||
|
align?: "left" | "right";
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={cell.id}
|
||||||
|
sx={{
|
||||||
|
flex: `0 0 ${cell.column.getSize()}px`,
|
||||||
|
minWidth: cell.column.columnDef.minSize || 80,
|
||||||
|
maxWidth: cell.column.columnDef.maxSize,
|
||||||
|
px: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent:
|
||||||
|
meta?.align === "right"
|
||||||
|
? "flex-end"
|
||||||
|
: "flex-start",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ConnectionColumnManager
|
<ConnectionColumnManager
|
||||||
open={columnManagerOpen}
|
open={columnManagerOpen}
|
||||||
@ -550,52 +662,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
onOrderChange={handleManagerOrderChange}
|
onOrderChange={handleManagerOrderChange}
|
||||||
onReset={handleResetColumns}
|
onReset={handleResetColumns}
|
||||||
/>
|
/>
|
||||||
</ColumnManagerContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConnectionColumnMenuManageItemProps = GridColumnMenuItemProps & {
|
|
||||||
onOpenColumnManager: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectionColumnMenuManageItem = (
|
|
||||||
props: ConnectionColumnMenuManageItemProps,
|
|
||||||
) => {
|
|
||||||
const { onClick, onOpenColumnManager } = props;
|
|
||||||
const rootProps = useGridRootProps();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(event: MouseEvent<HTMLElement>) => {
|
|
||||||
onClick(event);
|
|
||||||
onOpenColumnManager();
|
|
||||||
},
|
|
||||||
[onClick, onOpenColumnManager],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rootProps.disableColumnSelector) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MenuItem = rootProps.slots.baseMenuItem;
|
|
||||||
const Icon = rootProps.slots.columnMenuManageColumnsIcon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem onClick={handleClick} iconStart={<Icon fontSize="small" />}>
|
|
||||||
{t("connections.components.columnManager.title")}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectionColumnMenuColumnsItem = (props: GridColumnMenuItemProps) => {
|
|
||||||
const onOpenColumnManager = use(ColumnManagerContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<GridColumnMenuHideItem {...props} />
|
|
||||||
<ConnectionColumnMenuManageItem
|
|
||||||
{...props}
|
|
||||||
onOpenColumnManager={onOpenColumnManager}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material";
|
import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material";
|
||||||
import {
|
|
||||||
arSD as arXDataGrid,
|
|
||||||
enUS as enXDataGrid,
|
|
||||||
faIR as faXDataGrid,
|
|
||||||
ruRU as ruXDataGrid,
|
|
||||||
zhCN as zhXDataGrid,
|
|
||||||
} from "@mui/x-data-grid/locales";
|
|
||||||
import {
|
import {
|
||||||
getCurrentWebviewWindow,
|
getCurrentWebviewWindow,
|
||||||
WebviewWindow,
|
WebviewWindow,
|
||||||
} from "@tauri-apps/api/webviewWindow";
|
} from "@tauri-apps/api/webviewWindow";
|
||||||
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
|
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { defaultDarkTheme, defaultTheme } from "@/pages/_theme";
|
import { defaultDarkTheme, defaultTheme } from "@/pages/_theme";
|
||||||
@ -73,24 +65,12 @@ ${css}
|
|||||||
return scopedBlock;
|
return scopedBlock;
|
||||||
};
|
};
|
||||||
|
|
||||||
const languagePackMap: Record<string, any> = {
|
|
||||||
zh: { ...zhXDataGrid },
|
|
||||||
fa: { ...faXDataGrid },
|
|
||||||
ru: { ...ruXDataGrid },
|
|
||||||
ar: { ...arXDataGrid },
|
|
||||||
en: { ...enXDataGrid },
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLanguagePackMap = (key: string) =>
|
|
||||||
languagePackMap[key] || languagePackMap.en;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* custom theme
|
* custom theme
|
||||||
*/
|
*/
|
||||||
export const useCustomTheme = () => {
|
export const useCustomTheme = () => {
|
||||||
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const { theme_mode, theme_setting } = verge ?? {};
|
const { theme_mode, theme_setting } = verge ?? {};
|
||||||
const mode = useThemeMode();
|
const mode = useThemeMode();
|
||||||
const setMode = useSetThemeMode();
|
const setMode = useSetThemeMode();
|
||||||
@ -237,37 +217,34 @@ export const useCustomTheme = () => {
|
|||||||
let muiTheme: MuiTheme;
|
let muiTheme: MuiTheme;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
muiTheme = createTheme(
|
muiTheme = createTheme({
|
||||||
{
|
breakpoints: {
|
||||||
breakpoints: {
|
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
|
||||||
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
|
},
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
primary: { main: setting.primary_color || dt.primary_color },
|
||||||
|
secondary: { main: setting.secondary_color || dt.secondary_color },
|
||||||
|
info: { main: setting.info_color || dt.info_color },
|
||||||
|
error: { main: setting.error_color || dt.error_color },
|
||||||
|
warning: { main: setting.warning_color || dt.warning_color },
|
||||||
|
success: { main: setting.success_color || dt.success_color },
|
||||||
|
text: {
|
||||||
|
primary: setting.primary_text || dt.primary_text,
|
||||||
|
secondary: setting.secondary_text || dt.secondary_text,
|
||||||
},
|
},
|
||||||
palette: {
|
background: {
|
||||||
mode,
|
paper: dt.background_color,
|
||||||
primary: { main: setting.primary_color || dt.primary_color },
|
default: dt.background_color,
|
||||||
secondary: { main: setting.secondary_color || dt.secondary_color },
|
|
||||||
info: { main: setting.info_color || dt.info_color },
|
|
||||||
error: { main: setting.error_color || dt.error_color },
|
|
||||||
warning: { main: setting.warning_color || dt.warning_color },
|
|
||||||
success: { main: setting.success_color || dt.success_color },
|
|
||||||
text: {
|
|
||||||
primary: setting.primary_text || dt.primary_text,
|
|
||||||
secondary: setting.secondary_text || dt.secondary_text,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: dt.background_color,
|
|
||||||
default: dt.background_color,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shadows: Array(25).fill("none") as Shadows,
|
|
||||||
typography: {
|
|
||||||
fontFamily: setting.font_family
|
|
||||||
? `${setting.font_family}, ${dt.font_family}`
|
|
||||||
: dt.font_family,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getLanguagePackMap(i18n.language),
|
shadows: Array(25).fill("none") as Shadows,
|
||||||
);
|
typography: {
|
||||||
|
fontFamily: setting.font_family
|
||||||
|
? `${setting.font_family}, ${dt.font_family}`
|
||||||
|
: dt.font_family,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error creating MUI theme, falling back to defaults:", e);
|
console.error("Error creating MUI theme, falling back to defaults:", e);
|
||||||
muiTheme = createTheme({
|
muiTheme = createTheme({
|
||||||
@ -419,13 +396,7 @@ export const useCustomTheme = () => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return muiTheme;
|
return muiTheme;
|
||||||
}, [
|
}, [mode, theme_setting, userBackgroundImage, hasUserBackground]);
|
||||||
mode,
|
|
||||||
theme_setting,
|
|
||||||
i18n.language,
|
|
||||||
userBackgroundImage,
|
|
||||||
hasUserBackground,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { theme };
|
return { theme };
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user