crane-cloud/frontend

View on GitHub
src/pages/Apps/AppSettingsPage.tsx

Summary

Maintainability
A
35 mins
Test Coverage
import { Table } from "@/components/Elements/CustomTable";
import { ModalConfirm } from "@/components/Elements/Modals";
import {
CreateSingleAppForm,
EnvironmentVariablesForm,
EnvironmentVariablesTable,
} from "@/components/Forms/CreateAppForm";
import TitleText from "@/components/TitleText";
import { API_APPS } from "@/utils/apis";
import {
convertArrayToObject,
convertObjectToArray,
dateFormat,
useGetApp,
} from "@/utils/helpers";
import useGet from "@/utils/useGet";
import usePost from "@/utils/usePost";
import {
Badge,
Button,
Card,
Divider,
Flex,
Grid,
Group,
Input,
Stack,
Tabs,
Text,
Tooltip,
TextInput,
} from "@mantine/core";
import { useContext, useEffect, useState } from "react";
import {
HiLockClosed,
HiLockOpen,
HiPencil,
HiPlus,
HiTrash,
} from "react-icons/hi2";
import { FiCalendar } from "react-icons/fi";
import { LiaDocker } from "react-icons/lia";
 
import { useNavigate, useParams } from "react-router-dom";
import { TbCheck, TbCopy } from "react-icons/tb";
import { useClipboard } from "@mantine/hooks";
import { useAuth } from "@/utils/AuthContext";
import { MenuContext } from "@/components/Layouts/DashboardLayout";
 
const AppSettingsPage = () => {
const { app_id } = useParams();
const { app, setRefresh } = useGetApp(app_id || "");
const { setContainerSize } = useContext(MenuContext);
 
useEffect(() => {
setContainerSize("md");
return () => {
setContainerSize("xl");
};
}, [setContainerSize]);
useEffect(() => {}, [app]);
return (
<div>
<Tabs defaultValue="general">
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="ci/cd">CI / CD</Tabs.Tab>
<Tabs.Tab value="deployments">Deployments</Tabs.Tab>
</Tabs.List>
 
<Tabs.Panel value="general" pt={10}>
<GeneralTab app={app} setRefresh={setRefresh} />
</Tabs.Panel>
<Tabs.Panel value="deployments" pt={10}>
<DeploymentsTab app={app} />
</Tabs.Panel>
<Tabs.Panel value="ci/cd" pt={10}>
<CICDTab app={app} />
</Tabs.Panel>
</Tabs>
</div>
);
};
 
export default AppSettingsPage;
 
