diff --git a/remote_frontend/index.html b/remote_frontend/index.html index e4b78ea..92aa951 100644 --- a/remote_frontend/index.html +++ b/remote_frontend/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + VirtWebRemote
diff --git a/remote_frontend/public/remote.svg b/remote_frontend/public/remote.svg new file mode 100644 index 0000000..027453c --- /dev/null +++ b/remote_frontend/public/remote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/remote_frontend/src/App.tsx b/remote_frontend/src/App.tsx index bd1141d..005b232 100644 --- a/remote_frontend/src/App.tsx +++ b/remote_frontend/src/App.tsx @@ -1,6 +1,23 @@ +import { + Menu, + MenuButton, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + makeStyles, + typographyStyles, +} from "@fluentui/react-components"; import { ServerApi } from "./api/ServerApi"; import { AuthRouteWidget } from "./routes/AuthRouteWidget"; import { AsyncWidget } from "./widgets/AsyncWidget"; +import { AuthApi } from "./api/AuthApi"; +import { useAlert } from "./hooks/providers/AlertDialogProvider"; +import { useConfirm } from "./hooks/providers/ConfirmDialogProvider"; + +const useStyles = makeStyles({ + title: typographyStyles.title2, +}); export function App() { return ( @@ -15,7 +32,45 @@ export function App() { } function AppInner(): React.ReactElement { + const alert = useAlert(); + const confirm = useConfirm(); + const styles = useStyles(); + + const signOut = async () => { + try { + if (!(await confirm("Do you really want to sign out?"))) return; + await AuthApi.SignOut(); + } catch (e) { + console.error(e); + alert("Failed to perform sign out!"); + } + }; + if (!ServerApi.Config.authenticated && !ServerApi.Config.disable_auth) return ; - return <>todo authenticated; + + return ( +
+
+ VirtWebRemote + + + Account + + + + + Sign out + + + +
+
+ ); } diff --git a/remote_frontend/src/api/AuthApi.ts b/remote_frontend/src/api/AuthApi.ts index a367ca0..25c4ba6 100644 --- a/remote_frontend/src/api/AuthApi.ts +++ b/remote_frontend/src/api/AuthApi.ts @@ -25,4 +25,16 @@ export class AuthApi { window.location.href = "/"; } + + /** + * Sign out + */ + static async SignOut(): Promise { + await APIClient.exec({ + uri: "/auth/sign_out", + method: "GET", + }); + + window.location.href = "/"; + } } diff --git a/remote_frontend/src/hooks/providers/AlertDialogProvider.tsx b/remote_frontend/src/hooks/providers/AlertDialogProvider.tsx new file mode 100644 index 0000000..de5b20a --- /dev/null +++ b/remote_frontend/src/hooks/providers/AlertDialogProvider.tsx @@ -0,0 +1,69 @@ +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, +} from "@fluentui/react-components"; +import React, { PropsWithChildren } from "react"; + +type AlertContext = (message: string, title?: string) => Promise; + +const AlertContextK = React.createContext(null); + +export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + + const cb = React.useRef void)>(null); + + const handleClose = () => { + setOpen(false); + + if (cb.current !== null) cb.current(); + cb.current = null; + }; + + const hook: AlertContext = (message, title) => { + setTitle(title); + setMessage(message); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + {p.children} + + { + if (!data.open) setOpen(data.open); + }} + > + + + {title && {title}} + {message} + + + + + + + + ); +} + +export function useAlert(): AlertContext { + return React.useContext(AlertContextK)!; +} diff --git a/remote_frontend/src/hooks/providers/ConfirmDialogProvider.tsx b/remote_frontend/src/hooks/providers/ConfirmDialogProvider.tsx new file mode 100644 index 0000000..8630aea --- /dev/null +++ b/remote_frontend/src/hooks/providers/ConfirmDialogProvider.tsx @@ -0,0 +1,98 @@ +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, +} from "@fluentui/react-components"; +import React, { PropsWithChildren } from "react"; + +type ConfirmContext = ( + message: string, + title?: string, + confirmButton?: string, + cancelButton?: string +) => Promise; + +const ConfirmContextK = React.createContext(null); + +export function ConfirmDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + const [confirmButton, setConfirmButton] = React.useState( + undefined + ); + const [cancelButton, setCancelButton] = React.useState( + undefined + ); + + const cb = React.useRef void)>(null); + + const handleClose = (confirm: boolean) => { + setOpen(false); + + if (cb.current !== null) cb.current(confirm); + cb.current = null; + }; + + const hook: ConfirmContext = ( + message, + title, + confirmButton, + cancelButton + ) => { + setTitle(title); + setMessage(message); + setConfirmButton(confirmButton); + setCancelButton(cancelButton); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + { + if (!data.open) setOpen(false); + }} + > + + + {title && {title}} + {message} + + + + + + + + + ); +} + +export function useConfirm(): ConfirmContext { + return React.useContext(ConfirmContextK)!; +} diff --git a/remote_frontend/src/main.tsx b/remote_frontend/src/main.tsx index 8dc1b59..2624abb 100644 --- a/remote_frontend/src/main.tsx +++ b/remote_frontend/src/main.tsx @@ -6,6 +6,8 @@ import { FluentProvider, teamsHighContrastTheme, } from "@fluentui/react-components"; +import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider"; +import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider"; ReactDOM.createRoot(document.getElementById("root")!).render( @@ -13,7 +15,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render( theme={teamsHighContrastTheme} style={{ display: "flex", flex: 1 }} > - + + + + + );