@@ -51,12 +51,6 @@ export const Graph: React.FC<GraphProps> = ({
|
|||||||
setContextMenu({ x: event.clientX, y: event.clientY, node, link: null });
|
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) {
|
if (!data || data.nodes.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
|
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
|
||||||
@@ -86,7 +80,6 @@ export const Graph: React.FC<GraphProps> = ({
|
|||||||
ref={fgRef}
|
ref={fgRef}
|
||||||
data={data}
|
data={data}
|
||||||
onNodeRightClick={handleNodeRightClick}
|
onNodeRightClick={handleNodeRightClick}
|
||||||
onLinkRightClick={handleLinkRightClick}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GraphContextMenu
|
<GraphContextMenu
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import { useThemeStore } from "@/modules/theme-bw/stores/theme.store";
|
|||||||
interface ForceGraphProps {
|
interface ForceGraphProps {
|
||||||
data: GraphData;
|
data: GraphData;
|
||||||
onNodeRightClick: (node: GraphNode, event: MouseEvent) => void;
|
onNodeRightClick: (node: GraphNode, event: MouseEvent) => void;
|
||||||
onLinkRightClick: (link: GraphLink, event: MouseEvent) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ForceGraph = forwardRef<any, ForceGraphProps>(
|
export const ForceGraph = forwardRef<any, ForceGraphProps>(
|
||||||
({ data, onNodeRightClick, onLinkRightClick }, ref) => {
|
({ data, onNodeRightClick }, ref) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 480, height: 600 });
|
const [dimensions, setDimensions] = useState({ width: 480, height: 600 });
|
||||||
|
|
||||||
@@ -87,10 +86,35 @@ export const ForceGraph = forwardRef<any, ForceGraphProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getNodeColor = (node: GraphNode) => {
|
const getNodeColor = (node: GraphNode) => {
|
||||||
if (highlightNodes.has(node.id)) return "#fbbf24";
|
|
||||||
if (selectedNode?.id === node.id && isLinkMode) return "#f97316";
|
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") {
|
if (node.type === "agent") {
|
||||||
// Проверяем, есть ли у агента хотя бы один упавший сервис
|
// Проверяем, есть ли у агента хотя бы один упавший сервис
|
||||||
@@ -189,7 +213,6 @@ export const ForceGraph = forwardRef<any, ForceGraphProps>(
|
|||||||
linkDirectionalParticles={0}
|
linkDirectionalParticles={0}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeRightClick={onNodeRightClick}
|
onNodeRightClick={onNodeRightClick}
|
||||||
onLinkRightClick={onLinkRightClick}
|
|
||||||
onNodeHover={handleNodeHover}
|
onNodeHover={handleNodeHover}
|
||||||
cooldownTicks={50}
|
cooldownTicks={50}
|
||||||
cooldownTime={2000}
|
cooldownTime={2000}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FiLink, FiTrash2, FiMinusCircle } from "react-icons/fi";
|
import { FiLink, FiTrash2 } from "react-icons/fi";
|
||||||
import type {
|
import type { ContextMenuState, GraphNode, GraphData } from "../types";
|
||||||
ContextMenuState,
|
|
||||||
GraphNode,
|
|
||||||
GraphLink,
|
|
||||||
GraphData,
|
|
||||||
} from "../types";
|
|
||||||
import { useGraphStore } from "../store/useGraphStore";
|
import { useGraphStore } from "../store/useGraphStore";
|
||||||
|
|
||||||
interface GraphContextMenuProps {
|
interface GraphContextMenuProps {
|
||||||
@@ -20,7 +15,6 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const removeNode = useGraphStore((s) => s.removeNode);
|
const removeNode = useGraphStore((s) => s.removeNode);
|
||||||
const removeLink = useGraphStore((s) => s.removeLink);
|
|
||||||
const toggleLinkMode = useGraphStore((s) => s.toggleLinkMode);
|
const toggleLinkMode = useGraphStore((s) => s.toggleLinkMode);
|
||||||
const setSelectedNode = useGraphStore((s) => s.setSelectedNode);
|
const setSelectedNode = useGraphStore((s) => s.setSelectedNode);
|
||||||
|
|
||||||
@@ -31,11 +25,6 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLink = (link: GraphLink) => {
|
|
||||||
removeLink(link);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateLink = (node: GraphNode) => {
|
const handleCreateLink = (node: GraphNode) => {
|
||||||
toggleLinkMode();
|
toggleLinkMode();
|
||||||
setSelectedNode(node);
|
setSelectedNode(node);
|
||||||
@@ -92,40 +81,6 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{menu.link && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="px-3 py-1 text-xs border-b"
|
|
||||||
style={{
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
borderColor: "var(--border)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Связь:{" "}
|
|
||||||
{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}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteLink(menu.link!)}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm flex items-center gap-2"
|
|
||||||
style={{ color: "#f87171" }}
|
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "rgba(248,113,113,0.1)")
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.backgroundColor = "transparent")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FiMinusCircle size={14} /> Удалить связь
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export interface RunScriptResponse {
|
|||||||
wait_url: string;
|
wait_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateInterpreterPayload {
|
||||||
|
argv: string[];
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface JobWaitResponse {
|
export interface JobWaitResponse {
|
||||||
command: string[];
|
command: string[];
|
||||||
id: number;
|
id: number;
|
||||||
@@ -119,4 +125,14 @@ export const scriptsApi = {
|
|||||||
const res = await apiClient.post<JobWaitResponse>(`/jobs/${id}/wait`);
|
const res = await apiClient.post<JobWaitResponse>(`/jobs/${id}/wait`);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createInterpreter: async (
|
||||||
|
payload: CreateInterpreterPayload,
|
||||||
|
): Promise<Interpreter> => {
|
||||||
|
const res = await apiClient.post<Interpreter>(
|
||||||
|
"/scripts/interpreters",
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<AddInterpreterModalProps> = ({
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [label, setLabel] = useState("");
|
||||||
|
const [argv, setArgv] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const nameRef = useRef<HTMLInputElement>(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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 2000,
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card-bg)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "8px",
|
||||||
|
width: "420px",
|
||||||
|
maxWidth: "90vw",
|
||||||
|
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.4)",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 20px",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Interpreter
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "4px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdClose size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} style={{ padding: "20px" }}>
|
||||||
|
{/* Name */}
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginBottom: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Name <span style={{ color: "#f44747" }}>*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={nameRef}
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => 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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Label */}
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginBottom: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Label <span style={{ color: "#f44747" }}>*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => 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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Args */}
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginBottom: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Arguments <span style={{ color: "#858585" }}>(optional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={argv}
|
||||||
|
onChange={(e) => 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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "8px 12px",
|
||||||
|
backgroundColor: "rgba(244, 71, 71, 0.1)",
|
||||||
|
border: "1px solid #f44747",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "#f44747",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginBottom: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submit button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "10px",
|
||||||
|
backgroundColor: loading ? "#555" : "#0e639c",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: loading ? "not-allowed" : "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
animation: "spin 1s linear infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⏳
|
||||||
|
</span>
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MdAdd size={16} />
|
||||||
|
Add Interpreter
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FiEdit3 } from "react-icons/fi";
|
import { FiEdit3 } from "react-icons/fi";
|
||||||
|
import { MdAdd } from "react-icons/md";
|
||||||
import { FaSpinner } from "react-icons/fa";
|
import { FaSpinner } from "react-icons/fa";
|
||||||
import { FilePicker } from "../modules/ide";
|
import { FilePicker } from "../modules/ide";
|
||||||
import { RunScriptModal } from "../modules/ide/components/RunScriptModal";
|
import { RunScriptModal } from "../modules/ide/components/RunScriptModal";
|
||||||
|
import { AddInterpreterModal } from "../modules/ide/components/AddInterpreterModal";
|
||||||
import type { FileNode } from "../modules/ide";
|
import type { FileNode } from "../modules/ide";
|
||||||
import { scriptsApi } from "../modules/ide/api/scripts.api";
|
import { scriptsApi } from "../modules/ide/api/scripts.api";
|
||||||
|
|
||||||
@@ -47,8 +49,10 @@ export const TemplatesPage = () => {
|
|||||||
scriptPath: string;
|
scriptPath: string;
|
||||||
scriptId: number;
|
scriptId: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [showAddInterpreter, setShowAddInterpreter] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const reloadTree = () => {
|
||||||
|
setLoading(true);
|
||||||
scriptsApi
|
scriptsApi
|
||||||
.getTree()
|
.getTree()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -59,6 +63,10 @@ export const TemplatesPage = () => {
|
|||||||
setFiles({ name: "templates", type: "folder", children: [] });
|
setFiles({ name: "templates", type: "folder", children: [] });
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadTree();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRun = (path: string, id?: number) => {
|
const handleRun = (path: string, id?: number) => {
|
||||||
@@ -85,11 +93,40 @@ export const TemplatesPage = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
padding: "12px 16px",
|
padding: "12px 16px",
|
||||||
|
gap: "12px",
|
||||||
borderBottom: "1px solid var(--border)",
|
borderBottom: "1px solid var(--border)",
|
||||||
backgroundColor: "var(--card-bg)",
|
backgroundColor: "var(--card-bg)",
|
||||||
flexShrink: 0,
|
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 */}
|
{/* Open in Editor button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/ide")}
|
onClick={() => navigate("/ide")}
|
||||||
@@ -175,6 +212,14 @@ export const TemplatesPage = () => {
|
|||||||
onClose={() => setRunModal(null)}
|
onClose={() => setRunModal(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Add Interpreter Modal */}
|
||||||
|
{showAddInterpreter && (
|
||||||
|
<AddInterpreterModal
|
||||||
|
onClose={() => setShowAddInterpreter(false)}
|
||||||
|
onSuccess={reloadTree}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user