forked from Github/frigate
Add config editor to webUI (#4608)
* Add raw config endpoint * Add config editor * Add code editor * Add error * Add ability to copy config * Only show the save button when code has been edited * Update errors * Remove debug config from system page * Break out config saving steps to pinpoint where error occurred. * Show correct config errors * Switch to monaco editor * Adjust UI colors and behavior * Get yaml validation working * Set success color
This commit is contained in:
@@ -44,8 +44,10 @@ export default function Sidebar() {
|
||||
</Match>
|
||||
{birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
|
||||
<Destination href="/events" text="Events" />
|
||||
<Separator />
|
||||
<Destination href="/storage" text="Storage" />
|
||||
<Destination href="/system" text="System" />
|
||||
<Destination href="/config" text="Config" />
|
||||
<Separator />
|
||||
<div className="flex flex-grow" />
|
||||
{ENV !== 'production' ? (
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function App() {
|
||||
/>
|
||||
<AsyncRoute path="/storage" getComponent={Routes.getStorage} />
|
||||
<AsyncRoute path="/system" getComponent={Routes.getSystem} />
|
||||
<AsyncRoute path="/config" getComponent={Routes.getConfig} />
|
||||
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
|
||||
<Cameras default path="/" />
|
||||
</Router>
|
||||
|
||||
99
web/src/routes/Config.jsx
Normal file
99
web/src/routes/Config.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { h } from 'preact';
|
||||
import useSWR from 'swr';
|
||||
import axios from 'axios';
|
||||
import { useApiHost } from '../api';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Heading from '../components/Heading';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import Button from '../components/Button';
|
||||
import { editor, Uri } from 'monaco-editor';
|
||||
import { setDiagnosticsOptions } from 'monaco-yaml';
|
||||
|
||||
export default function Config() {
|
||||
const apiHost = useApiHost();
|
||||
|
||||
const { data: config } = useSWR('config/raw');
|
||||
const [success, setSuccess] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onHandleSaveConfig = async (e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
axios
|
||||
.post('config/save', window.editor.getValue(), {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setSuccess(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
setError(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyConfig = async () => {
|
||||
await window.navigator.clipboard.writeText(window.editor.getValue());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelUri = Uri.parse('a://b/api/config/schema.json');
|
||||
|
||||
setDiagnosticsOptions({
|
||||
enableSchemaRequest: true,
|
||||
hover: true,
|
||||
completion: true,
|
||||
validate: true,
|
||||
format: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: `${apiHost}/api/config/schema.json`,
|
||||
fileMatch: [String(modelUri)],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
window.editor = editor.create(document.getElementById('container'), {
|
||||
language: 'yaml',
|
||||
model: editor.createModel(config, 'yaml', modelUri),
|
||||
scrollBeyondLastLine: false,
|
||||
theme: 'vs-dark',
|
||||
});
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2 px-4 h-full">
|
||||
<div className="flex justify-between">
|
||||
<Heading>Config</Heading>
|
||||
<div>
|
||||
<Button className="mx-2" onClick={(e) => handleCopyConfig(e)}>
|
||||
Copy Config
|
||||
</Button>
|
||||
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e)}>
|
||||
Save & Restart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{success && <div className="max-h-20 text-green-500">{success}</div>}
|
||||
{error && <div className="p-4 overflow-scroll text-red-500 whitespace-pre-wrap">{error}</div>}
|
||||
|
||||
<div id="container" className="h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useWs } from '../api/ws';
|
||||
import useSWR from 'swr';
|
||||
import axios from 'axios';
|
||||
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { useState } from 'preact/hooks';
|
||||
import Dialog from '../components/Dialog';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
@@ -34,13 +34,6 @@ export default function System() {
|
||||
const gpuNames = Object.keys(gpu_usages || emptyObject);
|
||||
const cameraNames = Object.keys(cameras || emptyObject);
|
||||
|
||||
const handleCopyConfig = useCallback(() => {
|
||||
async function copy() {
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||
}
|
||||
copy();
|
||||
}, [config]);
|
||||
|
||||
const onHandleFfprobe = async (camera, e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
@@ -267,16 +260,6 @@ export default function System() {
|
||||
<p>System stats update automatically every {config.mqtt.stats_interval} seconds.</p>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<Heading size="sm">Config</Heading>
|
||||
<Button className="absolute top-8 right-4" onClick={handleCopyConfig}>
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
|
||||
{JSON.stringify(config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ export async function getStorage(_url, _cb, _props) {
|
||||
return module.default;
|
||||
}
|
||||
|
||||
export async function getConfig(_url, _cb, _props) {
|
||||
const module = await import('./Config.jsx');
|
||||
return module.default;
|
||||
}
|
||||
|
||||
export async function getStyleGuide(_url, _cb, _props) {
|
||||
const module = await import('./StyleGuide.jsx');
|
||||
return module.default;
|
||||
|
||||
Reference in New Issue
Block a user