const GeneralTab = ({
app,
setRefresh,
}: {
app: any;
setRefresh: (refresh: boolean) => void;
}) => {
const [deleteConfirmOpened, setDeleteConfirmOpened] = useState(false);
const [disableConfirmOpened, setDisableConfirmOpened] = useState(false);
const [enableConfirmOpened, setEnableConfirmOpened] = useState(false);
const [updateConfirmOpened, setUpdateConfirmOpened] = useState(false);
const [envVariablesConfirmOpened, setEnvVariablesConfirmOpened] =
useState(false);
const [envVariables, setEnvVariables] = useState(
convertObjectToArray(app?.env_vars) || [{ key: "", value: "" }]
);
 
const {
uploadData: deleteApp,
submitting: deletingApp,
success: deletedAppSuccess,
} = usePost();
const {
uploadData: disableApp,
submitting: disablingApp,
success: disabledAppSuccess,
} = usePost();
const {
uploadData: enableApp,
submitting: enablingApp,
success: enabledAppSuccess,
} = usePost();
const {
uploadData: addEnvVariables,
submitting: addingEnvVariables,
success: addedEnvVariablesSuccess,
} = usePost();
const navigate = useNavigate();
 
useEffect(() => {
if (deletedAppSuccess) {
navigate(`/projects/${app?.project_id}/apps`);
}
}, [deletedAppSuccess]);
 
useEffect(() => {
if (enabledAppSuccess || disabledAppSuccess) {
navigate(`/projects/${app?.project_id}/apps/${app?.id}`);
}
}, [enabledAppSuccess, disabledAppSuccess]);
 
useEffect(() => {
setRefresh(true);
}, [addedEnvVariablesSuccess]);
 
const submitEnvVariables = () => {
const envObject = convertArrayToObject(envVariables);
addEnvVariables({
api: "apps",
id: app?.id,
method: "PATCH",
params: {
env_vars: envObject,
},
});
setEnvVariablesConfirmOpened(false);
};
 
useEffect(() => {
setEnvVariables(convertObjectToArray(app?.env_vars));
}, [app]);
 
const handleDelete = () => {
deleteApp({
id: app?.id,
api: API_APPS,
method: "DELETE",
});
};
 
const handleDisable = () => {
disableApp({
api: `${API_APPS}/${app?.id}/disable`,
});
};
 
const handleEnable = () => {
enableApp({
api: `${API_APPS}/${app?.id}/enable`,
});
};
 
const appInfo = [
{
label: "Application Name",
value: app?.name,
},
{
label: "Alias",
value: app?.alias || "N/A",
},
{
label: "Image",
value: app?.image || "N/A",
},
{
label: "Application Status",
value: app?.app_status?.[0]?.status || "N/A",
},
{
label: "Replicas",
value: app?.replicas || "N/A",
},
{
label: "Age",
value: app?.age || "N/A",
},
{
label: "Port",
value: app?.port || "N/A",
},
{
label: "Entry Command",
value: app?.entry_command || "N/A",
},
];
 
const appSingleInfo = [
{
label: "Application Link",
value: app?.url || "N/A",
},
{
label: "Internal Link",
value: app?.internal_url || "N/A",
},
];
return (
<Stack gap={30}>
<Stack gap={0}>
<TitleText>App Details</TitleText>
<Card p="lg" radius="md" withBorder>
<Grid>
{appInfo.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.Col span={{ base: 12, md: 12, lg: 12 }}>
<Divider />
</Grid.Col>
{appSingleInfo.map((info) => (
<Grid.Col span={{ base: 12, md: 12, lg: 12 }}>
<Flex gap={20}>
<Text className="subtitle">{info.label}</Text>
<Text size="sm" flex={1}>
{info.value}
</Text>
</Flex>
</Grid.Col>
))}
</Grid>
</Card>
</Stack>
<Stack gap={0}>
<TitleText>Environment Variables</TitleText>
{Object.keys(app?.env_vars || {}).length > 0 ? (
<EnvironmentVariablesTable
envVariables={convertObjectToArray(app?.env_vars)}
/>
) : (
<Text className="subtext">
Use environment variables to store API keys or secrets.
</Text>
)}
<Flex justify="flex-end" mt="md">
<Button
variant="outline"
onClick={() => setEnvVariablesConfirmOpened(true)}
leftSection={
Object.keys(app?.env_vars || {}).length > 0 ? (
<HiPencil />
) : (
<HiPlus />
)
}
>
{Object.keys(app?.env_vars || {}).length > 0
? "Update Environment Variables"
: "Add Environment Variables"}
</Button>
</Flex>
</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">Update App</Text>
<Text className="subtext">
Modify the app name and description
</Text>
</Stack>
<Button
variant="outline"
onClick={() => setUpdateConfirmOpened(true)}
>
Update
</Button>
</Group>
<Divider />
{app.disabled ? (
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text className="title">Enable App</Text>
<Text className="subtext">
Enable app 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 App</Text>
<Text className="subtext">
Prevent app from being billed by blocking access to it's
resources.
</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 App</Text>
<Text className="subtext">
This action is irreversible and will delete the app
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 App"
buttonColor="red"
buttonText="Delete"
onConfirm={handleDelete}
loading={deletingApp}
leftSection={<HiTrash />}
>
Are you sure you want to delete <b>{app?.name}</b> app permanently?
This action cannot be undone.
</ModalConfirm>
<ModalConfirm
opened={disableConfirmOpened}
onClose={() => setDisableConfirmOpened(false)}
title="Disable App"
buttonText="Disable"
onConfirm={handleDisable}
loading={disablingApp}
buttonColor="red"
leftSection={<HiLockClosed />}
>
Are you sure you want to disable <b>{app?.name}</b> app? This action
will prevent the app contents from being accessed.
</ModalConfirm>
<ModalConfirm
opened={enableConfirmOpened}
onClose={() => setEnableConfirmOpened(false)}
title="Enable App"
buttonText="Enable"
buttonColor="green"
onConfirm={handleEnable}
loading={enablingApp}
leftSection={<HiLockOpen />}
>
Are you sure you want to enable <b>{app?.name}</b> app? This action
will allow the app contents to be accessed.
</ModalConfirm>
<ModalConfirm
opened={updateConfirmOpened}
onClose={() => setUpdateConfirmOpened(false)}
title="Update App"
buttonText="Update"
onConfirm={() => {}}
size="xl"
showFooterActions={false}
>
<CreateSingleAppForm
app={app}
showEnvs={false}
showTitle={false}
onCancel={() => setUpdateConfirmOpened(false)}
refresh={() => setRefresh(true)}
/>
</ModalConfirm>
 
<ModalConfirm
opened={envVariablesConfirmOpened}
onClose={() => setEnvVariablesConfirmOpened(false)}
title="Environment Variables"
buttonText="Add Variables"
size="xl"
onConfirm={submitEnvVariables}
>
<EnvironmentVariablesForm
envVariables={envVariables}
setEnvVariables={setEnvVariables}
loading={addingEnvVariables}
showTitle={false}
/>
</ModalConfirm>
</Card>
</Stack>
</Stack>
);
};
 
const DeploymentsTab = ({ app }: { app: any }) => {
const { data: revisionsData, getData: getRevisions, loading } = useGet();
 
useEffect(() => {
if (app?.id) {
getRevisions({
api: `${API_APPS}/${app?.id}/revisions`,
});
}
}, [app]);
 
const tableColumns = [
{ id: "revision_id", header: "Revision ID" },
{ id: "image", header: "Image" },
{ id: "replicas", header: "Replicas" },
{ id: "created_at", header: "Created At" },
];
const tableData = (data: any) => {
return data?.map((item: any) => ({
...item,
replicas: item.replicas || 1,
revision_id: (
<Stack gap={0} align="flex-start">
<Text className="subtext">{item.revision_id}</Text>
{item.current && (
<Badge color="green" size="xs" leftSection={<TbCheck />}>
Current
</Badge>
)}
</Stack>
),
image: (
<Flex gap={5} align="center">
<LiaDocker size={16} />
<Text className="subtext">{item.image}</Text>
</Flex>
),
created_at: (
<Stack gap={0} align="flex-end">
<Flex gap={5} align="center">
<FiCalendar size={13} />
<Text className="subtext">{dateFormat(item.created_at)}</Text>
</Flex>
<Text className="subtext" c="dimmed">
{dateFormat(item.created_at, "HH:mm A")}
</Text>
</Stack>
),
}));
};
 
return (
<div>
<TitleText>Deployments</TitleText>
<Table
verticalSpacing="sm"
columns={tableColumns}
data={tableData(revisionsData?.data?.revisions)}
props={{
verticalSpacing: "sm",
}}
showIndex={false}
rowHover
loading={loading}
/>
</div>
);
};
 
Function `CICDTab` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
const CICDTab = ({ app }: { app: any }) => {
const clipboard = useClipboard();
const { user } = useAuth();
const [imageTag, setImageTag] = useState("");
const [webhookUrl, setwebhookUrl] = useState("");
 
const isStaging = /localhost|staging/.test(window.location.href);
 
const generateWebhook = () => {
const tag = imageTag || "latest";
const defaultUrl = isStaging
? `https://staging-api.cranecloud.io/apps/${app?.id}/${user?.id}/docker`
: `https://api.cranecloud.io/apps/${app?.id}/${user?.id}/docker`;
if (app?.id && user?.id) {
setwebhookUrl(`${defaultUrl}/docker/${tag}/webhook`);
}
};
 
return (
<div>
<Stack gap={30}>
<Stack gap={10}>
<TitleText>Set up Continous Integration</TitleText>
<Card p="lg" radius="md" withBorder>
<Stack gap={20}>
<Stack gap={1}>
<Text size="md" fw={700}>
Specify Image Tag
</Text>
<Text size="sm">
Specify Image Tag for your image and if none is provided{" "}
<b>"Latest"</b> will be chosen as the default tag.
</Text>
</Stack>
<TextInput
placeholder="Enter Image Tag"
value={imageTag}
onChange={(e) => setImageTag(e.target.value)}
/>
<Stack gap={10}>
<Divider />
<Flex justify="space-between" gap={20}>
<Stack gap={1}>
<Text size="md" fw={700}>
Generate Link
</Text>
<Text size="sm">
To Generate a link with a token click the Generate link
button.
</Text>
</Stack>
<Button variant="outline" onClick={generateWebhook}>
Generate Link
</Button>
</Flex>
{webhookUrl && (
<Stack gap={10}>
<Flex
justify="space-between"
align="center"
gap={30}
mt={10}
>
<Text className="subtitle">Webhook URL</Text>
<Text size="sm" flex={1}>
{imageTag}
</Text>
</Flex>
<Flex justify="space-between" align="center" gap={30}>
<Text className="subtitle">Image Tag</Text>
<Text size="sm" flex={1}>
<Tooltip
label={clipboard.copied ? "Copied" : "Copy"}
position="bottom"
withArrow
>
<Input
value={webhookUrl}
readOnly
variant="filled"
style={{ flex: 1 }}
styles={{
input: {
cursor: "pointer",
outline: "none",
border: "none",
wrap: "wrap",
},
}}
rightSection={<TbCopy />}
onClick={() => clipboard.copy(webhookUrl)}
/>
</Tooltip>
</Text>
</Flex>
</Stack>
)}
</Stack>
</Stack>
</Card>
</Stack>
<Stack gap={10}>
<TitleText>Add link to Dockerhub</TitleText>
<Card p="lg" radius="md" withBorder>
<Stack gap={10}>
<Text size="sm">
1. To add the webhook, copy the generated link above and head to
<b>dockerhub.com</b> under the repository of your docker image.
</Text>
<Text size="sm">
2. On the image repository click the <b>Webhooks</b> tab.
</Text>
<Text size="sm">
3. Under new Webhook give your webhook any name of your choice
and add the genereated URL token from Crane Cloud and click
create.
</Text>
<Text size="sm">
4. Continous Integration has been successfully been added for
your application.
</Text>
</Stack>
</Card>
</Stack>
</Stack>
</div>
);
};