User can sign out

This commit is contained in:
Pierre HUBERT 2024-05-03 22:03:25 +02:00
parent 9407d8f0a8
commit 8a8b1a8846
7 changed files with 245 additions and 4 deletions

View File

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/remote.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>VirtWebRemote</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,0C8.96,0 6.21,1.23 4.22,3.22L5.63,4.63C7.26,3 9.5,2 12,2C14.5,2 16.74,3 18.36,4.64L19.77,3.23C17.79,1.23 15.04,0 12,0M7.05,6.05L8.46,7.46C9.37,6.56 10.62,6 12,6C13.38,6 14.63,6.56 15.54,7.46L16.95,6.05C15.68,4.78 13.93,4 12,4C10.07,4 8.32,4.78 7.05,6.05M12,15A2,2 0 0,1 10,13A2,2 0 0,1 12,11A2,2 0 0,1 14,13A2,2 0 0,1 12,15M15,9H9A1,1 0 0,0 8,10V22A1,1 0 0,0 9,23H15A1,1 0 0,0 16,22V10A1,1 0 0,0 15,9Z" /></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -1,6 +1,23 @@
import {
Menu,
MenuButton,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
makeStyles,
typographyStyles,
} from "@fluentui/react-components";
import { ServerApi } from "./api/ServerApi"; import { ServerApi } from "./api/ServerApi";
import { AuthRouteWidget } from "./routes/AuthRouteWidget"; import { AuthRouteWidget } from "./routes/AuthRouteWidget";
import { AsyncWidget } from "./widgets/AsyncWidget"; 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() { export function App() {
return ( return (
@ -15,7 +32,45 @@ export function App() {
} }
function AppInner(): React.ReactElement { 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) if (!ServerApi.Config.authenticated && !ServerApi.Config.disable_auth)
return <AuthRouteWidget />; return <AuthRouteWidget />;
return <>todo authenticated</>;
return (
<div
style={{
width: "100%",
maxWidth: "1000px",
margin: "50px auto",
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span className={styles.title}>VirtWebRemote</span>
<Menu>
<MenuTrigger disableButtonEnhancement>
<MenuButton>Account</MenuButton>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem onClick={signOut}>Sign out</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</div>
</div>
);
} }

View File

@ -25,4 +25,16 @@ export class AuthApi {
window.location.href = "/"; window.location.href = "/";
} }
/**
* Sign out
*/
static async SignOut(): Promise<void> {
await APIClient.exec({
uri: "/auth/sign_out",
method: "GET",
});
window.location.href = "/";
}
} }

View File

@ -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<void>;
const AlertContextK = React.createContext<AlertContext | null>(null);
export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [title, setTitle] = React.useState<string | undefined>(undefined);
const [message, setMessage] = React.useState("");
const cb = React.useRef<null | (() => 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 (
<>
<AlertContextK.Provider value={hook}>{p.children}</AlertContextK.Provider>
<Dialog
open={open}
onOpenChange={(_event, data) => {
if (!data.open) setOpen(data.open);
}}
>
<DialogSurface>
<DialogBody>
{title && <DialogTitle>{title}</DialogTitle>}
<DialogContent>{message}</DialogContent>
<DialogActions>
<Button onClick={handleClose} autoFocus appearance="secondary">
Ok
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
);
}
export function useAlert(): AlertContext {
return React.useContext(AlertContextK)!;
}

View File

@ -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<boolean>;
const ConfirmContextK = React.createContext<ConfirmContext | null>(null);
export function ConfirmDialogProvider(
p: PropsWithChildren
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [title, setTitle] = React.useState<string | undefined>(undefined);
const [message, setMessage] = React.useState("");
const [confirmButton, setConfirmButton] = React.useState<string | undefined>(
undefined
);
const [cancelButton, setCancelButton] = React.useState<string | undefined>(
undefined
);
const cb = React.useRef<null | ((a: boolean) => 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 (
<>
<ConfirmContextK.Provider value={hook}>
{p.children}
</ConfirmContextK.Provider>
<Dialog
open={open}
onOpenChange={(_event, data) => {
if (!data.open) setOpen(false);
}}
>
<DialogSurface>
<DialogBody>
{title && <DialogTitle>{title}</DialogTitle>}
<DialogContent>{message}</DialogContent>
<DialogActions>
<Button
onClick={() => handleClose(false)}
autoFocus
appearance="secondary"
>
{cancelButton ?? "Cancel"}
</Button>
<Button onClick={() => handleClose(true)} appearance="primary">
{confirmButton ?? "Confirm"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
);
}
export function useConfirm(): ConfirmContext {
return React.useContext(ConfirmContextK)!;
}

View File

@ -6,6 +6,8 @@ import {
FluentProvider, FluentProvider,
teamsHighContrastTheme, teamsHighContrastTheme,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
@ -13,7 +15,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
theme={teamsHighContrastTheme} theme={teamsHighContrastTheme}
style={{ display: "flex", flex: 1 }} style={{ display: "flex", flex: 1 }}
> >
<App /> <AlertDialogProvider>
<ConfirmDialogProvider>
<App />
</ConfirmDialogProvider>
</AlertDialogProvider>
</FluentProvider> </FluentProvider>
</React.StrictMode> </React.StrictMode>
); );