Can export data from UI
This commit is contained in:
parent
5fa3d79b4c
commit
6c82104cdc
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 { 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>
|
||||||
|
@ -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> </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",
|
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
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user