Skip to content

Commit

Permalink
Merge pull request #375 from roflcoopter/feature/restart-from-ui
Browse files Browse the repository at this point in the history
Feature/restart from UI
  • Loading branch information
roflcoopter authored Oct 28, 2022
2 parents 27e85e6 + 2d805a3 commit 9a629b0
Show file tree
Hide file tree
Showing 15 changed files with 19,981 additions and 479 deletions.
20,146 changes: 19,751 additions & 395 deletions frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"buffer": "^6.0.3",
"http-proxy-middleware": "^2.0.2",
"monaco-editor": "^0.31.1",
"monaco-yaml": "4.0.0-alpha.1",
"monaco-yaml": "4.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-lazyload": "^3.2.0",
Expand Down
114 changes: 103 additions & 11 deletions frontend/src/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RestartAlt } from "@mui/icons-material";
import SaveIcon from "@mui/icons-material/Save";
import LoadingButton from "@mui/lab/LoadingButton";
import {
Expand Down Expand Up @@ -32,6 +33,7 @@ import MonacoEditor, {

import { Loading } from "components/loading/Loading";
import { ViseronContext } from "context/ViseronContext";
import { getConfig, restartViseron, saveConfig } from "lib/commands";

type GlobalThis = typeof globalThis &
Window & {
Expand Down Expand Up @@ -180,6 +182,9 @@ const Editor = () => {
const [errorDialog, setErrorDialog] = useState({ open: false, text: "" });
const [syntaxWarningDialog, setSyntaxWarningDialog] = useState(false);

const [restartPending, setRestartPending] = useState(false);
const [restartDialog, setRestartDialog] = useState({ open: false, text: "" });

const updateDimensions = useCallback(() => {
if (editorInstance) {
editorInstance!.current!.layout();
Expand All @@ -200,7 +205,7 @@ const Editor = () => {
const save = () => {
setSavePending(true);
const config = editorInstance!.current!.getModel()!.getValue();
viseron.connection!.saveConfig(config).then(
saveConfig(viseron.connection!, config).then(
(_value) => {
setSavePending(false);
setSavedConfig(config);
Expand All @@ -224,6 +229,28 @@ const Editor = () => {
}
};

const _restartViseron = () => {
const _restart = async () => {
setRestartPending(true);
await restartViseron(viseron.connection!).catch(() =>
setRestartPending(false)
);
};
_restart();
};

const handleRestart = () => {
if (viseron.connection && editorInstance.current) {
let text = "Are you sure you want to restart Viseron?";
if (problemsRef.current.length > 0) {
text = `You have synxat errors in your config. ${text}`;
} else if (configUnsaved) {
text = `You have unsaved changes to your config. Do you want to restart Viseron anyway?`;
}
setRestartDialog({ open: true, text });
}
};

const editorDidMount: EditorDidMount = (editor, monaco) => {
editorInstance.current = editor;
monacoInstance.current = monaco;
Expand All @@ -250,14 +277,18 @@ const Editor = () => {

useEffect(() => {
if (viseron.connection) {
const getConfig = async () => {
const config = await viseron.connection!.getConfig();
const _getConfig = async () => {
const config = await getConfig(viseron.connection!);
setSavedConfig(config);
};
getConfig();
_getConfig();
}
}, [viseron.connection]);

useEffect(() => {
setRestartPending(!viseron.connected);
}, [viseron.connected]);

if (savedConfig === undefined) {
return <Loading text="Loading Configuration" />;
}
Expand Down Expand Up @@ -331,21 +362,82 @@ const Editor = () => {
</Button>
</DialogActions>
</Dialog>
<Dialog
open={restartDialog.open}
onClose={() => {
setRestartDialog({ ...restartDialog, open: false });
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Restart Viseron."}</DialogTitle>
{restartDialog.text && (
<DialogContent>
<DialogContentText id="alert-dialog-description">
{restartDialog.text}
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button
onClick={() => {
_restartViseron();
setRestartDialog({ ...restartDialog, open: false });
// Editor does not focus without the timer
setTimeout(() => {
editorInstance.current?.focus();
}, 1);
}}
>
Yes
</Button>
<Button
onClick={() => {
setRestartDialog({ ...restartDialog, open: false });
// Editor does not focus without the timer
setTimeout(() => {
editorInstance.current?.focus();
}, 1);
}}
>
Cancel
</Button>
</DialogActions>
</Dialog>
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Tooltip title="Ctrl+S" enterDelay={300}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
>
<Tooltip title="Ctrl+S" enterDelay={300}>
<span>
<LoadingButton
startIcon={<SaveIcon />}
loadingPosition="start"
onClick={handleSave}
variant="contained"
loading={savePending}
disabled={!configUnsaved}
>
Save
</LoadingButton>
</span>
</Tooltip>
<span>
<LoadingButton
startIcon={<SaveIcon />}
startIcon={<RestartAlt />}
loadingPosition="start"
onClick={handleSave}
onClick={handleRestart}
variant="contained"
loading={savePending}
disabled={!configUnsaved}
loading={restartPending}
color="error"
>
Save
Restart
</LoadingButton>
</span>
</Tooltip>
</Stack>
<Box
sx={{
width: editorWidth,
Expand Down
16 changes: 13 additions & 3 deletions frontend/src/context/ViseronContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { FC, createContext, useEffect, useState } from "react";

import { getCameras, subscribeCameras } from "lib/commands";
import { sortObj } from "lib/helpers";
import * as types from "lib/types";
import { Connection } from "lib/websockets";
Expand All @@ -10,11 +11,13 @@ export type ViseronProviderProps = {

export type ViseronContextState = {
connection: Connection | undefined;
connected: boolean;
cameras: types.Cameras;
};

const contextDefaultValues: ViseronContextState = {
connection: undefined,
connected: false,
cameras: {},
};

Expand All @@ -27,6 +30,7 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
const [connection, setConnection] = useState<Connection | undefined>(
undefined
);
const [connected, setConnected] = useState<boolean>(false);
const [cameras, setCameras] = useState<types.Cameras>({});

useEffect(() => {
Expand All @@ -41,13 +45,19 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
};

const onConnect = async () => {
const registeredCameras = await connection!.getCameras();
setConnected(true);
const registeredCameras = await getCameras(connection);
setCameras(sortObj(registeredCameras));
};
connection!.addEventListener("connected", onConnect);

const onDisonnect = async () => {
setConnected(false);
};
connection!.addEventListener("disconnected", onDisonnect);

const connect = async () => {
connection!.subscribeCameras(cameraRegistered); // call without await to not block
subscribeCameras(connection, cameraRegistered); // call without await to not block
await connection!.connect();
};
connect();
Expand All @@ -59,7 +69,7 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
}, []);

return (
<ViseronContext.Provider value={{ cameras, connection }}>
<ViseronContext.Provider value={{ connection, connected, cameras }}>
{children}
</ViseronContext.Provider>
);
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as messages from "lib/messages";
import * as types from "lib/types";
import { Connection } from "./websockets";


export const getCameras = (async (connection: Connection): Promise<types.Cameras> => {
const response = await connection.sendMessagePromise(messages.getCameras());
const json = await JSON.parse(response);
return json;
})

export const subscribeCameras = (async (connection: Connection, cameraCallback: (camera: types.Camera) => void) => {
const storedCameraCallback = cameraCallback;
const _cameraCallback = (message: types.CameraRegisteredEvent) => {
storedCameraCallback(message.data);
};
const subscription = await connection.subscribeEvent(
"domain/registered/camera",
_cameraCallback,
true
);
return subscription;
})

export const getConfig = (async (connection: Connection): Promise<string> => {
const response = await connection.sendMessagePromise(messages.getConfig());
return response.config;
})

export const saveConfig = (
connection: Connection,
config: string
): Promise<
types.WebSocketResultResponse["result"] | types.WebSocketResultErrorResponse["error"]
> => connection.sendMessagePromise(messages.saveConfig(config))

export const restartViseron = (async (connection: Connection): Promise<void> => {
await connection.sendMessagePromise(messages.restartViseron());
})
6 changes: 6 additions & 0 deletions frontend/src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ export function saveConfig(config: string) {

return message;
}

export function restartViseron() {
return {
type: "restart_viseron",
};
}
29 changes: 29 additions & 0 deletions frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,32 @@ export type CameraRegisteredEvent = EventBase & {
name: "camera_registered";
data: Camera;
};


export type WebSocketEventResponse = {
command_id: number;
type: "event";
event: Event;
};

export type WebSocketResultResponse = {
command_id: number;
type: "result";
success: true;
result: any;
};

export type WebSocketResultErrorResponse = {
command_id: number;
type: "result";
success: false;
error: {
code: string;
message: string;
};
};

export type WebSocketResponse =
| WebSocketEventResponse
| WebSocketResultResponse
| WebSocketResultErrorResponse;
Loading

0 comments on commit 9a629b0

Please sign in to comment.