diff --git a/frontend/.qwen/settings.json b/frontend/.qwen/settings.json index e8c96d2..e06f6ef 100644 --- a/frontend/.qwen/settings.json +++ b/frontend/.qwen/settings.json @@ -8,7 +8,8 @@ "Bash(dir)", "Bash(move *)", "Bash(findstr *)", - "Bash(del *)" + "Bash(del *)", + "Bash(mkdir *)" ] }, "$version": 3 diff --git a/frontend/src/app/providers/layout/store/agent.store.ts b/frontend/src/app/providers/layout/store/agent.store.ts index e39f16e..2cb8ebf 100644 --- a/frontend/src/app/providers/layout/store/agent.store.ts +++ b/frontend/src/app/providers/layout/store/agent.store.ts @@ -10,29 +10,8 @@ interface AgentState { removeAgent: (name: string) => void; } -const mockAgents: AgentInfo[] = [ - { - label: "agent-core-01", - token: "tok_a1b2c3d4e5f6g7h8", - services: ["postgres", "redis", "log-collector"], - connected_at: "2026-04-04 15:25:09", - }, - { - label: "agent-worker-02", - token: "tok_x9y8z7w6v5u4t3s2", - services: ["celery-worker", "flower"], - connected_at: "2026-04-04 15:25:09", - }, - { - label: "agent-monitor-03", - token: "tok_m1n2o3p4q5r6s7t8", - services: ["prometheus", "grafana", "alertmanager"], - connected_at: "2026-04-04 15:25:09", - }, -]; - export const useAgentStore = create()((set, get) => ({ - agents: mockAgents, + agents: [], isLoading: false, error: null, diff --git a/frontend/src/modules/ide/api/scripts.api.ts b/frontend/src/modules/ide/api/scripts.api.ts index c71fdc4..6059082 100644 --- a/frontend/src/modules/ide/api/scripts.api.ts +++ b/frontend/src/modules/ide/api/scripts.api.ts @@ -31,6 +31,26 @@ export interface UpdateScriptPayload { path: string; } +export interface RunScriptPayload { + stdin?: string; + token: string; +} + +export interface RunScriptResponse { + command: string[]; + id: number; + wait_url: string; +} + +export interface JobWaitResponse { + command: string[]; + id: number; + status: number; + stderr: string; + stdin: string; + stdout: string; +} + // apiClient уже имеет интерсептор для Authorization header export const scriptsApi = { getInterpreters: async (): Promise => { @@ -84,11 +104,19 @@ export const scriptsApi = { return res.data; }, - run: async (path: string): Promise<{ id: number; status: string }> => { - const res = await apiClient.post<{ id: number; status: string }>( - "/scripts/run", - { path }, + runScript: async ( + id: number, + payload: RunScriptPayload, + ): Promise => { + const res = await apiClient.post( + `/scripts/${id}/run`, + payload, ); return res.data; }, + + waitJob: async (id: number): Promise => { + const res = await apiClient.post(`/jobs/${id}/wait`); + return res.data; + }, }; diff --git a/frontend/src/modules/ide/components/FilePicker.tsx b/frontend/src/modules/ide/components/FilePicker.tsx index 944f866..1eb984f 100644 --- a/frontend/src/modules/ide/components/FilePicker.tsx +++ b/frontend/src/modules/ide/components/FilePicker.tsx @@ -14,12 +14,10 @@ const FilePickerTree: React.FC<{ onRun?: (path: string) => void; }> = ({ node, level, onRun }) => { const expandedFolders = useFilePickerStore((s) => s.expandedFolders); - const runningScripts = useFilePickerStore((s) => s.runningScripts); const toggleFolder = useFilePickerStore((s) => s.toggleFolder); const nodePath = node.path || node.name; const isExpanded = expandedFolders.has(nodePath); - const isRunning = node.type === "file" && runningScripts.has(nodePath); if (node.type === "file") { return ( @@ -27,7 +25,6 @@ const FilePickerTree: React.FC<{ name={node.name} type="file" path={nodePath} - isSelected={isRunning} level={level} onRun={onRun} /> diff --git a/frontend/src/modules/ide/components/FilePickerItem.tsx b/frontend/src/modules/ide/components/FilePickerItem.tsx index bf6479f..7a9d113 100644 --- a/frontend/src/modules/ide/components/FilePickerItem.tsx +++ b/frontend/src/modules/ide/components/FilePickerItem.tsx @@ -11,7 +11,6 @@ interface FilePickerItemProps { name: string; type: "file" | "folder"; path: string; - isSelected?: boolean; isExpanded?: boolean; children?: React.ReactNode; level: number; @@ -24,7 +23,6 @@ export const FilePickerItem: React.FC = ({ name, type, path, - isSelected, isExpanded, children, level, @@ -131,12 +129,10 @@ export const FilePickerItem: React.FC = ({ alignItems: "center", justifyContent: "center", padding: "4px", - backgroundColor: isSelected ? "#238636" : "transparent", - border: isSelected - ? "1px solid #2ea043" - : "1px solid transparent", + backgroundColor: "transparent", + border: "1px solid transparent", borderRadius: "3px", - color: isSelected ? "#ffffff" : "var(--text-secondary)", + color: "var(--text-secondary)", cursor: "pointer", flexShrink: 0, transition: "all 0.15s", @@ -151,15 +147,9 @@ export const FilePickerItem: React.FC = ({ e.currentTarget.style.borderColor = "#2ea043"; }} onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = isSelected - ? "#238636" - : "transparent"; - e.currentTarget.style.color = isSelected - ? "#ffffff" - : "var(--text-secondary)"; - e.currentTarget.style.borderColor = isSelected - ? "#2ea043" - : "transparent"; + e.currentTarget.style.backgroundColor = "transparent"; + e.currentTarget.style.color = "var(--text-secondary)"; + e.currentTarget.style.borderColor = "transparent"; }} title="Run script" > diff --git a/frontend/src/modules/ide/components/RunScriptModal.tsx b/frontend/src/modules/ide/components/RunScriptModal.tsx new file mode 100644 index 0000000..d68d6b3 --- /dev/null +++ b/frontend/src/modules/ide/components/RunScriptModal.tsx @@ -0,0 +1,309 @@ +import React, { useState, useRef, useEffect } from "react"; +import { MdClose } from "react-icons/md"; +import { scriptsApi } from "../api/scripts.api"; +import { useTerminalStore } from "@/modules/terminal/store/useTerminalStore"; +import { useAgentStore } from "@/app/providers/layout/store/agent.store"; + +interface RunScriptModalProps { + scriptPath: string; + scriptId: number; + onClose: () => void; +} + +export const RunScriptModal: React.FC = ({ + scriptPath, + scriptId, + onClose, +}) => { + const [selectedAgentIdx, setSelectedAgentIdx] = useState(0); + const [stdinValue, setStdinValue] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(null); + + const agents = useAgentStore((s) => s.agents); + const addJob = useTerminalStore((s) => s.addJob); + const openTerminal = useTerminalStore((s) => s.openTerminal); + + const selectedAgent = agents[selectedAgentIdx]; + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleRun = async () => { + if (!selectedAgent) { + setError("No agents available"); + return; + } + + setLoading(true); + setError(null); + + try { + // 1. Запускаем скрипт + const runResult = await scriptsApi.runScript(scriptId, { + stdin: stdinValue, + token: selectedAgent.token, + }); + + // 2. Добавляем джоб в терминал + addJob({ + id: runResult.id, + scriptPath, + command: runResult.command, + }); + + // 3. Открываем терминал + openTerminal(); + + // 4. Ждём завершения по id + const jobResult = await scriptsApi.waitJob(runResult.id); + + // 5. Обновляем джоб + addJob({ + id: jobResult.id, + scriptPath, + command: jobResult.command, + stdin: jobResult.stdin, + }); + + // Обновляем с финальным статусом + const terminalStore = useTerminalStore.getState(); + terminalStore.updateJob(jobResult.id, { + status: jobResult.status, + stdout: jobResult.stdout, + stderr: jobResult.stderr, + stdin: jobResult.stdin, + isRunning: false, + }); + + onClose(); + } catch (e: any) { + console.error("Failed to run script:", e); + setError(e?.response?.data?.detail || "Failed to run script"); + } finally { + setLoading(false); + } + }; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ Run Script +

+ +
+ + {/* Content */} +
+ {/* Script path */} +
+ +
+ {scriptPath} +
+
+ + {/* Agent selector */} +
+ + +
+ + {/* Stdin (optional) */} +
+ +