231 lines
6.6 KiB
TypeScript
231 lines
6.6 KiB
TypeScript
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";
|
||
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||
|
||
const convertTreeToFileNode = (data: any[]): FileNode => {
|
||
const convertItem = (item: any): FileNode => {
|
||
const node: FileNode = {
|
||
id: item.id,
|
||
name: item.name,
|
||
type: item.type === "folder" ? "folder" : "file",
|
||
content: item.content || "",
|
||
path: item.name,
|
||
interpreter_id: item.interpreter_id,
|
||
};
|
||
|
||
if (item.type === "folder") {
|
||
node.children = [];
|
||
if (item.children && Array.isArray(item.children)) {
|
||
node.children = item.children.map((child: any) => {
|
||
const childNode = convertItem(child);
|
||
childNode.path = `${item.name}/${child.name}`;
|
||
return childNode;
|
||
});
|
||
}
|
||
}
|
||
|
||
return node;
|
||
};
|
||
|
||
return {
|
||
name: "templates",
|
||
type: "folder",
|
||
children: data.map((item) => convertItem(item)),
|
||
};
|
||
};
|
||
|
||
export const TemplatesPage = () => {
|
||
const navigate = useNavigate();
|
||
const { user } = useAuthStore();
|
||
const canManageAgent = user?.permission_manage_agent;
|
||
const [files, setFiles] = useState<FileNode | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [runModal, setRunModal] = useState<{
|
||
scriptPath: string;
|
||
scriptId: number;
|
||
} | null>(null);
|
||
const [showAddInterpreter, setShowAddInterpreter] = useState(false);
|
||
|
||
const reloadTree = () => {
|
||
setLoading(true);
|
||
scriptsApi
|
||
.getTree()
|
||
.then((data) => {
|
||
setFiles(convertTreeToFileNode(data));
|
||
})
|
||
.catch((e) => {
|
||
console.error("Failed to load tree:", e);
|
||
setFiles({ name: "templates", type: "folder", children: [] });
|
||
})
|
||
.finally(() => setLoading(false));
|
||
};
|
||
|
||
useEffect(() => {
|
||
reloadTree();
|
||
}, []);
|
||
|
||
const handleRun = (path: string, id?: number) => {
|
||
if (!id) {
|
||
console.warn("Script ID not found for:", path);
|
||
return;
|
||
}
|
||
setRunModal({ scriptPath: path, scriptId: id });
|
||
};
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
height: "100vh",
|
||
backgroundColor: "var(--bg-primary)",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
{/* Header bar */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
padding: "12px 16px",
|
||
gap: "12px",
|
||
borderBottom: "1px solid var(--border)",
|
||
backgroundColor: "var(--card-bg)",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{/* Add Interpreter button */}
|
||
<button
|
||
onClick={() => setShowAddInterpreter(true)}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "6px",
|
||
padding: "6px 14px",
|
||
backgroundColor: "#238636",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
color: "#ffffff",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
fontWeight: 500,
|
||
transition: "all 0.15s",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.backgroundColor = "#2ea043";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.backgroundColor = "#238636";
|
||
}}
|
||
>
|
||
<MdAdd size={14} />
|
||
Add Interpreter
|
||
</button>
|
||
|
||
{/* Open in Editor button — только с правом manage_agent */}
|
||
{canManageAgent && (
|
||
<button
|
||
onClick={() => navigate("/ide")}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "8px",
|
||
padding: "6px 16px",
|
||
backgroundColor: "#0e639c",
|
||
border: "none",
|
||
borderRadius: "4px",
|
||
color: "#ffffff",
|
||
cursor: "pointer",
|
||
fontSize: "12px",
|
||
fontWeight: 500,
|
||
transition: "all 0.15s",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.backgroundColor = "#1177bb";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.backgroundColor = "#0e639c";
|
||
}}
|
||
>
|
||
<FiEdit3 size={14} />
|
||
Open Editor
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* File Picker (terminal встроен внутрь) */}
|
||
<div style={{ flex: 1, overflow: "hidden" }}>
|
||
{loading ? (
|
||
<div
|
||
style={{
|
||
height: "100%",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
>
|
||
<FaSpinner
|
||
size={24}
|
||
style={{
|
||
color: "var(--accent)",
|
||
animation: "spin 1s linear infinite",
|
||
}}
|
||
/>
|
||
</div>
|
||
) : files ? (
|
||
<FilePicker
|
||
files={files}
|
||
onRun={(path) => {
|
||
// Находим ID скрипта по пути
|
||
const findNodeById = (
|
||
node: FileNode,
|
||
p: string,
|
||
): FileNode | null => {
|
||
if (node.path === p) return node;
|
||
if (node.children) {
|
||
for (const child of node.children) {
|
||
const found = findNodeById(child, p);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const node = findNodeById(files, path);
|
||
if (node?.id) {
|
||
handleRun(path, node.id);
|
||
} else {
|
||
console.warn("Script ID not found for path:", path);
|
||
}
|
||
}}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* Run Script Modal */}
|
||
{runModal && (
|
||
<RunScriptModal
|
||
scriptPath={runModal.scriptPath}
|
||
scriptId={runModal.scriptId}
|
||
onClose={() => setRunModal(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Add Interpreter Modal */}
|
||
{showAddInterpreter && (
|
||
<AddInterpreterModal
|
||
onClose={() => setShowAddInterpreter(false)}
|
||
onSuccess={reloadTree}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|