refactor: adjust settings components

pull/4806/head
James Elliott 2022-12-31 16:17:37 +11:00
parent f2ee86472d
commit 4239db6171
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
3 changed files with 174 additions and 196 deletions

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { Fragment, useState } from "react";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
@ -8,64 +8,131 @@ import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material"
import { ButtonProps } from "@mui/material/Button";
import { useTranslation } from "react-i18next";
import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@models/Webauthn";
import { deleteDevice, updateDevice } from "@services/Webauthn";
import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog";
import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog";
import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog";
interface Props {
index: number;
device: WebauthnDevice;
deleting: boolean;
editing: boolean;
webauthnShowDetails: boolean;
handleWebAuthnDetailsChange: () => void;
handleDetails: () => void;
handleDelete: () => void;
handleEdit: () => void;
handleDeviceEdit(index: number, device: WebauthnDevice): void;
handleDeviceDelete(device: WebauthnDevice): void;
}
export default function WebauthnDeviceItem(props: Props) {
const { t: translate } = useTranslation("settings");
const { createErrorNotification } = useNotifications();
const [showDialogDetails, setShowDialogDetails] = useState<boolean>(false);
const [showDialogEdit, setShowDialogEdit] = useState<boolean>(false);
const [showDialogDelete, setShowDialogDelete] = useState<boolean>(false);
const [loadingEdit, setLoadingEdit] = useState<boolean>(false);
const [loadingDelete, setLoadingDelete] = useState<boolean>(false);
const handleEdit = async (ok: boolean, name: string) => {
setShowDialogEdit(false);
if (!ok) {
return;
}
setLoadingEdit(true);
const status = await updateDevice(props.device.id, name);
setLoadingEdit(false);
if (status !== 200) {
createErrorNotification(translate("There was a problem updating the device"));
return;
}
props.handleDeviceEdit(props.index, { ...props.device, description: name });
};
const handleDelete = async (ok: boolean) => {
setShowDialogDelete(false);
if (!ok) {
return;
}
setLoadingDelete(true);
const status = await deleteDevice(props.device.id);
setLoadingDelete(false);
if (status !== 200) {
createErrorNotification(translate("There was a problem deleting the device"));
return;
}
props.handleDeviceDelete(props.device);
};
return (
<Stack direction="row" spacing={1} alignItems="center">
<KeyRoundedIcon fontSize="large" />
<Stack spacing={0} sx={{ minWidth: 400 }}>
<Box>
<Typography display="inline" sx={{ fontWeight: "bold" }}>
{props.device.description}
<Fragment>
<WebauthnDeviceDetailsDialog
device={props.device}
open={showDialogDetails}
handleClose={() => {
setShowDialogDetails(false);
}}
/>
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
<WebauthnDeviceDeleteDialog device={props.device} open={showDialogDelete} handleClose={handleDelete} />
<Stack direction="row" spacing={1} alignItems="center">
<KeyRoundedIcon fontSize="large" />
<Stack spacing={0} sx={{ minWidth: 400 }}>
<Box>
<Typography display="inline" sx={{ fontWeight: "bold" }}>
{props.device.description}
</Typography>
<Typography
display="inline"
variant="body2"
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
</Box>
<Typography>Added {props.device.created_at.toString()}</Typography>
<Typography>
{props.device.last_used_at === undefined
? translate("Never used")
: "Last used " + props.device.last_used_at.toString()}
</Typography>
<Typography
display="inline"
variant="body2"
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
</Box>
<Typography>Added {props.device.created_at.toString()}</Typography>
<Typography>
{props.device.last_used_at === undefined
? translate("Never used")
: "Last used " + props.device.last_used_at.toString()}
</Typography>
</Stack>
<Button
variant="outlined"
color="primary"
startIcon={<InfoOutlinedIcon />}
onClick={() => setShowDialogDetails(true)}
>
{translate("Info")}
</Button>
<LoadingButton
loading={loadingEdit}
variant="outlined"
color="primary"
startIcon={<EditIcon />}
onClick={() => setShowDialogEdit(true)}
>
{translate("Edit")}
</LoadingButton>
<LoadingButton
loading={loadingDelete}
variant="outlined"
color="secondary"
startIcon={<DeleteIcon />}
onClick={() => setShowDialogDelete(true)}
>
{translate("Remove")}
</LoadingButton>
</Stack>
<Button variant="outlined" color="primary" startIcon={<InfoOutlinedIcon />} onClick={props.handleDetails}>
{translate("Info")}
</Button>
<LoadingButton
loading={props.editing}
variant="outlined"
color="primary"
startIcon={<EditIcon />}
onClick={props.handleEdit}
>
{translate("Edit")}
</LoadingButton>
<LoadingButton
loading={props.deleting}
variant="outlined"
color="secondary"
startIcon={<DeleteIcon />}
onClick={props.handleDelete}
>
{translate("Remove")}
</LoadingButton>
</Stack>
</Fragment>
);
}

View File

