Can export data from UI

This commit is contained in:
Pierre HUBERT 2023-08-18 15:10:16 +02:00
parent 5fa3d79b4c
commit 6c82104cdc
7 changed files with 273 additions and 88 deletions

View File

@ -0,0 +1,17 @@
import { APIClient } from "./ApiClient";
/**
* Data management api client
*/
export class DataApi {
/**
* Export the data of a family
*/
static async ExportData(family_id: number): Promise<Blob> {
const res = await APIClient.exec({
uri: `/family/${family_id}/data/export`,
method: "GET",
});
return res.data;
}
}

View File

@ -0,0 +1,67 @@
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
import React, { PropsWithChildren } from "react";
type LoadingMessageContext = {
show: (message: string) => void;
hide: () => void;
};
const LoadingMessageContextK =
React.createContext<LoadingMessageContext | null>(null);
export function LoadingMessageProvider(
p: PropsWithChildren
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [message, setMessage] = React.useState("");
const hook: LoadingMessageContext = {
show(message) {
setMessage(message);
setOpen(true);
},
hide() {
setMessage("");
setOpen(false);
},
};
return (
<>
<LoadingMessageContextK.Provider value={hook}>
{p.children}
</LoadingMessageContextK.Provider>
<Dialog open={open}>
<DialogContent>
<DialogContentText>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<CircularProgress style={{ marginRight: "15px" }} />
{message}
</div>
</DialogContentText>
</DialogContent>
</Dialog>
</>
);
}
export function useLoadingMessage(): LoadingMessageContext {
return React.useContext(LoadingMessageContextK)!;
}

View File

