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,6 +29,7 @@ async function init() {
 | 
			
		||||
          <AlertDialogProvider>
 | 
			
		||||
            <ConfirmDialogProvider>
 | 
			
		||||
              <SnackbarProvider>
 | 
			
		||||
                <LoadingMessageProvider>
 | 
			
		||||
                  <div style={{ height: "100vh" }}>
 | 
			
		||||
                    <AsyncWidget
 | 
			
		||||
                      loadKey={1}
 | 
			
		||||
@@ -36,6 +38,7 @@ async function init() {
 | 
			
		||||
                      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,10 +51,55 @@ 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>}
 | 
			
		||||
      <FamilySettingsCard />
 | 
			
		||||
      <FamilyExportCard />
 | 
			
		||||
      <div style={{ textAlign: "center", marginTop: "50px" }}>
 | 
			
		||||
        <Button
 | 
			
		||||
          size="small"
 | 
			
		||||
          color="error"
 | 
			
		||||
          onClick={deleteFamily}
 | 
			
		||||
          disabled={!family.family.is_admin}
 | 
			
		||||
        >
 | 
			
		||||
          Supprimer la famille
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -124,18 +148,63 @@ export function FamilySettingsRoute(): React.ReactElement {
 | 
			
		||||
          Enregistrer
 | 
			
		||||
        </Button>
 | 
			
		||||
      </CardActions>
 | 
			
		||||
      </Card>
 | 
			
		||||
 | 
			
		||||
      <div style={{ textAlign: "center", marginTop: "50px" }}>
 | 
			
		||||
        <Button
 | 
			
		||||
          size="small"
 | 
			
		||||
          color="error"
 | 
			
		||||
          onClick={deleteFamily}
 | 
			
		||||
          disabled={!family.family.is_admin}
 | 
			
		||||
        >
 | 
			
		||||
          Supprimer la famille
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
    </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