mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 02:36:37 +08:00
@@ -117,6 +117,19 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
tunnel.setCreatedTime(currentTime);
|
tunnel.setCreatedTime(currentTime);
|
||||||
tunnel.setUpdatedTime(currentTime);
|
tunnel.setUpdatedTime(currentTime);
|
||||||
|
|
||||||
|
// When tunnels are ordered via `inx`, new tunnels should be appended.
|
||||||
|
// Only apply this when an order already exists (max `inx` > 0) to avoid
|
||||||
|
// changing behavior for deployments still relying on local ordering.
|
||||||
|
Tunnel lastByInx = this.getOne(new QueryWrapper<Tunnel>()
|
||||||
|
.select("inx")
|
||||||
|
.orderByDesc("inx")
|
||||||
|
.orderByDesc("id")
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
Integer maxInx = lastByInx == null ? null : lastByInx.getInx();
|
||||||
|
if (maxInx != null && maxInx > 0) {
|
||||||
|
tunnel.setInx(maxInx + 1);
|
||||||
|
}
|
||||||
if (StringUtils.isEmpty(tunnel.getInIp())){
|
if (StringUtils.isEmpty(tunnel.getInIp())){
|
||||||
java.util.LinkedHashSet<String> inIps = new java.util.LinkedHashSet<>();
|
java.util.LinkedHashSet<String> inIps = new java.util.LinkedHashSet<>();
|
||||||
for (ChainTunnel chainTunnel : tunnelDto.getInNodeId()) {
|
for (ChainTunnel chainTunnel : tunnelDto.getInNodeId()) {
|
||||||
|
|||||||
@@ -741,31 +741,45 @@ export default function DashboardPage() {
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={{ stroke: '#e5e7eb', strokeWidth: 1 }}
|
axisLine={{ stroke: '#e5e7eb', strokeWidth: 1 }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={{ stroke: '#e5e7eb', strokeWidth: 1 }}
|
axisLine={{ stroke: '#e5e7eb', strokeWidth: 1 }}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value: number | string) => {
|
||||||
if (value === 0) return '0';
|
const v = typeof value === 'number' ? value : Number(value);
|
||||||
if (value < 1024) return `${value}B`;
|
if (!Number.isFinite(v)) return String(value);
|
||||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}K`;
|
if (v === 0) return '0';
|
||||||
if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)}M`;
|
if (v < 1024) return `${v}B`;
|
||||||
return `${(value / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
if (v < 1024 * 1024) return `${(v / 1024).toFixed(1)}K`;
|
||||||
}}
|
if (v < 1024 * 1024 * 1024) return `${(v / (1024 * 1024)).toFixed(1)}M`;
|
||||||
/>
|
return `${(v / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
||||||
<Tooltip
|
}}
|
||||||
content={({ active, payload, label }) => {
|
/>
|
||||||
if (active && payload && payload.length) {
|
<Tooltip
|
||||||
return (
|
content={({
|
||||||
<div className="bg-white dark:bg-default-100 border border-default-200 rounded-lg shadow-lg p-3">
|
active,
|
||||||
<p className="font-medium text-foreground">{`时间: ${label}`}</p>
|
payload,
|
||||||
<p className="text-primary">
|
label,
|
||||||
{`流量: ${formatFlow(payload[0]?.value as number || 0)}`}
|
}: {
|
||||||
</p>
|
active?: boolean;
|
||||||
</div>
|
payload?: Array<{ value?: number | string }>;
|
||||||
);
|
label?: string | number;
|
||||||
}
|
}) => {
|
||||||
return null;
|
if (active && payload && payload.length) {
|
||||||
|
const firstValue = payload[0]?.value;
|
||||||
|
const numericValue = typeof firstValue === 'number' ? firstValue : Number(firstValue);
|
||||||
|
const flowValue = Number.isFinite(numericValue) ? numericValue : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-default-100 border border-default-200 rounded-lg shadow-lg p-3">
|
||||||
|
<p className="font-medium text-foreground">{`时间: ${label ?? ''}`}</p>
|
||||||
|
<p className="text-primary">
|
||||||
|
{`流量: ${formatFlow(flowValue)}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line
|
||||||
@@ -983,4 +997,4 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -353,8 +354,6 @@ export default function ForwardPage() {
|
|||||||
const userMap = new Map<string, UserGroup>();
|
const userMap = new Map<string, UserGroup>();
|
||||||
|
|
||||||
// 获取排序后的转发列表
|
// 获取排序后的转发列表
|
||||||
const sortedForwards = getSortedForwards();
|
|
||||||
|
|
||||||
sortedForwards.forEach(forward => {
|
sortedForwards.forEach(forward => {
|
||||||
const userKey = forward.userId ? forward.userId.toString() : 'unknown';
|
const userKey = forward.userId ? forward.userId.toString() : 'unknown';
|
||||||
const userName = forward.userName || '未知用户';
|
const userName = forward.userName || '未知用户';
|
||||||
@@ -855,7 +854,7 @@ export default function ForwardPage() {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 直接显示模式下,过滤指定隧道的转发
|
// 直接显示模式下,过滤指定隧道的转发
|
||||||
forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport);
|
forwardsToExport = sortedForwards.filter(forward => forward.tunnelId === selectedTunnelForExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forwardsToExport.length === 0) {
|
if (forwardsToExport.length === 0) {
|
||||||
@@ -1102,64 +1101,80 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
// 传感器配置 - 使用默认配置避免错误
|
// 传感器配置 - 使用默认配置避免错误
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tokenUserId = JwtUtil.getUserIdFromToken();
|
||||||
|
|
||||||
// 根据排序顺序获取转发列表
|
// 根据排序顺序获取转发列表
|
||||||
const getSortedForwards = (): Forward[] => {
|
const sortedForwards = useMemo((): Forward[] => {
|
||||||
// 确保 forwards 数组存在且有效
|
// 确保 forwards 数组存在且有效
|
||||||
if (!forwards || forwards.length === 0) {
|
if (!forwards || forwards.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在平铺模式下,只显示当前用户的转发
|
// 在平铺模式下,只显示当前用户的转发
|
||||||
let filteredForwards = forwards;
|
let filteredForwards = forwards;
|
||||||
if (viewMode === 'direct') {
|
if (viewMode === 'direct') {
|
||||||
const currentUserId = JwtUtil.getUserIdFromToken();
|
if (tokenUserId !== null) {
|
||||||
if (currentUserId !== null) {
|
filteredForwards = forwards.filter(forward => forward.userId === tokenUserId);
|
||||||
filteredForwards = forwards.filter(forward => forward.userId === currentUserId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保过滤后的转发列表有效
|
// 确保过滤后的转发列表有效
|
||||||
if (!filteredForwards || filteredForwards.length === 0) {
|
if (!filteredForwards || filteredForwards.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用数据库中的 inx 字段进行排序
|
// 优先使用数据库中的 inx 字段进行排序
|
||||||
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
const sortedByDb = [...filteredForwards].sort((a, b) => {
|
||||||
const aInx = a.inx ?? 0;
|
const aInx = a.inx ?? 0;
|
||||||
const bInx = b.inx ?? 0;
|
const bInx = b.inx ?? 0;
|
||||||
return aInx - bInx;
|
return aInx - bInx;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||||
if (forwardOrder && forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) {
|
if (forwardOrder && forwardOrder.length > 0 && sortedByDb.every(f => f.inx === undefined || f.inx === 0)) {
|
||||||
const forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
|
const forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
|
||||||
const localSortedForwards: Forward[] = [];
|
const localSortedForwards: Forward[] = [];
|
||||||
|
|
||||||
forwardOrder.forEach(id => {
|
forwardOrder.forEach(id => {
|
||||||
const forward = forwardMap.get(id);
|
const forward = forwardMap.get(id);
|
||||||
if (forward) {
|
if (forward) {
|
||||||
localSortedForwards.push(forward);
|
localSortedForwards.push(forward);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加不在排序列表中的转发(新添加的)
|
// 添加不在排序列表中的转发(新添加的)
|
||||||
filteredForwards.forEach(forward => {
|
filteredForwards.forEach(forward => {
|
||||||
if (!forwardOrder.includes(forward.id)) {
|
if (!forwardOrder.includes(forward.id)) {
|
||||||
localSortedForwards.push(forward);
|
localSortedForwards.push(forward);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return localSortedForwards;
|
return localSortedForwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedForwards;
|
return sortedByDb;
|
||||||
};
|
}, [forwards, forwardOrder, viewMode, tokenUserId]);
|
||||||
|
|
||||||
|
const sortableForwardIds = useMemo(
|
||||||
|
() => sortedForwards.map(f => f.id).filter(id => id > 0),
|
||||||
|
[sortedForwards]
|
||||||
|
);
|
||||||
|
|
||||||
// 可拖拽的转发卡片组件
|
// 可拖拽的转发卡片组件
|
||||||
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
||||||
@@ -1179,8 +1194,9 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||||
transition: transition || undefined,
|
transition: isDragging ? undefined : transition || undefined,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
willChange: 'transform',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1529,11 +1545,11 @@ export default function ForwardPage() {
|
|||||||
onDragStart={() => {}} // 添加空的 onDragStart 处理器
|
onDragStart={() => {}} // 添加空的 onDragStart 处理器
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={getSortedForwards().map(f => f.id || 0).filter(id => id > 0)}
|
items={sortableForwardIds}
|
||||||
strategy={rectSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{getSortedForwards().map((forward) => (
|
{sortedForwards.map((forward) => (
|
||||||
forward && forward.id ? (
|
forward && forward.id ? (
|
||||||
<SortableForwardCard key={forward.id} forward={forward} />
|
<SortableForwardCard key={forward.id} forward={forward} />
|
||||||
) : null
|
) : null
|
||||||
@@ -2394,4 +2410,4 @@ export default function ForwardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useMemo, useRef } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
@@ -16,7 +16,8 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@@ -102,8 +103,9 @@ const SortableItem = ({
|
|||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||||
transition: transition || undefined,
|
transition: isDragging ? undefined : transition || undefined,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
willChange: 'transform',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -806,24 +808,34 @@ export default function NodePage() {
|
|||||||
|
|
||||||
// 传感器配置
|
// 传感器配置
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 根据排序顺序获取节点列表
|
// 根据排序顺序获取节点列表
|
||||||
const getSortedNodes = (): Node[] => {
|
const sortedNodes = useMemo((): Node[] => {
|
||||||
if (!nodeList || nodeList.length === 0) return [];
|
if (!nodeList || nodeList.length === 0) return [];
|
||||||
|
|
||||||
const sortedNodes = [...nodeList].sort((a, b) => {
|
const sortedByDb = [...nodeList].sort((a, b) => {
|
||||||
const aInx = a.inx ?? 0;
|
const aInx = a.inx ?? 0;
|
||||||
const bInx = b.inx ?? 0;
|
const bInx = b.inx ?? 0;
|
||||||
return aInx - bInx;
|
return aInx - bInx;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||||
if (nodeOrder && nodeOrder.length > 0 && sortedNodes.every((n) => n.inx === undefined || n.inx === 0)) {
|
if (nodeOrder && nodeOrder.length > 0 && sortedByDb.every((n) => n.inx === undefined || n.inx === 0)) {
|
||||||
const nodeMap = new Map(nodeList.map((n) => [n.id, n] as const));
|
const nodeMap = new Map(nodeList.map((n) => [n.id, n] as const));
|
||||||
const localSorted: Node[] = [];
|
const localSorted: Node[] = [];
|
||||||
|
|
||||||
@@ -841,8 +853,10 @@ export default function NodePage() {
|
|||||||
return localSorted;
|
return localSorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedNodes;
|
return sortedByDb;
|
||||||
};
|
}, [nodeList, nodeOrder]);
|
||||||
|
|
||||||
|
const sortableNodeIds = useMemo(() => sortedNodes.map((n) => n.id), [sortedNodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
@@ -900,11 +914,11 @@ export default function NodePage() {
|
|||||||
) : (
|
) : (
|
||||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={getSortedNodes().map((n) => n.id)}
|
items={sortableNodeIds}
|
||||||
strategy={rectSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{getSortedNodes().map((node) => (
|
{sortedNodes.map((node) => (
|
||||||
<SortableItem key={node.id} id={node.id}>
|
<SortableItem key={node.id} id={node.id}>
|
||||||
{(listeners) => (
|
{(listeners) => (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Input, Textarea } from "@heroui/input";
|
import { Input, Textarea } from "@heroui/input";
|
||||||
@@ -13,7 +13,8 @@ import toast from 'react-hot-toast';
|
|||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@@ -586,24 +587,34 @@ export default function TunnelPage() {
|
|||||||
|
|
||||||
// 传感器配置
|
// 传感器配置
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 根据排序顺序获取隧道列表
|
// 根据排序顺序获取隧道列表
|
||||||
const getSortedTunnels = (): Tunnel[] => {
|
const sortedTunnels = useMemo((): Tunnel[] => {
|
||||||
if (!tunnels || tunnels.length === 0) return [];
|
if (!tunnels || tunnels.length === 0) return [];
|
||||||
|
|
||||||
const sortedTunnels = [...tunnels].sort((a, b) => {
|
const sortedByDb = [...tunnels].sort((a, b) => {
|
||||||
const aInx = a.inx ?? 0;
|
const aInx = a.inx ?? 0;
|
||||||
const bInx = b.inx ?? 0;
|
const bInx = b.inx ?? 0;
|
||||||
return aInx - bInx;
|
return aInx - bInx;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||||
if (tunnelOrder && tunnelOrder.length > 0 && sortedTunnels.every((t) => t.inx === undefined || t.inx === 0)) {
|
if (tunnelOrder && tunnelOrder.length > 0 && sortedByDb.every((t) => t.inx === undefined || t.inx === 0)) {
|
||||||
const tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const));
|
const tunnelMap = new Map(tunnels.map((t) => [t.id, t] as const));
|
||||||
const localSorted: Tunnel[] = [];
|
const localSorted: Tunnel[] = [];
|
||||||
|
|
||||||
@@ -621,8 +632,10 @@ export default function TunnelPage() {
|
|||||||
return localSorted;
|
return localSorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedTunnels;
|
return sortedByDb;
|
||||||
};
|
}, [tunnels, tunnelOrder]);
|
||||||
|
|
||||||
|
const sortableTunnelIds = useMemo(() => sortedTunnels.map((t) => t.id), [sortedTunnels]);
|
||||||
|
|
||||||
const SortableItem = ({
|
const SortableItem = ({
|
||||||
id,
|
id,
|
||||||
@@ -642,8 +655,9 @@ export default function TunnelPage() {
|
|||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||||
transition: transition || undefined,
|
transition: isDragging ? undefined : transition || undefined,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
willChange: 'transform',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -690,11 +704,11 @@ export default function TunnelPage() {
|
|||||||
{tunnels.length > 0 ? (
|
{tunnels.length > 0 ? (
|
||||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={getSortedTunnels().map((t) => t.id)}
|
items={sortableTunnelIds}
|
||||||
strategy={rectSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{getSortedTunnels().map((tunnel) => {
|
{sortedTunnels.map((tunnel) => {
|
||||||
const typeDisplay = getTypeDisplay(tunnel.type);
|
const typeDisplay = getTypeDisplay(tunnel.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
declare module 'recharts' {
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export const LineChart: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const Line: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const XAxis: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const YAxis: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const CartesianGrid: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const Tooltip: React.ComponentType<Record<string, unknown>>;
|
||||||
|
export const ResponsiveContainer: React.ComponentType<Record<string, unknown>>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user