crane-cloud/frontend

View on GitHub
src/pages/databases/DatabaseDetails.tsx

Summary

Maintainability
A
55 mins
Test Coverage
import { useNavigate, useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import {
Stack,
Card,
Grid,
Flex,
Text,
Button,
Group,
Divider,
Input,
Tooltip,
ActionIcon,
Skeleton,
PasswordInput,
} from "@mantine/core";
import { HiLockOpen, HiLockClosed, HiTrash } from "react-icons/hi";
import TitleText from "@/components/TitleText";
import { ModalConfirm } from "@/components/Elements/Modals";
import useGet from "@/utils/useGet";
import { DATABASE_API_URL } from "@/config";
import usePost from "@/utils/usePost";
import { RiResetLeftLine } from "react-icons/ri";
import { LiaExchangeAltSolid } from "react-icons/lia";
import {
beautify,
getConnectionString,
getDatabaseStatus,
numberFormat,
useGetProject,
useSetContainerSize,
} from "@/utils/helpers";
import { BiLogoPostgresql } from "react-icons/bi";
import { TbBrandMysql, TbCopy } from "react-icons/tb";
import moment from "moment";
import { useClipboard } from "@mantine/hooks";
import { AiOutlineEyeInvisible, AiOutlineEye } from "react-icons/ai";
import useForm from "@/hooks/useForm";
import { showNotification } from "@mantine/notifications";
import { MdOutlineLock } from "react-icons/md";
 
Function `DatabaseDetails` has a Cognitive Complexity of 9 (exceeds 5 allowed). Consider refactoring.
const DatabaseDetails = () => {
const [deleteConfirmOpened, setDeleteConfirmOpened] = useState(false);
const [disableConfirmOpened, setDisableConfirmOpened] = useState(false);
const [enableConfirmOpened, setEnableConfirmOpened] = useState(false);
const [resetConfirmOpened, setResetConfirmOpened] = useState(false);
const [changePasswordConfirmOpened, setChangePasswordConfirmOpened] =
useState(false);
const { form: changePasswordForm, onChange: changePasswordOnChange } =
useForm();
 
const { database_id, project_id } = useParams();
const clipboard = useClipboard({ timeout: 500 });
const [database, setDatabase] = useState<any>({});
const [showFields, setShowFields] = useState<Record<string, boolean>>({});
const [databaseConnection, setDatabaseConnection] = useState<any>({});
useSetContainerSize("md");
useGetProject(project_id || "");
 
const {
getData: getDatabase,
data: databaseData,
loading: isLoadingDatabase,
success: successDatabase,
} = useGet();
 
const {
uploadData: deleteDatabase,
submitting: deletingDatabase,
success: deletedDatabaseSuccess,
} = usePost();
 
const {
uploadData: disableDatabase,
submitting: disablingDatabase,
success: disabledDatabaseSuccess,
} = usePost();
 
const {
uploadData: enableDatabase,
submitting: enablingDatabase,
success: enabledDatabaseSuccess,
} = usePost();
 
const {
uploadData: resetDatabase,
submitting: resettingDatabase,
success: resetDatabaseSuccess,
} = usePost();
 
const {
uploadData: changePasswordDatabase,
submitting: changingPasswordDatabase,
success: changedPasswordDatabaseSuccess,
} = usePost();
 
const navigate = useNavigate();
 
useEffect(() => {
getDatabase({
api: `${DATABASE_API_URL}/databases/${database_id}`,
isExternal: true,
});
}, [
database_id,
changedPasswordDatabaseSuccess,
resetDatabaseSuccess,
disabledDatabaseSuccess,
enabledDatabaseSuccess,
]);
 
useEffect(() => {
if (successDatabase) {
setDatabase(databaseData?.data?.database);
setDatabaseConnection(getConnectionString(databaseData?.data?.database));
}
}, [successDatabase]);
 
const handleDelete = () => {
deleteDatabase({
id: database?.id,
api: `${DATABASE_API_URL}/databases`,
method: "DELETE",
isExternal: true,
successMessage: "Database deleted successfully",
});
};
 
const handleDisable = () => {
disableDatabase({
api: `${DATABASE_API_URL}/databases/${database?.id}/disable`,
isExternal: true,
});
};
 
const handleEnable = () => {
enableDatabase({
api: `${DATABASE_API_URL}/databases/${database?.id}/enable`,
isExternal: true,
});
};
const handleReset = () => {
resetDatabase({
api: `${DATABASE_API_URL}/databases/${database?.id}/reset`,
isExternal: true,
successMessage: "Database reset successfully",
});
};
 
const handleChangePassword = () => {
if (changePasswordForm.password !== changePasswordForm.confirm_password) {
showNotification({
title: "Opps!",
message: "Passwords do not match",
color: "red",
});
return;
}
changePasswordDatabase({
api: `${DATABASE_API_URL}/databases/${database?.id}/reset_password`,
params: { password: changePasswordForm.password },
isExternal: true,
successMessage: "Password changed successfully",
});
};
 
useEffect(() => {
if (deletedDatabaseSuccess) {
navigate(`/projects/${project_id}/databases`);
}
}, [deletedDatabaseSuccess]);
 
useEffect(() => {
if (changedPasswordDatabaseSuccess) {
setChangePasswordConfirmOpened(false);
}
if (resetDatabaseSuccess) {
setResetConfirmOpened(false);
}
if (disabledDatabaseSuccess) {
setDisableConfirmOpened(false);
}
if (enabledDatabaseSuccess) {
setEnableConfirmOpened(false);
}
}, [
changedPasswordDatabaseSuccess,
resetDatabaseSuccess,
disabledDatabaseSuccess,
enabledDatabaseSuccess,
]);
 
const projectInfo = [
{
label: "Name",
value: database?.name,
},
{
label: "Type",
value: (
<Group gap="xs" align="center">
{database?.database_flavour_name === "postgres" ? (
<BiLogoPostgresql size={16} color="#0064a5" />
) : (
<TbBrandMysql size={16} color="#00758f" />
)}
<Text size="sm" fw={500}>
{beautify(database?.database_flavour_name)}
</Text>
</Group>
),
},
{
label: "Status",
value: getDatabaseStatus(database?.db_status),
},
{
label: "Age",
value: moment(database?.date_created).fromNow(),
},
{
label: "Used",
value: database?.default_storage_kb || "N/A",
},
{
label: "Allocated",
value: database?.allocated_size_kb
? `${numberFormat(database?.allocated_size_kb)} KB`
: "N/A",
},
];
 
const connectionInfo = [
{
label: "Host",
value: database?.host,
},
{
label: "Port",
value: database?.port,
},
{
label: "Database",
value: database?.name,
},
{
label: "User",
value: database?.user,
},
{
label: "Password",
value: database?.password,
hidden: true,
},
{
label: "Connection String",
value: databaseConnection,
hidden: true,
},
];
 
return (
<Stack gap={30}>
<Stack gap={0}>
<TitleText>Database Details</TitleText>
{isLoadingDatabase ? (
<DatabaseDetailsSkeleton />
) : (
<Card p="lg" radius="md" withBorder>
<Grid>
{projectInfo.map((info) => (
<Grid.Col span={{ base: 6, md: 4, lg: 4 }}>
<Flex>
<Stack gap={1}>
<Text className="subtitle">{info.label}</Text>
<Text size="sm">{info.value}</Text>
</Stack>
</Flex>
</Grid.Col>
))}
</Grid>
</Card>
)}
</Stack>
<Stack gap={0}>
<TitleText>Database Connection</TitleText>
{isLoadingDatabase ? (
<DatabaseConnectionSkeleton />
) : (
<Card p="lg" radius="md" withBorder>
<Stack gap="sm">
{connectionInfo.map((info) => (
<Flex
key={info.label}
justify="space-between"
align="center"
w="100%"
gap={5}
>
<Text className="subtitle" w="20%">
{info.label}
</Text>
<Group gap="xs" w="80%">
{info.hidden && (
<Tooltip
label={showFields[info.label] ? "Hide" : "Show"}
withArrow
position="right"
>
<ActionIcon
variant="default"
style={{ cursor: "pointer" }}
onClick={() =>
setShowFields((prev) => ({
...prev,
[info.label]: !prev[info.label],
}))
}
>
{showFields[info.label] ? (
<AiOutlineEyeInvisible size={16} />
) : (
<AiOutlineEye size={16} />
)}
</ActionIcon>
</Tooltip>
)}
<Tooltip
label={clipboard.copied ? "Copied" : "Copy"}
position="right"
withArrow
>
<Input
value={info.value}
readOnly
variant="filled"
style={{ flex: 1 }}
styles={{
input: {
cursor: "pointer",
outline: "none",
border: "none",
},
}}
type={
info.hidden
? showFields[info.label]
? "text"
: "password"
: "text"
}
rightSection={<TbCopy />}
rightSectionProps={{
onClick: () => {
clipboard.copy(info.value);
},
}}
onClick={() => {
clipboard.copy(info.value);
}}
/>
</Tooltip>
</Group>
</Flex>
))}
</Stack>
</Card>
)}
</Stack>
<Stack gap={0}>
<TitleText>Danger Zone</TitleText>
<Card p="lg" radius="md" withBorder>
<Stack gap={10}>
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Change Password</Text>
<Text className="subtext">
Update the password for the database
</Text>
</Stack>
<Button
variant="outline"
onClick={() => setChangePasswordConfirmOpened(true)}
leftSection={<LiaExchangeAltSolid />}
>
Change Password
</Button>
</Group>
<Divider />
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Reset Database</Text>
<Text className="subtext">
Delete all data inside this database and restore it to its
initial state.
</Text>
</Stack>
<Button
variant="outline"
color="red"
onClick={() => setResetConfirmOpened(true)}
leftSection={<RiResetLeftLine />}
>
Reset
</Button>
</Group>
<Divider />
{database?.status === "disabled" ? (
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Enable Database</Text>
<Text className="subtext">
Enable the database to allow access to resources.
</Text>
</Stack>
<Button
variant="outline"
color="green"
onClick={() => setEnableConfirmOpened(true)}
leftSection={<HiLockOpen />}
>
Enable
</Button>
</Group>
) : (
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Disable Database</Text>
<Text className="subtext">
This will temporary disable the database. .
</Text>
</Stack>
<Button
variant="outline"
color="red"
onClick={() => setDisableConfirmOpened(true)}
leftSection={<HiLockClosed />}
>
Disable
</Button>
</Group>
)}
<Divider />
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Delete Database</Text>
<Text className="subtext">
This action is irreversible and will delete the database
permanently.
</Text>
</Stack>
<Button
variant="outline"
color="red"
onClick={() => setDeleteConfirmOpened(true)}
leftSection={<HiTrash />}
>
Delete
</Button>
</Group>
</Stack>
<ModalConfirm
opened={deleteConfirmOpened}
onClose={() => setDeleteConfirmOpened(false)}
title="Delete Database"
buttonColor="red"
buttonText="Delete"
onConfirm={handleDelete}
loading={deletingDatabase}
leftSection={<HiTrash />}
>
Are you sure you want to delete <b>{database?.name}</b> database
permanently? Destroy the entire database, delete all tables and data
inside them.
</ModalConfirm>
<ModalConfirm
opened={disableConfirmOpened}
onClose={() => setDisableConfirmOpened(false)}
title="Disable Database"
buttonText="Disable"
onConfirm={handleDisable}
loading={disablingDatabase}
buttonColor="red"
leftSection={<HiLockClosed />}
>
Are you sure you want to disable <b>{database?.name}</b> database?
This action will prevent the database contents from being accessed.
</ModalConfirm>
<ModalConfirm
opened={enableConfirmOpened}
onClose={() => setEnableConfirmOpened(false)}
title="Enable Database"
buttonText="Enable"
buttonColor="green"
onConfirm={handleEnable}
loading={enablingDatabase}
leftSection={<HiLockOpen />}
>
Are you sure you want to enable <b>{database?.name}</b> database?
This action will allow the database contents to be accessed.
</ModalConfirm>
<ModalConfirm
opened={resetConfirmOpened}
onClose={() => setResetConfirmOpened(false)}
title="Reset Database"
buttonText="Reset"
buttonColor="red"
leftSection={<RiResetLeftLine />}
onConfirm={handleReset}
loading={resettingDatabase}
>
Are you sure you want to reset <b>{database?.name}</b> database?
</ModalConfirm>
<ModalConfirm
opened={changePasswordConfirmOpened}
onClose={() => setChangePasswordConfirmOpened(false)}
title="Change Password"
buttonText="Change"
buttonColor="red"
onConfirm={handleChangePassword}
loading={changingPasswordDatabase}
>
<form onSubmit={handleChangePassword}>
<Stack gap={20}>
<PasswordInput
placeholder="New Password"
label="New Password"
name="password"
required
onChange={changePasswordOnChange}
leftSection={<MdOutlineLock />}
/>
<PasswordInput
placeholder="Confirm New Password"
label="Confirm New Password"
name="confirm_password"
required
onChange={changePasswordOnChange}
leftSection={<MdOutlineLock />}
/>
</Stack>
</form>
</ModalConfirm>
</Card>
</Stack>
</Stack>
);
};
 
export default DatabaseDetails;
 
const DatabaseDetailsSkeleton = () => (
<Card p="lg" radius="md" withBorder>
<Grid>
{[...Array(6)].map((_, i) => (
<Grid.Col key={i} span={{ base: 6, md: 4, lg: 4 }}>
<Flex>
<Stack gap={7}>
<Skeleton height={18} width={60} />
<Skeleton height={14} width={150} />
</Stack>
</Flex>
</Grid.Col>
))}
</Grid>
</Card>
);
 
const DatabaseConnectionSkeleton = () => (
<Card p="lg" radius="md" withBorder>
<Stack gap="sm">
{[...Array(6)].map((_, i) => (
<Flex key={i} justify="space-between" align="center" w="100%" gap={20}>
<Skeleton height={20} width="20%" />
<Group gap="xs" w="80%">
<Skeleton height={36} width="100%" />
</Group>
</Flex>
))}
</Stack>
</Card>
);