blakeblackshear.frigate/web/src/pages/ConfigEditor.tsx
Nicolas Mowen 64988c9be0
Streamline live view (#9772)
* Break out live page

* Improving layouts and add chip component

* Improve default camera player sizing

* Improve live updating

* Cleanup and fit figma

* Use fixed height

* Masonry layout

* Fix stuff

* Don't force heights

* Adjust scaling

* Cleanup

* remove sidebar (#9731)

* remove sidebar

* keep sidebar on mobile for now and add icons

* Fix revalidation

* Cleanup

* Cleanup width

* Add chips for activity on cameras

* Remove dashboard from header

* Use Inter font (#9735)

* Show still image when no activity is occurring

* remove unused search params

* add playing check for webrtc

* Don't use grid at all for single column

* Fix height on mobile

* a few style updates to better match figma (#9745)

* Remove active objects when they become stationary

* Move to sidebar only and make settings separate component

* Fix layout

* Animate visibility of chips

* Sidebar is full screen

* Fix tall aspect ratio cameras

* Fix complicated aspect logic

* remove

* Adjust thumbnail aspect and add text

* margin on single column layout

* Smaller event thumb text

* Simplify basic image view

* Only show the red dot when camera is recording

* Improve typing for camera toggles

* animate chips with react-transition-group (#9763)

* don't flash when going to still image

* revalidate

* tooltips and active tracking outline (#9766)

* tooltips

* fix tooltip provider and add active tracking outline

* remove unused icon

* remove figma comment

* Get live mode working for jsmpeg

* add small gradient below timeago on event thumbnails (#9767)

* Create live mode hook and make sure jsmpeg can be used

* Enforce env var

* Use print

* Remove unstable

* Add tooltips to thumbnails

* Put back vite

* Format

* Update web/src/components/player/JSMpegPlayer.tsx

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Co-authored-by: Blake Blackshear <blake@frigate.video>
2024-02-10 05:30:53 -07:00

167 lines
4.3 KiB
TypeScript

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/ui/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";
type SaveOptions = "saveonly" | "restart";
function ConfigEditor() {
const apiHost = useApiHost();
const { data: config } = useSWR<string>("config/raw");
const { theme } = useTheme();
const [error, setError] = useState<string | undefined>();
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const modelRef = useRef<monaco.editor.IEditorModel | null>(null);
const configRef = useRef<HTMLDivElement | null>(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());
}, [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: theme == "dark" ? "vs-dark" : "vs-light",
});
}
return () => {
configRef.current = null;
editorRef.current = null;
modelRef.current = null;
};
});
if (!config) {
return <ActivityIndicator />;
}
return (
<div className="absolute top-2 bottom-16 right-0 left-0 md:left-12">
<div className="lg:flex justify-between mr-1">
<Heading as="h2">Config</Heading>
<div>
<Button
size="sm"
className="mx-1"
onClick={(_) => handleCopyConfig()}
>
Copy Config
</Button>
<Button
size="sm"
className="mx-1"
onClick={(_) => onHandleSaveConfig("restart")}
>
Save & Restart
</Button>
<Button
size="sm"
className="mx-1"
onClick={(_) => onHandleSaveConfig("saveonly")}
>
Save Only
</Button>
</div>
</div>
{error && (
<div className="p-4 overflow-scroll text-danger whitespace-pre-wrap">
{error}
</div>
)}
<div ref={configRef} className="h-full mt-2" />
<Toaster />
</div>
);
}
export default ConfigEditor;