mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 17:15:38 +08:00
* feat(connections): enable persistent column visibility and ordering fix(connections): smooth column manager access in table view fix(connections): integrate column manager into column menu - Move the Connections column manager trigger into the DataGrid column menu, reusing existing i18n strings - Clean up the page toolbar by removing the standalone button and lifting dialog state to the parent page fix(connections): unify column order handling and resolve lint warnings fix(connections): unify column order handling and enforce 0 ESLint warnings - extract reconcileColumnOrder helper and reuse it across: - initial normalization (useEffect) - manager drag order handler (handleManagerOrderChange) - derive columnOptions directly from the computed, sorted columns to keep a single source of truth and prevent dialog/grid drift - connections.tsx: remove direct setState in useEffect; gate dialog open with `isTableLayout && isColumnManagerOpen`; clean up unused imports - connection-column-manager.tsx: replace deprecated `primaryTypographyProps` with `slotProps` - run full lint; project now has 0 warnings on main configuration * feat(connection-table): safeguard column visibility --------- Co-authored-by: Slinetrac <realakayuki@gmail.com>
This commit is contained in:
191
src/components/connection/connection-column-manager.tsx
Normal file
191
src/components/connection/connection-column-manager.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DragEndEvent,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { DragIndicatorRounded } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface ColumnOption {
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
columns: ColumnOption[];
|
||||||
|
onClose: () => void;
|
||||||
|
onToggle: (field: string, visible: boolean) => void;
|
||||||
|
onOrderChange: (order: string[]) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectionColumnManager = ({
|
||||||
|
open,
|
||||||
|
columns,
|
||||||
|
onClose,
|
||||||
|
onToggle,
|
||||||
|
onOrderChange,
|
||||||
|
onReset,
|
||||||
|
}: Props) => {
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: { distance: 6 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const items = useMemo(() => columns.map((column) => column.field), [columns]);
|
||||||
|
const visibleCount = useMemo(
|
||||||
|
() => columns.filter((column) => column.visible).length,
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id) return;
|
||||||
|
|
||||||
|
const order = columns.map((column) => column.field);
|
||||||
|
const oldIndex = order.indexOf(active.id as string);
|
||||||
|
const newIndex = order.indexOf(over.id as string);
|
||||||
|
if (oldIndex === -1 || newIndex === -1) return;
|
||||||
|
|
||||||
|
onOrderChange(arrayMove(order, oldIndex, newIndex));
|
||||||
|
},
|
||||||
|
[columns, onOrderChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("connections.components.columnManager.title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ pt: 1 }}>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext items={items}>
|
||||||
|
<List
|
||||||
|
dense
|
||||||
|
disablePadding
|
||||||
|
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
|
||||||
|
>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<SortableColumnItem
|
||||||
|
key={column.field}
|
||||||
|
column={column}
|
||||||
|
onToggle={onToggle}
|
||||||
|
dragHandleLabel={t(
|
||||||
|
"connections.components.columnManager.dragHandle",
|
||||||
|
)}
|
||||||
|
disableToggle={column.visible && visibleCount <= 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Button variant="text" onClick={onReset}>
|
||||||
|
{t("shared.actions.resetToDefault")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={onClose}>
|
||||||
|
{t("shared.actions.close")}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SortableColumnItemProps {
|
||||||
|
column: ColumnOption;
|
||||||
|
onToggle: (field: string, visible: boolean) => void;
|
||||||
|
dragHandleLabel: string;
|
||||||
|
disableToggle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableColumnItem = ({
|
||||||
|
column,
|
||||||
|
onToggle,
|
||||||
|
dragHandleLabel,
|
||||||
|
disableToggle = false,
|
||||||
|
}: SortableColumnItemProps) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: column.field });
|
||||||
|
|
||||||
|
const style = useMemo(
|
||||||
|
() => ({
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
}),
|
||||||
|
[transform, transition],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
ref={setNodeRef}
|
||||||
|
disableGutters
|
||||||
|
sx={{
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
|
backgroundColor: isDragging ? "action.hover" : "transparent",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
edge="start"
|
||||||
|
checked={column.visible}
|
||||||
|
disabled={disableToggle}
|
||||||
|
onChange={(event) => onToggle(column.field, event.target.checked)}
|
||||||
|
/>
|
||||||
|
<ListItemText
|
||||||
|
primary={column.label}
|
||||||
|
slotProps={{ primary: { variant: "body2" } }}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
sx={{ cursor: isDragging ? "grabbing" : "grab" }}
|
||||||
|
aria-label={dragHandleLabel}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<DragIndicatorRounded fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,24 +1,63 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
DataGrid,
|
||||||
GridColDef,
|
GridColDef,
|
||||||
|
GridColumnOrderChangeParams,
|
||||||
GridColumnResizeParams,
|
GridColumnResizeParams,
|
||||||
|
GridColumnVisibilityModel,
|
||||||
useGridApiRef,
|
useGridApiRef,
|
||||||
|
GridColumnMenuItemProps,
|
||||||
|
GridColumnMenuHideItem,
|
||||||
|
useGridRootProps,
|
||||||
} from "@mui/x-data-grid";
|
} from "@mui/x-data-grid";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useLocalStorage } from "foxact/use-local-storage";
|
import { useLocalStorage } from "foxact/use-local-storage";
|
||||||
import { useLayoutEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
createContext,
|
||||||
|
use,
|
||||||
|
} 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";
|
||||||
import { truncateStr } from "@/utils/truncate-str";
|
import { truncateStr } from "@/utils/truncate-str";
|
||||||
|
|
||||||
|
import { ConnectionColumnManager } from "./connection-column-manager";
|
||||||
|
|
||||||
|
const ColumnManagerContext = createContext<() => void>(() => {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile stored column order with base columns to handle added/removed fields
|
||||||
|
*/
|
||||||
|
const reconcileColumnOrder = (
|
||||||
|
storedOrder: string[],
|
||||||
|
baseFields: string[],
|
||||||
|
): string[] => {
|
||||||
|
const filtered = storedOrder.filter((field) => baseFields.includes(field));
|
||||||
|
const missing = baseFields.filter((field) => !filtered.includes(field));
|
||||||
|
return [...filtered, ...missing];
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[];
|
connections: IConnectionsItem[];
|
||||||
onShowDetail: (data: IConnectionsItem) => void;
|
onShowDetail: (data: IConnectionsItem) => void;
|
||||||
|
columnManagerOpen: boolean;
|
||||||
|
onOpenColumnManager: () => void;
|
||||||
|
onCloseColumnManager: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionTable = (props: Props) => {
|
export const ConnectionTable = (props: Props) => {
|
||||||
const { connections, onShowDetail } = props;
|
const {
|
||||||
|
connections,
|
||||||
|
onShowDetail,
|
||||||
|
columnManagerOpen,
|
||||||
|
onOpenColumnManager,
|
||||||
|
onCloseColumnManager,
|
||||||
|
} = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const apiRef = useGridApiRef();
|
const apiRef = useGridApiRef();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -145,10 +184,6 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
};
|
};
|
||||||
}, [apiRef]);
|
}, [apiRef]);
|
||||||
|
|
||||||
const [columnVisible, setColumnVisible] = useState<
|
|
||||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||||
Record<string, number>
|
Record<string, number>
|
||||||
>(
|
>(
|
||||||
@@ -158,7 +193,43 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useMemo<GridColDef[]>(() => {
|
const [columnVisibilityModel, setColumnVisibilityModel] = useLocalStorage<
|
||||||
|
Partial<Record<string, boolean>>
|
||||||
|
>(
|
||||||
|
"connection-table-visibility",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
serializer: JSON.stringify,
|
||||||
|
deserializer: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (parsed && typeof parsed === "object") return parsed;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to parse connection-table-visibility", err);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>(
|
||||||
|
"connection-table-order",
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
serializer: JSON.stringify,
|
||||||
|
deserializer: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (Array.isArray(parsed)) return parsed;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to parse connection-table-order", err);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseColumns = useMemo<GridColDef[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: "host",
|
field: "host",
|
||||||
@@ -248,6 +319,49 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
];
|
];
|
||||||
}, [columnWidths, t]);
|
}, [columnWidths, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColumnOrder((prevValue) => {
|
||||||
|
const baseFields = baseColumns.map((col) => col.field);
|
||||||
|
const prev = Array.isArray(prevValue) ? prevValue : [];
|
||||||
|
const reconciled = reconcileColumnOrder(prev, baseFields);
|
||||||
|
if (
|
||||||
|
reconciled.length === prev.length &&
|
||||||
|
reconciled.every((field, i) => field === prev[i])
|
||||||
|
) {
|
||||||
|
return prevValue;
|
||||||
|
}
|
||||||
|
return reconciled;
|
||||||
|
});
|
||||||
|
}, [baseColumns, setColumnOrder]);
|
||||||
|
|
||||||
|
const columns = useMemo<GridColDef[]>(() => {
|
||||||
|
const order = Array.isArray(columnOrder) ? columnOrder : [];
|
||||||
|
const orderMap = new Map(order.map((field, index) => [field, index]));
|
||||||
|
|
||||||
|
return [...baseColumns].sort((a, b) => {
|
||||||
|
const aIndex = orderMap.has(a.field)
|
||||||
|
? (orderMap.get(a.field) as number)
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
const bIndex = orderMap.has(b.field)
|
||||||
|
? (orderMap.get(b.field) as number)
|
||||||
|
: Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
if (aIndex === bIndex) {
|
||||||
|
return order.indexOf(a.field) - order.indexOf(b.field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
}, [baseColumns, columnOrder]);
|
||||||
|
|
||||||
|
const visibleColumnsCount = useMemo(() => {
|
||||||
|
return columns.reduce((count, column) => {
|
||||||
|
return (columnVisibilityModel?.[column.field] ?? true) !== false
|
||||||
|
? count + 1
|
||||||
|
: count;
|
||||||
|
}, 0);
|
||||||
|
}, [columns, columnVisibilityModel]);
|
||||||
|
|
||||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||||
const { colDef, width } = params;
|
const { colDef, width } = params;
|
||||||
setColumnWidths((prev) => ({
|
setColumnWidths((prev) => ({
|
||||||
@@ -256,6 +370,111 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
(field: string, visible: boolean) => {
|
||||||
|
if (!visible && visibleColumnsCount <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setColumnVisibilityModel((prev) => {
|
||||||
|
const next = { ...(prev ?? {}) };
|
||||||
|
if (visible) {
|
||||||
|
delete next[field];
|
||||||
|
} else {
|
||||||
|
next[field] = false;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[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(
|
||||||
|
(order: string[]) => {
|
||||||
|
setColumnOrder(() => {
|
||||||
|
const baseFields = baseColumns.map((col) => col.field);
|
||||||
|
return reconcileColumnOrder(order, baseFields);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[baseColumns, setColumnOrder],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResetColumns = useCallback(() => {
|
||||||
|
setColumnVisibilityModel({});
|
||||||
|
setColumnOrder(baseColumns.map((col) => col.field));
|
||||||
|
}, [baseColumns, setColumnOrder, setColumnVisibilityModel]);
|
||||||
|
|
||||||
|
const gridVisibilityModel = useMemo(() => {
|
||||||
|
const result: GridColumnVisibilityModel = {};
|
||||||
|
if (!columnVisibilityModel) return result;
|
||||||
|
Object.entries(columnVisibilityModel).forEach(([field, value]) => {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
result[field] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [columnVisibilityModel]);
|
||||||
|
|
||||||
|
const columnOptions = useMemo(() => {
|
||||||
|
return columns.map((column) => ({
|
||||||
|
field: column.field,
|
||||||
|
label: column.headerName ?? column.field,
|
||||||
|
visible: (columnVisibilityModel?.[column.field] ?? true) !== false,
|
||||||
|
}));
|
||||||
|
}, [columns, columnVisibilityModel]);
|
||||||
|
|
||||||
const connRows = useMemo(() => {
|
const connRows = useMemo(() => {
|
||||||
return connections.map((each) => {
|
return connections.map((each) => {
|
||||||
const { metadata, rulePayload } = each;
|
const { metadata, rulePayload } = each;
|
||||||
@@ -286,24 +505,98 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
}, [connections]);
|
}, [connections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid
|
<ColumnManagerContext value={onOpenColumnManager}>
|
||||||
apiRef={apiRef}
|
<Box
|
||||||
hideFooter
|
sx={{
|
||||||
rows={connRows}
|
display: "flex",
|
||||||
columns={columns}
|
flexDirection: "column",
|
||||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
flex: 1,
|
||||||
density="compact"
|
minHeight: 0,
|
||||||
sx={{
|
}}
|
||||||
border: "none",
|
>
|
||||||
"div:focus": { outline: "none !important" },
|
<DataGrid
|
||||||
"& .MuiDataGrid-columnHeader": {
|
apiRef={apiRef}
|
||||||
userSelect: "none",
|
hideFooter
|
||||||
},
|
rows={connRows}
|
||||||
}}
|
columns={columns}
|
||||||
columnVisibilityModel={columnVisible}
|
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
density="compact"
|
||||||
onColumnResize={handleColumnResize}
|
sx={{
|
||||||
disableColumnMenu={false}
|
flex: 1,
|
||||||
/>
|
border: "none",
|
||||||
|
minHeight: 0,
|
||||||
|
"div:focus": { outline: "none !important" },
|
||||||
|
"& .MuiDataGrid-columnHeader": {
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
columnVisibilityModel={gridVisibilityModel}
|
||||||
|
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||||
|
onColumnResize={handleColumnResize}
|
||||||
|
onColumnOrderChange={handleColumnOrderChange}
|
||||||
|
slotProps={{
|
||||||
|
columnMenu: {
|
||||||
|
slots: {
|
||||||
|
columnMenuColumnsItem: ConnectionColumnMenuColumnsItem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ConnectionColumnManager
|
||||||
|
open={columnManagerOpen}
|
||||||
|
columns={columnOptions}
|
||||||
|
onClose={onCloseColumnManager}
|
||||||
|
onToggle={handleToggleColumn}
|
||||||
|
onOrderChange={handleManagerOrderChange}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "إغلاق الاتصال"
|
"closeConnection": "إغلاق الاتصال"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "الأعمدة",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Verbindung schließen"
|
"closeConnection": "Verbindung schließen"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Spalten",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Close Connection"
|
"closeConnection": "Close Connection"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Columns",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Cerrar conexión"
|
"closeConnection": "Cerrar conexión"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Columnas",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "بستن اتصال"
|
"closeConnection": "بستن اتصال"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "ستونها",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Tutup Koneksi"
|
"closeConnection": "Tutup Koneksi"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Kolom",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "接続を閉じる"
|
"closeConnection": "接続を閉じる"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "列",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "연결 닫기"
|
"closeConnection": "연결 닫기"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "열",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Закрыть соединение"
|
"closeConnection": "Закрыть соединение"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Столбцы",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Bağlantıyı Kapat"
|
"closeConnection": "Bağlantıyı Kapat"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Sütunlar",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "Тоташуны ябу"
|
"closeConnection": "Тоташуны ябу"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "Баганалар",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "关闭连接"
|
"closeConnection": "关闭连接"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "列设置",
|
||||||
|
"dragHandle": "拖拽控件"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"closeConnection": "關閉連線"
|
"closeConnection": "關閉連線"
|
||||||
|
},
|
||||||
|
"columnManager": {
|
||||||
|
"title": "欄位設定",
|
||||||
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const ConnectionsPage = () => {
|
|||||||
|
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
||||||
|
const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false);
|
||||||
|
|
||||||
// 使用全局连接数据
|
// 使用全局连接数据
|
||||||
const displayData = useMemo(() => {
|
const displayData = useMemo(() => {
|
||||||
@@ -110,14 +111,17 @@ const ConnectionsPage = () => {
|
|||||||
|
|
||||||
const [filterConn] = useMemo(() => {
|
const [filterConn] = useMemo(() => {
|
||||||
const orderFunc = orderFunctionMap[curOrderOpt];
|
const orderFunc = orderFunctionMap[curOrderOpt];
|
||||||
let conns = displayData.connections?.filter((conn) => {
|
let conns: IConnectionsItem[] = (displayData.connections ?? []).filter(
|
||||||
const { host, destinationIP, process } = conn.metadata;
|
(conn) => {
|
||||||
return (
|
const { host, destinationIP, process } = conn.metadata;
|
||||||
match(host || "") || match(destinationIP || "") || match(process || "")
|
return (
|
||||||
);
|
match(host || "") ||
|
||||||
});
|
match(destinationIP || "") ||
|
||||||
|
match(process || "")
|
||||||
if (orderFunc) conns = orderFunc(conns ?? []);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (orderFunc) conns = orderFunc(conns);
|
||||||
|
|
||||||
return [conns];
|
return [conns];
|
||||||
}, [displayData, match, curOrderOpt]);
|
}, [displayData, match, curOrderOpt]);
|
||||||
@@ -145,6 +149,8 @@ const ConnectionsPage = () => {
|
|||||||
});
|
});
|
||||||
}, [connections]);
|
}, [connections]);
|
||||||
|
|
||||||
|
const hasTableData = filterConn.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
full
|
full
|
||||||
@@ -214,9 +220,10 @@ const ConnectionsPage = () => {
|
|||||||
pt: 1,
|
pt: 1,
|
||||||
mb: 0.5,
|
mb: 0.5,
|
||||||
mx: "10px",
|
mx: "10px",
|
||||||
height: "36px",
|
minHeight: "36px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -235,15 +242,29 @@ const ConnectionsPage = () => {
|
|||||||
))}
|
))}
|
||||||
</BaseStyledSelect>
|
</BaseStyledSelect>
|
||||||
)}
|
)}
|
||||||
<BaseSearchBox onSearch={handleSearch} />
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
"& > *": {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseSearchBox onSearch={handleSearch} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{!filterConn || filterConn.length === 0 ? (
|
{!hasTableData ? (
|
||||||
<BaseEmpty />
|
<BaseEmpty />
|
||||||
) : isTableLayout ? (
|
) : isTableLayout ? (
|
||||||
<ConnectionTable
|
<ConnectionTable
|
||||||
connections={filterConn}
|
connections={filterConn}
|
||||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||||
|
columnManagerOpen={isTableLayout && isColumnManagerOpen}
|
||||||
|
onOpenColumnManager={() => setIsColumnManagerOpen(true)}
|
||||||
|
onCloseColumnManager={() => setIsColumnManagerOpen(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export const translationKeys = [
|
|||||||
"connections.components.order.uploadSpeed",
|
"connections.components.order.uploadSpeed",
|
||||||
"connections.components.order.downloadSpeed",
|
"connections.components.order.downloadSpeed",
|
||||||
"connections.components.actions.closeConnection",
|
"connections.components.actions.closeConnection",
|
||||||
|
"connections.components.columnManager.title",
|
||||||
|
"connections.components.columnManager.dragHandle",
|
||||||
"home.page.tooltips.lightweightMode",
|
"home.page.tooltips.lightweightMode",
|
||||||
"home.page.tooltips.manual",
|
"home.page.tooltips.manual",
|
||||||
"home.page.tooltips.settings",
|
"home.page.tooltips.settings",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export interface TranslationResources {
|
|||||||
actions: {
|
actions: {
|
||||||
closeConnection: string;
|
closeConnection: string;
|
||||||
};
|
};
|
||||||
|
columnManager: {
|
||||||
|
dragHandle: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
fields: {
|
fields: {
|
||||||
chains: string;
|
chains: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user