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:
Nicolas Mowen
2022-12-07 06:36:56 -07:00
committed by GitHub
parent 97161310a5
commit 7888059c9f
10 changed files with 370 additions and 29 deletions

View File

@@ -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' ? (

View File

@@ -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
View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;