Skip to content

Commit 40b85fe

Browse files
committed
feat: 隧道实例增加编辑功能
1 parent a5b0c4c commit 40b85fe

2 files changed

Lines changed: 120 additions & 14 deletions

File tree

app/tunnels/page.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { TunnelToolBox } from "./components/toolbox";
3636
import { useTunnelActions } from "@/lib/hooks/use-tunnel-actions";
3737
import { addToast } from "@heroui/toast";
3838
import { buildApiUrl } from '@/lib/utils';
39+
import QuickCreateTunnelModal from "./components/quick-create-tunnel-modal";
3940

4041
// 定义实例类型
4142
interface Tunnel {
@@ -89,6 +90,10 @@ export default function TunnelsPage() {
8990
// 是否移入回收站
9091
const [moveToRecycle, setMoveToRecycle] = useState(false);
9192

93+
// 编辑模态控制
94+
const [editModalOpen,setEditModalOpen]=useState(false);
95+
const [editTunnel,setEditTunnel]=useState<Tunnel|null>(null);
96+
9297
// 获取实例列表
9398
const fetchTunnels = async () => {
9499
try {
@@ -286,6 +291,9 @@ export default function TunnelsPage() {
286291
setEditModalTunnel(tunnel);
287292
setNewTunnelName(tunnel.name);
288293
setIsEditModalOpen(true);
294+
// open quick modal for comprehensive edit
295+
setEditTunnel(tunnel);
296+
setEditModalOpen(true);
289297
};
290298

291299
const handleEditSubmit = async () => {
@@ -470,6 +478,14 @@ export default function TunnelsPage() {
470478
isDisabled={tunnel.status.type !== "success"}
471479
startContent={<FontAwesomeIcon icon={faRotateRight} className="text-xs" />}
472480
/>
481+
<Button
482+
isIconOnly
483+
variant="light"
484+
size="sm"
485+
color="default"
486+
onClick={()=>{ setEditTunnel(tunnel); setEditModalOpen(true);} }
487+
startContent={<FontAwesomeIcon icon={faPen} className="text-xs" />}
488+
/>
473489
<Button
474490
isIconOnly
475491
variant="light"
@@ -654,6 +670,14 @@ export default function TunnelsPage() {
654670
isDisabled={tunnel.status.type !== "success"}
655671
startContent={<FontAwesomeIcon icon={faRotateRight} className="text-xs" />}
656672
/>
673+
<Button
674+
isIconOnly
675+
variant="light"
676+
size="sm"
677+
color="default"
678+
onClick={()=>{ setEditTunnel(tunnel); setEditModalOpen(true);} }
679+
startContent={<FontAwesomeIcon icon={faPen} className="text-xs" />}
680+
/>
657681
<Button
658682
isIconOnly
659683
variant="light"
@@ -683,7 +707,7 @@ export default function TunnelsPage() {
683707
{(column) => (
684708
<TableColumn
685709
key={column.key}
686-
hideHeader={column.key === "actions"}
710+
hideHeader={false}
687711
align={column.key === "actions" ? "center" : "start"}
688712
>
689713
{column.label}
@@ -883,6 +907,17 @@ export default function TunnelsPage() {
883907
)}
884908
</ModalContent>
885909
</Modal>
910+
911+
{/* Quick Edit Modal */}
912+
{editModalOpen && editTunnel && (
913+
<QuickCreateTunnelModal
914+
isOpen={editModalOpen}
915+
onOpenChange={(open)=>setEditModalOpen(open)}
916+
mode="edit"
917+
editData={editTunnel as any}
918+
onSaved={()=>{ setEditModalOpen(false); fetchTunnels(); }}
919+
/>
920+
)}
886921
</>
887922
);
888923
}

internal/api/tunnel.go

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,24 @@ func (h *TunnelHandler) HandleUpdateTunnel(w http.ResponseWriter, r *http.Reques
304304
return
305305
}
306306

307-
var req tunnel.UpdateTunnelRequest
308-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
307+
// 尝试解析为创建/替换请求体(与创建接口保持一致)
308+
var rawCreate struct {
309+
Name string `json:"name"`
310+
EndpointID int64 `json:"endpointId"`
311+
Mode string `json:"mode"`
312+
TunnelAddress string `json:"tunnelAddress"`
313+
TunnelPort json.RawMessage `json:"tunnelPort"`
314+
TargetAddress string `json:"targetAddress"`
315+
TargetPort json.RawMessage `json:"targetPort"`
316+
TLSMode string `json:"tlsMode"`
317+
CertPath string `json:"certPath"`
318+
KeyPath string `json:"keyPath"`
319+
LogLevel string `json:"logLevel"`
320+
Min json.RawMessage `json:"min"`
321+
Max json.RawMessage `json:"max"`
322+
}
323+
324+
if err := json.NewDecoder(r.Body).Decode(&rawCreate); err != nil {
309325
w.WriteHeader(http.StatusBadRequest)
310326
json.NewEncoder(w).Encode(tunnel.TunnelResponse{
311327
Success: false,
@@ -314,21 +330,76 @@ func (h *TunnelHandler) HandleUpdateTunnel(w http.ResponseWriter, r *http.Reques
314330
return
315331
}
316332

317-
req.ID = tunnelID
333+
// 如果请求体包含 EndpointID 和 Mode,则认定为"替换"逻辑,否则执行原 Update 逻辑
334+
if rawCreate.EndpointID != 0 && rawCreate.Mode != "" {
335+
// 1. 获取旧 instanceId
336+
instanceID, err := h.tunnelService.GetInstanceIDByTunnelID(tunnelID)
337+
if err != nil {
338+
w.WriteHeader(http.StatusBadRequest)
339+
json.NewEncoder(w).Encode(tunnel.TunnelResponse{Success: false, Error: err.Error()})
340+
return
341+
}
318342

319-
if err := h.tunnelService.UpdateTunnel(req); err != nil {
320-
w.WriteHeader(http.StatusBadRequest)
321-
json.NewEncoder(w).Encode(tunnel.TunnelResponse{
322-
Success: false,
323-
Error: err.Error(),
324-
})
343+
// 2. 删除旧实例(回收站=true)
344+
if err := h.tunnelService.DeleteTunnelAndWait(instanceID, 3*time.Second, true); err != nil {
345+
w.WriteHeader(http.StatusBadRequest)
346+
json.NewEncoder(w).Encode(tunnel.TunnelResponse{Success: false, Error: "编辑实例失败,遭遇无法删除旧实例: " + err.Error()})
347+
return
348+
}
349+
log.Infof("[API.%v] 编辑实例=>删除旧实例: %v", rawCreate.EndpointID, instanceID)
350+
351+
// 工具函数解析 int 字段
352+
parseInt := func(j json.RawMessage) (int, error) {
353+
if j == nil {
354+
return 0, nil
355+
}
356+
var i int
357+
if err := json.Unmarshal(j, &i); err == nil {
358+
return i, nil
359+
}
360+
var s string
361+
if err := json.Unmarshal(j, &s); err == nil {
362+
return strconv.Atoi(s)
363+
}
364+
return 0, strconv.ErrSyntax
365+
}
366+
367+
tunnelPort, _ := parseInt(rawCreate.TunnelPort)
368+
targetPort, _ := parseInt(rawCreate.TargetPort)
369+
minVal, _ := parseInt(rawCreate.Min)
370+
maxVal, _ := parseInt(rawCreate.Max)
371+
372+
createReq := tunnel.CreateTunnelRequest{
373+
Name: rawCreate.Name,
374+
EndpointID: rawCreate.EndpointID,
375+
Mode: rawCreate.Mode,
376+
TunnelAddress: rawCreate.TunnelAddress,
377+
TunnelPort: tunnelPort,
378+
TargetAddress: rawCreate.TargetAddress,
379+
TargetPort: targetPort,
380+
TLSMode: tunnel.TLSMode(rawCreate.TLSMode),
381+
CertPath: rawCreate.CertPath,
382+
KeyPath: rawCreate.KeyPath,
383+
LogLevel: tunnel.LogLevel(rawCreate.LogLevel),
384+
Min: minVal,
385+
Max: maxVal,
386+
}
387+
388+
newTunnel, err := h.tunnelService.CreateTunnel(createReq)
389+
if err != nil {
390+
w.WriteHeader(http.StatusBadRequest)
391+
json.NewEncoder(w).Encode(tunnel.TunnelResponse{Success: false, Error: "编辑实例失败,无法创建新实例: " + err.Error()})
392+
return
393+
}
394+
log.Infof("[API.%v] 编辑实例=>创建新实例: %v", rawCreate.EndpointID, newTunnel.InstanceID)
395+
396+
json.NewEncoder(w).Encode(tunnel.TunnelResponse{Success: true, Message: "编辑实例成功", Tunnel: newTunnel})
325397
return
326398
}
327399

328-
json.NewEncoder(w).Encode(tunnel.TunnelResponse{
329-
Success: true,
330-
Message: "隧道更新成功",
331-
})
400+
// -------- 原局部更新逻辑 ----------
401+
w.WriteHeader(http.StatusBadRequest)
402+
json.NewEncoder(w).Encode(tunnel.TunnelResponse{Success: false, Error: "不支持的更新请求"})
332403
}
333404

334405
// HandleGetTunnelLogs GET /api/tunnel-logs

0 commit comments

Comments
 (0)