import useSWR from "swr"; import * as monaco from "monaco-editor"; import { configureMonacoYaml } from "monaco-yaml"; import { useCallback, useEffect, useRef, useState } from "react"; import { useApiHost } from "@/api"; import Heading from "@/components/ui/heading"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { Button } from "@/components/ui/button"; import axios from "axios"; import copy from "copy-to-clipboard"; import { useTheme } from "@/context/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import { LuCopy, LuSave } from "react-icons/lu"; import { MdOutlineRestartAlt } from "react-icons/md"; type SaveOptions = "saveonly" | "restart"; function ConfigEditor() { const apiHost = useApiHost(); useEffect(() => { document.title = "Config Editor - Frigate"; }, []); const { data: config } = useSWR("config/raw"); const { theme, systemTheme } = useTheme(); const [error, setError] = useState(); const editorRef = useRef(null); const modelRef = useRef(null); const configRef = useRef(null); const onHandleSaveConfig = useCallback( async (save_option: SaveOptions) => { if (!editorRef.current) { return; } axios .post( `config/save?save_option=${save_option}`, editorRef.current.getValue(), { headers: { "Content-Type": "text/plain" }, }, ) .then((response) => { if (response.status === 200) { setError(""); toast.success(response.data.message, { position: "top-center" }); } }) .catch((error) => { toast.error("Error saving config", { position: "top-center" }); if (error.response) { setError(error.response.data.message); } else { setError(error.message); } }); }, [editorRef], ); const handleCopyConfig = useCallback(async () => { if (!editorRef.current) { return; } copy(editorRef.current.getValue()); toast.success("Config copied to clipboard.", { position: "top-center" }); }, [editorRef]); useEffect(() => { if (!config) { return; } if (modelRef.current != null) { // we don't need to recreate the editor if it already exists editorRef.current?.layout(); return; } const modelUri = monaco.Uri.parse("a://b/api/config/schema.json"); if (monaco.editor.getModels().length > 0) { modelRef.current = monaco.editor.getModel(modelUri); } else { modelRef.current = monaco.editor.createModel(config, "yaml", modelUri); } configureMonacoYaml(monaco, { enableSchemaRequest: true, hover: true, completion: true, validate: true, format: true, schemas: [ { uri: `${apiHost}api/config/schema.json`, fileMatch: [String(modelUri)], }, ], }); const container = configRef.current; if (container != null) { editorRef.current = monaco.editor.create(container, { language: "yaml", model: modelRef.current, scrollBeyondLastLine: false, theme: (systemTheme || theme) == "dark" ? "vs-dark" : "vs-light", }); } return () => { configRef.current = null; modelRef.current = null; }; }); // monitoring state const [hasChanges, setHasChanges] = useState(false); useEffect(() => { if (!config || !modelRef.current) { return; } modelRef.current.onDidChangeContent(() => { if (modelRef.current?.getValue() != config) { setHasChanges(true); } else { setHasChanges(false); } }); }, [config]); useEffect(() => { if (config && modelRef.current) { modelRef.current.setValue(config); setHasChanges(false); } }, [config]); useEffect(() => { let listener: ((e: BeforeUnloadEvent) => void) | undefined; if (hasChanges) { listener = (e) => { e.preventDefault(); e.returnValue = true; return "Exit without saving?"; }; window.addEventListener("beforeunload", listener); } return () => { if (listener) { window.removeEventListener("beforeunload", listener); } }; }, [hasChanges]); if (!config) { return ; } return (
Config Editor
{error && (
{error}
)}
); } export default ConfigEditor;