Can export data from UI
This commit is contained in:
		
							
								
								
									
										17
									
								
								geneit_app/src/api/DataApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								geneit_app/src/api/DataApi.ts
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @@ -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)!; | ||||
| } | ||||
| @@ -15,6 +15,7 @@ import { ConfirmDialogProvider } from "./hooks/context_providers/ConfirmDialogPr | ||||
| import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider"; | ||||
| import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "./widgets/AsyncWidget"; | ||||
| import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider"; | ||||
|  | ||||
| async function init() { | ||||
|   try { | ||||
| @@ -28,14 +29,16 @@ async function init() { | ||||
|           <AlertDialogProvider> | ||||
|             <ConfirmDialogProvider> | ||||
|               <SnackbarProvider> | ||||
|                 <div style={{ height: "100vh" }}> | ||||
|                   <AsyncWidget | ||||
|                     loadKey={1} | ||||
|                     load={async () => await ServerApi.LoadConfig()} | ||||
|                     errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !" | ||||
|                     build={() => <App />} | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <LoadingMessageProvider> | ||||
|                   <div style={{ height: "100vh" }}> | ||||
|                     <AsyncWidget | ||||
|                       loadKey={1} | ||||
|                       load={async () => await ServerApi.LoadConfig()} | ||||
|                       errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !" | ||||
|                       build={() => <App />} | ||||
|                     /> | ||||
|                   </div> | ||||
|                 </LoadingMessageProvider> | ||||
|               </SnackbarProvider> | ||||
|             </ConfirmDialogProvider> | ||||
|           </AlertDialogProvider> | ||||
|   | ||||
| @@ -16,6 +16,11 @@ import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; | ||||
| import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; | ||||
| import { useFamily } from "../../widgets/BaseFamilyRoute"; | ||||
| 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 { | ||||
|   const alert = useAlert(); | ||||
| @@ -24,32 +29,6 @@ export function FamilySettingsRoute(): React.ReactElement { | ||||
|  | ||||
|   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 () => { | ||||
|     try { | ||||
|       if ( | ||||
| @@ -72,60 +51,8 @@ export function FamilySettingsRoute(): React.ReactElement { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Card style={{ margin: "10px auto", maxWidth: "450px" }}> | ||||
|         {error && <Alert severity="error">{error}</Alert>} | ||||
|         {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> | ||||
|  | ||||
|       <FamilySettingsCard /> | ||||
|       <FamilyExportCard /> | ||||
|       <div style={{ textAlign: "center", marginTop: "50px" }}> | ||||
|         <Button | ||||
|           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> </p> | ||||
|  | ||||
|         <Button | ||||
|           startIcon={<DownloadIcon />} | ||||
|           variant="outlined" | ||||
|           fullWidth | ||||
|           onClick={exportData} | ||||
|         > | ||||
|           Exporter les données de la famille | ||||
|         </Button> | ||||
|       </CardContent> | ||||
|     </FamilyCard> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								geneit_app/src/utils/blob_utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								geneit_app/src/utils/blob_utils.ts
									
									
									
									
									
										Normal 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(); | ||||
| } | ||||
| @@ -141,6 +141,10 @@ export function BaseFamilyRoute(): React.ReactElement { | ||||
|                   backgroundColor: "background.paper", | ||||
|                 }} | ||||
|               > | ||||
|                 <ListSubheader component="div"> | ||||
|                   Famille <em>{family?.name}</em> | ||||
|                 </ListSubheader> | ||||
|  | ||||
|                 <FamilyLink icon={<HomeIcon />} label="Accueil" uri="" /> | ||||
|  | ||||
|                 <FamilyLink | ||||
|   | ||||
							
								
								
									
										15
									
								
								geneit_app/src/widgets/FamilyCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								geneit_app/src/widgets/FamilyCard.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user