@ -1,120 +1,26 @@
import React, { useEffect, useState } from "react";
import React, { Fragment, Suspense, useState } from "react";
import { Box, Button, Paper, Skeleton, Stack, Typography } from "@mui/material";
import { Box, Button, Paper, Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { RegisterWebauthnRoute } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@models/Webauthn";
import { initiateWebauthnRegistrationProcess } from "@services/RegisterDevice";
import { AutheliaState, AuthenticationLevel } from "@services/State";
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
import { deleteDevice, updateDevice } from "@services/Webauthn";
import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog";
import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog";
import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog";
import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem";
import LoadingPage from "@views/LoadingPage/LoadingPage";
import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack";
interface Props {
state: AutheliaState;
}
interface WebauthnDeviceDisplay extends WebauthnDevice {
deleting: boolean;
editing: boolean;
}
export default function WebauthnDevices(props: Props) {
const { t: translate } = useTranslation("settings");
const navigate = useNavigate();
const { createInfoNotification, createErrorNotification } = useNotifications();
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
const [detailsIdx, setDetailsIdx] = useState<number>(-1);
const [deletingIdx, setDeletingIdx] = useState<number>(-1);
const [editingIdx, setEditingIdx] = useState<number>(-1);
const [registrationInProgress, setRegistrationInProgress] = useState(false);
const [ready, setReady] = useState(false);
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
useEffect(() => {
(async function () {
const devices = await getWebauthnDevices();
const devicesDisplay = devices.map((x, idx) => {
return {
...x,
deleting: false,
editing: false,
} as WebauthnDeviceDisplay;
});
setWebauthnDevices(devicesDisplay);
setReady(true);
})();
}, []);
const handleWebAuthnDetailsChange = (idx: number) => {
if (webauthnShowDetails === idx) {
setWebauthnShowDetails(-1);
} else {
setWebauthnShowDetails(idx);
}
};
const handleDetailsItem = async (idx: number) => {
setDetailsIdx(idx);
setDetailsDialogOpen(true);
};
const handleDeleteItem = async (idx: number) => {
setDeletingIdx(idx);
setDeleteDialogOpen(true);
};
const handleDeleteItemConfirm = async (ok: boolean) => {
setDeleteDialogOpen(false);
const idx = deletingIdx;
if (ok !== true) {
return;
}
webauthnDevices[idx].deleting = true;
const status = await deleteDevice(webauthnDevices[idx].id);
if (status !== 200) {
webauthnDevices[idx].deleting = false;
createErrorNotification(translate("There was a problem deleting the device"));
return;
}
let updatedDevices = [...webauthnDevices];
updatedDevices.splice(idx, 1);
setWebauthnDevices(updatedDevices);
};
const handleEditItem = async (idx: number) => {
setEditingIdx(idx);
setEditDialogOpen(true);
};
const handleEditItemConfirm = async (ok: boolean, name: string) => {
setEditDialogOpen(false);
const idx = editingIdx;
if (ok !== true) {
return;
}
webauthnDevices[idx].editing = true;
const status = await updateDevice(webauthnDevices[idx].id, name);
webauthnDevices[idx].editing = false;
if (status !== 200) {
createErrorNotification(translate("There was a problem updating the device"));
return;
}
let updatedDevices = [...webauthnDevices];
updatedDevices[idx].description = name;
setWebauthnDevices(updatedDevices);
};
const initiateRegistration = async (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => {
if (props.state.authentication_level >= AuthenticationLevel.TwoFactor) {
@ -140,24 +46,7 @@ export default function WebauthnDevices(props: Props) {
};
return (
<>
<WebauthnDeviceDetailsDialog
device={detailsIdx > -1 ? webauthnDevices[detailsIdx] : undefined}
open={detailsDialogOpen}
handleClose={() => {
setDetailsDialogOpen(false);
}}
/>
<WebauthnDeviceEditDialog
device={editingIdx > -1 ? webauthnDevices[editingIdx] : undefined}
open={editDialogOpen}
handleClose={handleEditItemConfirm}
/>
<WebauthnDeviceDeleteDialog
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
open={deleteDialogOpen}
handleClose={handleDeleteItemConfirm}
/>
<Fragment>
<Paper variant="outlined">
<Box sx={{ p: 3 }}>
<Stack spacing={2}>
@ -169,41 +58,12 @@ export default function WebauthnDevices(props: Props) {
{"Add new device"}
</Button>
</Box>
{ready ? (
<Stack spacing={3}>
{webauthnDevices
? webauthnDevices.map((x, idx) => (
<WebauthnDeviceItem
device={x}
deleting={x.deleting}
editing={x.editing}
webauthnShowDetails={webauthnShowDetails === idx}
handleWebAuthnDetailsChange={() => {
handleWebAuthnDetailsChange(idx);
}}
handleDetails={() => {
handleDetailsItem(idx);
}}
handleEdit={() => {
handleEditItem(idx);
}}
handleDelete={() => {
handleDeleteItem(idx);
}}
key={`webauthn-device-${idx}`}
/>
))
: null}
</Stack>
) : (
<>
<Skeleton height={20} />
<Skeleton height={40} />
</>
)}
<Suspense fallback={<LoadingPage />}>
<WebauthnDevicesStack />
</Suspense>
</Stack>
</Box>
</Paper>
</>
</Fragment>
);
}

View File

@ -0,0 +1,51 @@
import React, { useEffect, useState } from "react";
import { Stack } from "@mui/material";
import { WebauthnDevice } from "@models/Webauthn";
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem";
interface Props {}
export default function WebauthnDevicesStack(props: Props) {
const [devices, setDevices] = useState<WebauthnDevice[]>([]);
useEffect(() => {
(async function () {
const devices = await getWebauthnDevices();
setDevices(devices);
})();
}, []);
const handleEdit = (index: number, device: WebauthnDevice) => {
const nextDevices = devices.map((d, i) => {
if (i === index) {
return device;
} else {
return d;
}
});
setDevices(nextDevices);
};
const handleDelete = (device: WebauthnDevice) => {
setDevices(devices.filter((d) => d.id !== device.id && d.kid !== device.kid));
};
return (
<Stack spacing={3}>
{devices
? devices.map((x, idx) => (
<WebauthnDeviceItem
index={idx}
device={x}
handleDeviceEdit={handleEdit}
handleDeviceDelete={handleDelete}
/>
))
: null}
</Stack>
);
}