@ -15,6 +15,7 @@ import { ConfirmDialogProvider } from "./hooks/context_providers/ConfirmDialogPr
import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider"; import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider";
import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider"; import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider";
import { AsyncWidget } from "./widgets/AsyncWidget"; import { AsyncWidget } from "./widgets/AsyncWidget";
import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider";
async function init() { async function init() {
try { try {
@ -28,14 +29,16 @@ async function init() {
<AlertDialogProvider> <AlertDialogProvider>
<ConfirmDialogProvider> <ConfirmDialogProvider>
<SnackbarProvider> <SnackbarProvider>
<div style={{ height: "100vh" }}> <LoadingMessageProvider>
<AsyncWidget <div style={{ height: "100vh" }}>
loadKey={1} <AsyncWidget
load={async () => await ServerApi.LoadConfig()} loadKey={1}
errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !" load={async () => await ServerApi.LoadConfig()}
build={() => <App />} errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !"
/> build={() => <App />}
</div> />
</div>
</LoadingMessageProvider>
</SnackbarProvider> </SnackbarProvider>
</ConfirmDialogProvider> </ConfirmDialogProvider>
</AlertDialogProvider> </AlertDialogProvider>

View File

@ -16,6 +16,11 @@ import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../widgets/BaseFamilyRoute";
import { formatDate } from "../../widgets/TimeWidget"; import { formatDate } from "../../widgets/TimeWidget";
import { FamilyCard } from "../../widgets/FamilyCard";
import DownloadIcon from "@mui/icons-material/Download";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
import { DataApi } from "../../api/DataApi";
import { downloadBlob } from "../../utils/blob_utils";
export function FamilySettingsRoute(): React.ReactElement { export function FamilySettingsRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
@ -24,32 +29,6 @@ export function FamilySettingsRoute(): React.ReactElement {
const family = useFamily(); const family = useFamily();
const [newName, setNewName] = React.useState(family.family.name);
const canEdit = family.family.is_admin;
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState<string | null>(null);
const updateFamily = async () => {
try {
setError(null);
setSuccess(null);
await FamilyApi.UpdateFamily({
id: family.family.family_id,
name: newName,
});
family.reloadFamilyInfo();
alert("Les paramètres de la famille ont été mis à jour avec succès !");
} catch (e) {
console.error(e);
setError("Echec de la mise à jour des paramètres de la famille !");
}
};
const deleteFamily = async () => { const deleteFamily = async () => {
try { try {
if ( if (
@ -72,60 +51,8 @@ export function FamilySettingsRoute(): React.ReactElement {
return ( return (
<> <>
<Card style={{ margin: "10px auto", maxWidth: "450px" }}> <FamilySettingsCard />
{error && <Alert severity="error">{error}</Alert>} <FamilyExportCard />
{success && <Alert severity="success">{success}</Alert>}
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Paramètres de la famille
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
>
<TextField
disabled
fullWidth
label="Identifiant"
value={family.family.family_id}
/>
<TextField
disabled
fullWidth
label="Création de la famille"
value={formatDate(family.family.time_create)}
/>
<TextField
fullWidth
label="Nom de la famille"
value={newName}
disabled={!canEdit}
onChange={(e) => setNewName(e.target.value)}
inputProps={{
maxLength: ServerApi.Config.constraints.family_name_len.max,
}}
/>
</Box>
</CardContent>
<CardActions>
<Button
onClick={updateFamily}
disabled={!canEdit}
style={{ marginLeft: "auto" }}
>
Enregistrer
</Button>
</CardActions>
</Card>
<div style={{ textAlign: "center", marginTop: "50px" }}> <div style={{ textAlign: "center", marginTop: "50px" }}>
<Button <Button
size="small" size="small"
@ -139,3 +66,145 @@ export function FamilySettingsRoute(): React.ReactElement {
</> </>
); );
} }
function FamilySettingsCard(): React.ReactElement {
const alert = useAlert();
const family = useFamily();
const [newName, setNewName] = React.useState(family.family.name);
const canEdit = family.family.is_admin;
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const updateFamily = async () => {
try {
setError(undefined);
setSuccess(undefined);
await FamilyApi.UpdateFamily({
id: family.family.family_id,
name: newName,
});
family.reloadFamilyInfo();
alert("Les paramètres de la famille ont été mis à jour avec succès !");
} catch (e) {
console.error(e);
setError("Echec de la mise à jour des paramètres de la famille !");
}
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Paramètres de la famille
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
>
<TextField
disabled
fullWidth
label="Identifiant"
value={family.family.family_id}
/>
<TextField
disabled
fullWidth
label="Création de la famille"
value={formatDate(family.family.time_create)}
/>
<TextField
fullWidth
label="Nom de la famille"
value={newName}
disabled={!canEdit}
onChange={(e) => setNewName(e.target.value)}
inputProps={{
maxLength: ServerApi.Config.constraints.family_name_len.max,
}}
/>
</Box>
</CardContent>
<CardActions>
<Button
onClick={updateFamily}
disabled={!canEdit}
style={{ marginLeft: "auto" }}
>
Enregistrer
</Button>
</CardActions>
</FamilyCard>
);
}
function FamilyExportCard(): React.ReactElement {
const loading = useLoadingMessage();
const alert = useAlert();
const family = useFamily();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const exportData = async () => {
loading.show("Export des données");
try {
setError(undefined);
setSuccess(undefined);
const blob = await DataApi.ExportData(family.familyId);
downloadBlob(blob, `Export-${new Date().getTime()}.zip`);
} catch (e) {
console.error(e);
setError("Echec de l'export des données de la famille !");
}
loading.hide();
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Export / import des données de la famille
</Typography>
<p>
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et
importer l'ensemble des données des membres et des couples de cette
famille, sous format ZIP.
</p>
<Alert severity="warning">
Attention ! La restauration des données de la famille provoque
préalablement l'effacement de toutes les données enregistrées dans la
famille ! Par ailleurs, la restauration n'est pas réversible !
</Alert>
<p>&nbsp;</p>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
fullWidth
onClick={exportData}
>
Exporter les données de la famille
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -0,0 +1,10 @@
export async function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.rel = "noopener";
link.download = filename;
link.click();
}

View File

@ -141,6 +141,10 @@ export function BaseFamilyRoute(): React.ReactElement {
backgroundColor: "background.paper", backgroundColor: "background.paper",
}} }}
> >
<ListSubheader component="div">
Famille <em>{family?.name}</em>
</ListSubheader>
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" /> <FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
<FamilyLink <FamilyLink

View File

@ -0,0 +1,15 @@
import { Alert, Card } from "@mui/material";
import { PropsWithChildren } from "react";
export function FamilyCard(
p: PropsWithChildren<{ error?: string; success?: string }>
): React.ReactElement {
return (
<Card style={{ margin: "10px auto", maxWidth: "450px" }}>
{p.error && <Alert severity="error">{p.error}</Alert>}
{p.success && <Alert severity="success">{p.success}</Alert>}
{p.children}
</Card>
);
}