From 255fe2eaf33da85a2efa9da2d1a71bc802db6341 Mon Sep 17 00:00:00 2001 From: nikita Date: Sun, 5 Apr 2026 10:14:53 +0300 Subject: [PATCH] fix --- frontend/src/modules/graph/Graph.tsx | 7 - .../modules/graph/components/ForceGraph.tsx | 33 ++- .../graph/components/GraphContextMenu.tsx | 49 +--- frontend/src/modules/ide/api/scripts.api.ts | 16 + .../ide/components/AddInterpreterModal.tsx | 276 ++++++++++++++++++ frontend/src/pages/templates.page.tsx | 47 ++- 6 files changed, 368 insertions(+), 60 deletions(-) create mode 100644 frontend/src/modules/ide/components/AddInterpreterModal.tsx diff --git a/frontend/src/modules/graph/Graph.tsx b/frontend/src/modules/graph/Graph.tsx index 18e0442..40ffbe0 100644 --- a/frontend/src/modules/graph/Graph.tsx +++ b/frontend/src/modules/graph/Graph.tsx @@ -51,12 +51,6 @@ export const Graph: React.FC = ({ setContextMenu({ x: event.clientX, y: event.clientY, node, link: null }); }; - const handleLinkRightClick = (link: GraphLink, event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - setContextMenu({ x: event.clientX, y: event.clientY, node: null, link }); - }; - if (!data || data.nodes.length === 0) { return (
@@ -86,7 +80,6 @@ export const Graph: React.FC = ({ ref={fgRef} data={data} onNodeRightClick={handleNodeRightClick} - onLinkRightClick={handleLinkRightClick} /> void; - onLinkRightClick: (link: GraphLink, event: MouseEvent) => void; } export const ForceGraph = forwardRef( - ({ data, onNodeRightClick, onLinkRightClick }, ref) => { + ({ data, onNodeRightClick }, ref) => { const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 480, height: 600 }); @@ -87,10 +86,35 @@ export const ForceGraph = forwardRef( }; const getNodeColor = (node: GraphNode) => { - if (highlightNodes.has(node.id)) return "#fbbf24"; if (selectedNode?.id === node.id && isLinkMode) return "#f97316"; - if (node.type === "service" && node.status === "down") return "#ef4444"; + if (node.type === "service" && node.status === "down") { + // Проверяем, есть ли зависимости этого сервиса, которые тоже упали + const hasDownDependency = data.links.some((link) => { + const sourceId = + typeof link.source === "object" + ? (link.source as any).id + : link.source; + const targetId = + typeof link.target === "object" + ? (link.target as any).id + : link.target; + + if (sourceId !== node.id) return false; + + const isDependency = + link.type === "dependency" || link.type === "started"; + const targetIsDown = data.nodes.some( + (n) => n.id === targetId && n.status === "down", + ); + + return isDependency && targetIsDown; + }); + + // Если есть упавшая зависимость — не подсвечиваем красным + if (hasDownDependency) return "#3b82f6"; + return "#ef4444"; + } if (node.type === "agent") { // Проверяем, есть ли у агента хотя бы один упавший сервис @@ -189,7 +213,6 @@ export const ForceGraph = forwardRef( linkDirectionalParticles={0} onNodeClick={handleNodeClick} onNodeRightClick={onNodeRightClick} - onLinkRightClick={onLinkRightClick} onNodeHover={handleNodeHover} cooldownTicks={50} cooldownTime={2000} diff --git a/frontend/src/modules/graph/components/GraphContextMenu.tsx b/frontend/src/modules/graph/components/GraphContextMenu.tsx index 467384e..46704ed 100644 --- a/frontend/src/modules/graph/components/GraphContextMenu.tsx +++ b/frontend/src/modules/graph/components/GraphContextMenu.tsx @@ -1,11 +1,6 @@ import React from "react"; -import { FiLink, FiTrash2, FiMinusCircle } from "react-icons/fi"; -import type { - ContextMenuState, - GraphNode, - GraphLink, - GraphData, -} from "../types"; +import { FiLink, FiTrash2 } from "react-icons/fi"; +import type { ContextMenuState, GraphNode, GraphData } from "../types"; import { useGraphStore } from "../store/useGraphStore"; interface GraphContextMenuProps { @@ -20,7 +15,6 @@ export const GraphContextMenu: React.FC = ({ onClose, }) => { const removeNode = useGraphStore((s) => s.removeNode); - const removeLink = useGraphStore((s) => s.removeLink); const toggleLinkMode = useGraphStore((s) => s.toggleLinkMode); const setSelectedNode = useGraphStore((s) => s.setSelectedNode); @@ -31,11 +25,6 @@ export const GraphContextMenu: React.FC = ({ onClose(); }; - const handleDeleteLink = (link: GraphLink) => { - removeLink(link); - onClose(); - }; - const handleCreateLink = (node: GraphNode) => { toggleLinkMode(); setSelectedNode(node); @@ -92,40 +81,6 @@ export const GraphContextMenu: React.FC = ({ )} - {menu.link && ( - <> -
- Связь:{" "} - {typeof menu.link.source === "string" - ? menu.link.source - : (menu.link.source as any).name || - (menu.link.source as any).id}{" "} - →{" "} - {typeof menu.link.target === "string" - ? menu.link.target - : (menu.link.target as any).name || (menu.link.target as any).id} -
- - - )}
); }; diff --git a/frontend/src/modules/ide/api/scripts.api.ts b/frontend/src/modules/ide/api/scripts.api.ts index 6059082..480d404 100644 --- a/frontend/src/modules/ide/api/scripts.api.ts +++ b/frontend/src/modules/ide/api/scripts.api.ts @@ -42,6 +42,12 @@ export interface RunScriptResponse { wait_url: string; } +export interface CreateInterpreterPayload { + argv: string[]; + label: string; + name: string; +} + export interface JobWaitResponse { command: string[]; id: number; @@ -119,4 +125,14 @@ export const scriptsApi = { const res = await apiClient.post(`/jobs/${id}/wait`); return res.data; }, + + createInterpreter: async ( + payload: CreateInterpreterPayload, + ): Promise => { + const res = await apiClient.post( + "/scripts/interpreters", + payload, + ); + return res.data; + }, }; diff --git a/frontend/src/modules/ide/components/AddInterpreterModal.tsx b/frontend/src/modules/ide/components/AddInterpreterModal.tsx new file mode 100644 index 0000000..99948aa --- /dev/null +++ b/frontend/src/modules/ide/components/AddInterpreterModal.tsx @@ -0,0 +1,276 @@ +import React, { useState, useRef } from "react"; +import { MdClose, MdAdd } from "react-icons/md"; +import { scriptsApi } from "../api/scripts.api"; +import type { CreateInterpreterPayload } from "../api/scripts.api"; + +interface AddInterpreterModalProps { + onClose: () => void; + onSuccess: () => void; +} + +export const AddInterpreterModal: React.FC = ({ + onClose, + onSuccess, +}) => { + const [name, setName] = useState(""); + const [label, setLabel] = useState(""); + const [argv, setArgv] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const nameRef = useRef(null); + + React.useEffect(() => { + nameRef.current?.focus(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim() || !label.trim()) { + setError("Name and Label are required"); + return; + } + + setLoading(true); + setError(null); + + try { + const payload: CreateInterpreterPayload = { + name: name.trim(), + label: label.trim(), + argv: argv + .split(" ") + .map((s) => s.trim()) + .filter(Boolean), + }; + + await scriptsApi.createInterpreter(payload); + onSuccess(); + onClose(); + } catch (e: any) { + console.error("Failed to create interpreter:", e); + setError(e?.response?.data?.detail || "Failed to create interpreter"); + } finally { + setLoading(false); + } + }; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ Add Interpreter +

+ +
+ + {/* Form */} +
+ {/* Name */} +
+ + setName(e.target.value)} + placeholder="Python, Node.js, etc." + style={{ + width: "100%", + padding: "8px 12px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "4px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + boxSizing: "border-box", + }} + /> +
+ + {/* Label */} +
+ + setLabel(e.target.value)} + placeholder="python3, node, etc." + style={{ + width: "100%", + padding: "8px 12px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "4px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + boxSizing: "border-box", + }} + /> +
+ + {/* Args */} +
+ + setArgv(e.target.value)} + placeholder="-u -O (space separated)" + style={{ + width: "100%", + padding: "8px 12px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "4px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + boxSizing: "border-box", + }} + /> +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Submit button */} + +
+
+
+ ); +}; diff --git a/frontend/src/pages/templates.page.tsx b/frontend/src/pages/templates.page.tsx index b90d6d8..9b9150b 100644 --- a/frontend/src/pages/templates.page.tsx +++ b/frontend/src/pages/templates.page.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { FiEdit3 } from "react-icons/fi"; +import { MdAdd } from "react-icons/md"; import { FaSpinner } from "react-icons/fa"; import { FilePicker } from "../modules/ide"; import { RunScriptModal } from "../modules/ide/components/RunScriptModal"; +import { AddInterpreterModal } from "../modules/ide/components/AddInterpreterModal"; import type { FileNode } from "../modules/ide"; import { scriptsApi } from "../modules/ide/api/scripts.api"; @@ -47,8 +49,10 @@ export const TemplatesPage = () => { scriptPath: string; scriptId: number; } | null>(null); + const [showAddInterpreter, setShowAddInterpreter] = useState(false); - useEffect(() => { + const reloadTree = () => { + setLoading(true); scriptsApi .getTree() .then((data) => { @@ -59,6 +63,10 @@ export const TemplatesPage = () => { setFiles({ name: "templates", type: "folder", children: [] }); }) .finally(() => setLoading(false)); + }; + + useEffect(() => { + reloadTree(); }, []); const handleRun = (path: string, id?: number) => { @@ -85,11 +93,40 @@ export const TemplatesPage = () => { alignItems: "center", justifyContent: "flex-end", padding: "12px 16px", + gap: "12px", borderBottom: "1px solid var(--border)", backgroundColor: "var(--card-bg)", flexShrink: 0, }} > + {/* Add Interpreter button */} + + {/* Open in Editor button */}