cleanup, style picler
This commit is contained in:
1 parent
a80df60fc1
commit
f799d46025
16 files changed
+52
-1079
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { MuiColorInput } from "mui-color-input";
|
||||
import type { MuiColorInputProps } from "mui-color-input";
|
||||
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
||||
import { useTheme } from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "@/Components/v2/inputs/FieldLabel";
|
||||
|
||||
interface ColorPickerProps extends MuiColorInputProps {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps) => {
|
||||
const theme = useTheme();
|
||||
const input = (
|
||||
<MuiColorInput
|
||||
{...props}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{input}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
@@ -13,3 +13,4 @@ export * from "./Radio";
|
||||
export * from "./Switch";
|
||||
export * from "./Slider";
|
||||
export * from "./ImageUpload";
|
||||
export * from "./ColorPicker";
|
||||
@@ -1,110 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
|
||||
// Utils
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import { useTheme } from "@emotion/react";
|
||||
const MonitorListItem = ({
|
||||
monitor,
|
||||
innerRef,
|
||||
draggableProps,
|
||||
dragHandleProps,
|
||||
onDelete,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
ref={innerRef}
|
||||
gap={theme.spacing(4)}
|
||||
margin={theme.spacing(4)}
|
||||
padding={theme.spacing(4)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
alignItems={"center"}
|
||||
justifyContent={"start"}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
>
|
||||
<Icon
|
||||
name="GripVertical"
|
||||
size={20}
|
||||
/>
|
||||
<Typography>{monitor.name}</Typography>
|
||||
<Icon
|
||||
name="Trash2"
|
||||
size={20}
|
||||
style={{ marginLeft: "auto", cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
onDelete(monitor);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
const onDelete = (monitorToDelete) => {
|
||||
const newMonitors = selectedMonitors.filter(
|
||||
(monitor) => monitor.id !== monitorToDelete.id
|
||||
);
|
||||
setSelectedMonitors(newMonitors);
|
||||
};
|
||||
const reorder = (list, startIndex, endIndex) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
// dropped outside the list
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedMonitors = reorder(
|
||||
selectedMonitors,
|
||||
result.source.index,
|
||||
result.destination.index
|
||||
);
|
||||
|
||||
setSelectedMonitors(reorderedMonitors);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided, snapshot) => (
|
||||
<Stack
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{selectedMonitors?.map((monitor, index) => (
|
||||
<Draggable
|
||||
key={monitor.id}
|
||||
draggableId={monitor.id}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<MonitorListItem
|
||||
monitor={monitor}
|
||||
innerRef={provided.innerRef}
|
||||
draggableProps={provided.draggableProps}
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorList;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Button, Box } from "@mui/material";
|
||||
import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "../../../../../Utils/fileUtils.js";
|
||||
const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => {
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ProgressUpload
|
||||
icon={
|
||||
<Icon
|
||||
name="Image"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
label={logo?.name}
|
||||
size={formatBytes(logo?.size)}
|
||||
progress={progressValue}
|
||||
onClick={removeLogo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (logo && logoType) {
|
||||
return (
|
||||
<Box
|
||||
width="fit-content"
|
||||
alignSelf="center"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={removeLogo}
|
||||
>
|
||||
{t("removeLogo")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Progress;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
// This can be used to add any extra/additional section/stacks on top of existing sections on the tab
|
||||
const ConfigStack = ({ title, description, children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography component="p">{description}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigStack.propTypes = {
|
||||
title: PropTypes.string.isRequired, // Title must be a string and is required
|
||||
description: PropTypes.string.isRequired, // Description must be a string and is required
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default ConfigStack;
|
||||
@@ -1,107 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { TabPanel } from "@mui/lab";
|
||||
import MonitorList from "../MonitorList/index.jsx";
|
||||
import Search from "@/Components/v1/Inputs/Search/index.jsx";
|
||||
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ConfigStack from "./ConfigStack.jsx";
|
||||
const Content = ({
|
||||
tabValue,
|
||||
form,
|
||||
monitors,
|
||||
handleFormChange,
|
||||
errors,
|
||||
selectedMonitors,
|
||||
setSelectedMonitors,
|
||||
}) => {
|
||||
// Local state
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Handlers
|
||||
const handleMonitorsChange = (selectedMonitors) => {
|
||||
handleFormChange({
|
||||
target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) },
|
||||
});
|
||||
setSelectedMonitors(selectedMonitors);
|
||||
};
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TabPanel value={tabValue}>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<ConfigStack
|
||||
title={t("statusPageCreateTabsContent")}
|
||||
description={t("statusPageCreateTabsContentDescription")}
|
||||
>
|
||||
<Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
multiple={true}
|
||||
filteredBy="name"
|
||||
value={selectedMonitors}
|
||||
inputValue={search}
|
||||
handleInputChange={setSearch}
|
||||
handleChange={handleMonitorsChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{errors["monitors"]}
|
||||
</Typography>
|
||||
<MonitorList
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={handleMonitorsChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigStack>
|
||||
<ConfigStack
|
||||
title={t("features")}
|
||||
description={t("statusPageCreateTabsContentFeaturesDescription")}
|
||||
>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
id="showCharts"
|
||||
name="showCharts"
|
||||
label={t("showCharts")}
|
||||
isChecked={form.showCharts}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<Checkbox
|
||||
id="showUptimePercentage"
|
||||
name="showUptimePercentage"
|
||||
label={t("showUptimePercentage")}
|
||||
isChecked={form.showUptimePercentage}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<Checkbox
|
||||
id="showAdminLoginLink"
|
||||
name="showAdminLoginLink"
|
||||
label={t("showAdminLoginLink")}
|
||||
isChecked={form.showAdminLoginLink}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigStack>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
||||
@@ -1,187 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { TabPanel } from "@mui/lab";
|
||||
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
|
||||
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
|
||||
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
|
||||
import Search from "@/Components/v1/Inputs/Search/index.jsx";
|
||||
import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx";
|
||||
import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx";
|
||||
import Progress from "../Progress/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import timezones from "../../../../../Utils/timezones.json";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState, useCallback } from "react";
|
||||
|
||||
const TabSettings = ({
|
||||
isCreate,
|
||||
tabValue,
|
||||
form,
|
||||
handleFormChange,
|
||||
handleImageChange,
|
||||
progress,
|
||||
removeLogo,
|
||||
errors,
|
||||
}) => {
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [rawInput, setRawInput] = useState("");
|
||||
|
||||
const selectedTimezone = useMemo(
|
||||
() => timezones.find((tz) => tz._id === form.timezone) ?? null,
|
||||
[form.timezone]
|
||||
);
|
||||
|
||||
const handleTimezoneChange = useCallback(
|
||||
(newValue) => {
|
||||
setRawInput("");
|
||||
handleFormChange({
|
||||
target: {
|
||||
name: "timezone",
|
||||
value: newValue?._id ?? "",
|
||||
},
|
||||
});
|
||||
},
|
||||
[handleFormChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabPanel value={tabValue}>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<ConfigBox>
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("access")}
|
||||
</Typography>
|
||||
<Typography component="p">{t("statusPageCreateSettings")}</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<Checkbox
|
||||
id="publish"
|
||||
name="isPublished"
|
||||
label={t("statusPageCreateSettingsCheckboxLabel")}
|
||||
isChecked={form.isPublished}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("basicInformation")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateBasicInfoDescription")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<TextInput
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
label={t("companyName")}
|
||||
value={form.companyName}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["companyName"]}
|
||||
error={errors["companyName"] ? true : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
disabled={!isCreate}
|
||||
label={t("statusPageCreateBasicInfoStatusPageAddress")}
|
||||
value={form.url}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["url"]}
|
||||
error={errors["url"] ? true : false}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("timezone")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateSelectTimeZoneDescription")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Search
|
||||
id="timezone"
|
||||
label={t("settingsDisplayTimezone")}
|
||||
options={timezones}
|
||||
filteredBy="name"
|
||||
value={selectedTimezone}
|
||||
inputValue={rawInput}
|
||||
handleInputChange={(newVal) => setRawInput(newVal)}
|
||||
handleChange={handleTimezoneChange}
|
||||
isAdorned={true}
|
||||
unit="timezone"
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("statusPageCreateAppearanceTitle")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateAppearanceDescription")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<ImageUpload
|
||||
src={form?.logo?.src}
|
||||
onChange={handleImageChange}
|
||||
previewIsRound={false}
|
||||
/>
|
||||
<Progress
|
||||
isLoading={progress.isLoading}
|
||||
progressValue={progress.value}
|
||||
logo={form.logo}
|
||||
logoType={form.logo?.type}
|
||||
removeLogo={removeLogo}
|
||||
/>
|
||||
<ColorPicker
|
||||
id="color"
|
||||
name="color"
|
||||
value={form.color}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
TabSettings.propTypes = {
|
||||
isCreate: PropTypes.bool,
|
||||
tabValue: PropTypes.string,
|
||||
form: PropTypes.object,
|
||||
handleFormChange: PropTypes.func,
|
||||
handleImageChange: PropTypes.func,
|
||||
progress: PropTypes.object,
|
||||
removeLogo: PropTypes.func,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TabSettings;
|
||||
@@ -1,99 +0,0 @@
|
||||
// Components
|
||||
import { TabContext } from "@mui/lab";
|
||||
import { Tab } from "@mui/material";
|
||||
import Settings from "./Settings.jsx";
|
||||
import Content from "./Content.jsx";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import CustomTabList from "@/Components/v1/Tab/index.jsx";
|
||||
|
||||
const Tabs = ({
|
||||
isCreate,
|
||||
form,
|
||||
errors,
|
||||
monitors,
|
||||
selectedMonitors,
|
||||
setSelectedMonitors,
|
||||
handleFormChange,
|
||||
handleImageChange,
|
||||
progress,
|
||||
removeLogo,
|
||||
tab,
|
||||
setTab,
|
||||
TAB_LIST,
|
||||
handleDelete,
|
||||
isDeleteOpen,
|
||||
setIsDeleteOpen,
|
||||
isDeleting,
|
||||
isLoading,
|
||||
}) => {
|
||||
return (
|
||||
<TabContext value={TAB_LIST[tab]}>
|
||||
<CustomTabList
|
||||
onChange={(_, selected) => {
|
||||
setTab(TAB_LIST.indexOf(selected));
|
||||
}}
|
||||
aria-label="status page tabs"
|
||||
>
|
||||
{TAB_LIST.map((tabLabel) => (
|
||||
<Tab
|
||||
key={tabLabel}
|
||||
label={tabLabel}
|
||||
value={tabLabel}
|
||||
/>
|
||||
))}
|
||||
</CustomTabList>
|
||||
{tab === 0 ? (
|
||||
<Settings
|
||||
tabValue={TAB_LIST[0]}
|
||||
form={form}
|
||||
handleFormChange={handleFormChange}
|
||||
handleImageChange={handleImageChange}
|
||||
progress={progress}
|
||||
removeLogo={removeLogo}
|
||||
errors={errors}
|
||||
isCreate={isCreate}
|
||||
handleDelete={handleDelete}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
isDeleting={isDeleting}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : (
|
||||
<Content
|
||||
tabValue={TAB_LIST[1]}
|
||||
form={form}
|
||||
monitors={monitors}
|
||||
handleFormChange={handleFormChange}
|
||||
errors={errors}
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={setSelectedMonitors}
|
||||
/>
|
||||
)}
|
||||
</TabContext>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.propTypes = {
|
||||
isCreate: PropTypes.bool,
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
monitors: PropTypes.array,
|
||||
selectedMonitors: PropTypes.array,
|
||||
setSelectedMonitors: PropTypes.func,
|
||||
handleFormChange: PropTypes.func,
|
||||
handleImageChange: PropTypes.func,
|
||||
progress: PropTypes.object,
|
||||
removeLogo: PropTypes.func,
|
||||
tab: PropTypes.number,
|
||||
setTab: PropTypes.func,
|
||||
TAB_LIST: PropTypes.array,
|
||||
handleDelete: PropTypes.func,
|
||||
isDeleteOpen: PropTypes.bool,
|
||||
setIsDeleteOpen: PropTypes.func,
|
||||
isDeleting: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
|
||||
const useCreateStatusPage = (isCreate) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const createStatusPage = async ({ form, id }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await networkService.createStatusPage({ form, isCreate, id });
|
||||
return true;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error?.response?.data?.msg ?? error.message });
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [createStatusPage, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useCreateStatusPage };
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
|
||||
const useMonitorsFetch = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
limit: null, // donot return any checks for the monitors
|
||||
types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection
|
||||
});
|
||||
setMonitors(response.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [user]);
|
||||
|
||||
return [monitors, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useMonitorsFetch };
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Autocomplete,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
ColorInput,
|
||||
} from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -29,7 +30,6 @@ import type { StatusPageResponse } from "@/Types/StatusPage";
|
||||
import timezones from "@/Utils/timezones.json";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import { MuiColorInput } from "mui-color-input";
|
||||
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
||||
|
||||
interface TimezoneOption {
|
||||
@@ -393,11 +393,11 @@ const CreateStatusPage = () => {
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<MuiColorInput
|
||||
<ColorInput
|
||||
format="hex"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
label={t("pages.statusPages.form.appearance.option.color.label")}
|
||||
fieldLabel={t("pages.statusPages.form.appearance.option.color.label")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Button, Typography } from "@mui/material";
|
||||
import Tabs from "./Components/Tabs/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import SkeletonLayout from "./Components/Skeleton/index.jsx";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { statusPageValidation } from "../../../Validation/validation.js";
|
||||
import { buildErrors } from "../../../Validation/error.js";
|
||||
import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx";
|
||||
import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx";
|
||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
//Constants
|
||||
const ERROR_TAB_MAPPING = [
|
||||
["companyName", "url", "timezone", "color", "isPublished", "logo"],
|
||||
["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"],
|
||||
];
|
||||
|
||||
const CreateStatusPage = () => {
|
||||
const { url } = useParams();
|
||||
//Local state
|
||||
const [tab, setTab] = useState(0);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
isPublished: false,
|
||||
companyName: "",
|
||||
url: url ?? Math.floor(Math.random() * 1000000).toFixed(0),
|
||||
logo: undefined,
|
||||
timezone: "America/Toronto",
|
||||
color: "#4169E1",
|
||||
type: "uptime",
|
||||
monitors: [],
|
||||
showCharts: true,
|
||||
showUptimePercentage: true,
|
||||
showAdminLoginLink: false,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [selectedMonitors, setSelectedMonitors] = useState([]);
|
||||
// Refs
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
// Setup
|
||||
const isCreate = typeof url === "undefined";
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, isLoading, networkError] = useMonitorsFetch();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [createStatusPage] = useCreateStatusPage(isCreate);
|
||||
const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] =
|
||||
useStatusPageFetch(isCreate, url);
|
||||
// const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
|
||||
|
||||
// Handlers
|
||||
const handleFormChange = (e) => {
|
||||
let { type, name, value, checked } = e.target;
|
||||
// Handle errors
|
||||
const { error } = statusPageValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, name, error);
|
||||
});
|
||||
|
||||
//Handle checkbox
|
||||
if (type === "checkbox") {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: checked,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle other inputs
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleImageChange = useCallback((fileObj) => {
|
||||
if (!fileObj || !fileObj.file) return;
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: {
|
||||
src: fileObj.src,
|
||||
name: fileObj.name,
|
||||
type: fileObj.file.type,
|
||||
size: fileObj.file.size,
|
||||
},
|
||||
}));
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
}, []);
|
||||
|
||||
const removeLogo = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: undefined,
|
||||
}));
|
||||
// interrupt interval if image upload is canceled prior to completing the process
|
||||
clearInterval(intervalRef.current);
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle status page deletion with optimistic UI update
|
||||
* Immediately navigates away without waiting for the deletion to complete
|
||||
* to prevent unnecessary network requests for the deleted page
|
||||
*/
|
||||
const handleDelete = async () => {
|
||||
setIsDeleteOpen(false);
|
||||
// Start deletion process but don't wait for it
|
||||
// deleteStatusPage();
|
||||
// Immediately navigate away to prevent additional fetches for the deleted page
|
||||
navigate("/status");
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let toSubmit = {
|
||||
...form,
|
||||
logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
|
||||
};
|
||||
|
||||
const { error } = statusPageValidation.validate(toSubmit, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (typeof error === "undefined") {
|
||||
const success = await createStatusPage({ form, id: statusPage?.id });
|
||||
if (success) {
|
||||
createToast({
|
||||
body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"),
|
||||
});
|
||||
navigate(`/status/uptime/${form.url}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newErrors = {};
|
||||
error?.details?.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors((prev) => ({ ...prev, ...newErrors }));
|
||||
const errorTabs = Object.keys(newErrors).map((err) => {
|
||||
return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err));
|
||||
});
|
||||
|
||||
// If there's an error in the current tab, don't change the tab
|
||||
if (errorTabs.some((errorTab) => errorTab === tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get -1, there's an unknown error
|
||||
if (errorTabs[0] === -1) {
|
||||
createToast({ body: t("common.toasts.unknownError") });
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise go to tab with error
|
||||
setTab(errorTabs[0]);
|
||||
};
|
||||
|
||||
// If we are configuring, populate fields
|
||||
useEffect(() => {
|
||||
if (isCreate) return;
|
||||
if (typeof statusPage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
let newLogo = undefined;
|
||||
if (statusPage.logo && Object.keys(statusPage.logo).length > 0) {
|
||||
newLogo = {
|
||||
src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
|
||||
name: "logo",
|
||||
type: statusPage.logo.contentType,
|
||||
size: null,
|
||||
};
|
||||
}
|
||||
setForm((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
companyName: statusPage?.companyName,
|
||||
isPublished: statusPage?.isPublished,
|
||||
timezone: statusPage?.timezone,
|
||||
monitors: statusPageMonitors.map((monitor) => monitor.id),
|
||||
color: statusPage?.color,
|
||||
logo: newLogo,
|
||||
showCharts: statusPage?.showCharts ?? true,
|
||||
showUptimePercentage: statusPage?.showUptimePercentage ?? true,
|
||||
showAdminLoginLink: statusPage?.showAdminLoginLink ?? false,
|
||||
};
|
||||
});
|
||||
setSelectedMonitors(statusPageMonitors);
|
||||
}, [isCreate, statusPage, statusPageMonitors]);
|
||||
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <SkeletonLayout />;
|
||||
|
||||
// Load fields
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: t("statusBreadCrumbsStatusPages", "Status"), path: "/status" },
|
||||
{
|
||||
name: t("statusBreadCrumbsDetails", "Details"),
|
||||
path: `/status/uptime/${url}`,
|
||||
},
|
||||
{ name: t("configure", "Configure"), path: `/status/create/${url}` },
|
||||
]}
|
||||
/>
|
||||
{!isCreate && (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
// loading={isDeleting}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsDeleteOpen(true)}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
<Dialog
|
||||
title={t("deleteStatusPage")}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setIsDeleteOpen(false)}
|
||||
open={isDeleteOpen}
|
||||
confirmationButtonLabel={t("deleteStatusPageConfirm")}
|
||||
description={t("deleteStatusPageDescription")}
|
||||
// isLoading={isDeleting || statusPageIsLoading}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Tabs
|
||||
form={form}
|
||||
errors={errors}
|
||||
monitors={monitors}
|
||||
selectedMonitors={selectedMonitors}
|
||||
setSelectedMonitors={setSelectedMonitors}
|
||||
handleFormChange={handleFormChange}
|
||||
handleImageChange={handleImageChange}
|
||||
progress={progress}
|
||||
removeLogo={removeLogo}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
TAB_LIST={[
|
||||
t("statusPage.generalSettings", "General settings"),
|
||||
t("statusPage.contents", "Contents"),
|
||||
]}
|
||||
isCreate={isCreate}
|
||||
handleDelete={handleDelete}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
// isDeleting={isDeleting}
|
||||
isLoading={statusPageIsLoading}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t("statusPageCreate.buttonSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusPage;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../../../../Utils/NetworkService.js";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Hook for deleting a status page with optimistic UI update
|
||||
* @param {Function} fetchStatusPage - Function to fetch status page data
|
||||
* @param {string} url - URL of the status page
|
||||
* @returns {Array} - [deleteStatusPage function, isLoading state]
|
||||
*/
|
||||
const useStatusPageDelete = (fetchStatusPage, url) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Delete a status page with optimistic UI update
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
const deleteStatusPage = async () => {
|
||||
// We don't need to call fetchStatusPage after deletion
|
||||
// This prevents the 404 error when trying to fetch a deleted status page
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteStatusPage({ url });
|
||||
createToast({
|
||||
body: t("statusPage.deleteSuccess", "Status page deleted successfully"),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: t("statusPage.deleteFailed", "Failed to delete status page"),
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [deleteStatusPage, isLoading];
|
||||
};
|
||||
|
||||
export { useStatusPageDelete };
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js";
|
||||
|
||||
const useStatusPageFetch = (isCreate = false, url) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [statusPage, setStatusPage] = useState(undefined);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const theme = useTheme();
|
||||
const { getMonitorWithPercentage } = useMonitorUtils();
|
||||
const fetchStatusPage = useCallback(async () => {
|
||||
try {
|
||||
const response = await networkService.getStatusPageByUrl({
|
||||
url,
|
||||
type: "uptime",
|
||||
});
|
||||
if (!response?.data?.data) return;
|
||||
const { statusPage, monitors } = response.data.data;
|
||||
setStatusPage(statusPage);
|
||||
|
||||
const monitorsWithPercentage = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
|
||||
setMonitors(monitorsWithPercentage);
|
||||
} catch (error) {
|
||||
// If there is a 404, status page is not found
|
||||
if (error?.response?.status === 404) {
|
||||
setStatusPage(undefined);
|
||||
return;
|
||||
}
|
||||
createToast({ body: error.message });
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [theme, getMonitorWithPercentage, url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreate === true) {
|
||||
return;
|
||||
}
|
||||
fetchStatusPage();
|
||||
}, [isCreate, fetchStatusPage]);
|
||||
|
||||
return [statusPage, monitors, isLoading, networkError, fetchStatusPage];
|
||||
};
|
||||
|
||||
export { useStatusPageFetch };
|
||||
@@ -359,7 +359,6 @@
|
||||
"errors": "Errors",
|
||||
"expectedValue": "Expected value",
|
||||
"failedToSendEmail": "Failed to send email",
|
||||
"features": "Features",
|
||||
"FirstName": "First name",
|
||||
"frequency": "Frequency",
|
||||
"friendlyNameInput": "Friendly name",
|
||||
@@ -1044,7 +1043,6 @@
|
||||
},
|
||||
"rate": "Rate",
|
||||
"remove": "Remove",
|
||||
"removeLogo": "Remove Logo",
|
||||
"repeat": "Repeat",
|
||||
"reset": "Reset",
|
||||
"response": "RESPONSE",
|
||||
@@ -1058,7 +1056,6 @@
|
||||
},
|
||||
"save": "Save",
|
||||
"selectAll": "Select all",
|
||||
"settingsDisplayTimezone": "Display timezone",
|
||||
"settingsFailedToClearStats": "Failed to clear stats",
|
||||
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
|
||||
"settingsFailedToSave": "Failed to save settings",
|
||||
@@ -1151,25 +1148,18 @@
|
||||
"settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}",
|
||||
"settingsTestEmailSuccess": "Test email sent successfully",
|
||||
"settingsTestEmailUnknownError": "Unknown error",
|
||||
"showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page",
|
||||
"showCharts": "Show charts",
|
||||
"shown": "Shown",
|
||||
"showUptimePercentage": "Show uptime percentage",
|
||||
"starPromptDescription": "See the latest releases and help grow the community on GitHub",
|
||||
"starPromptTitle": "Star Checkmate",
|
||||
"startTime": "Start time",
|
||||
"state": "State",
|
||||
"status": "Status",
|
||||
"statusBreadCrumbsDetails": "Details",
|
||||
"statusBreadCrumbsStatusPages": "Status Pages",
|
||||
"statusCode": "Status code",
|
||||
"statusPage": {
|
||||
"contents": "Contents",
|
||||
"createSuccess": "Status page created successfully",
|
||||
"deleteFailed": "Failed to delete status page",
|
||||
"deleteSuccess": "Status page deleted successfully",
|
||||
"generalSettings": "General settings",
|
||||
"updateSuccess": "Status page updated successfully",
|
||||
"details": {
|
||||
"statusHeader": {
|
||||
"allUp": "All systems operational",
|
||||
@@ -1181,16 +1171,6 @@
|
||||
"statusPageCreate": {
|
||||
"buttonSave": "Save"
|
||||
},
|
||||
"statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.",
|
||||
"statusPageCreateAppearanceTitle": "Appearance",
|
||||
"statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.",
|
||||
"statusPageCreateBasicInfoStatusPageAddress": "Your status page address",
|
||||
"statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.",
|
||||
"statusPageCreateSettings": "If your status page is ready, you can mark it as published.",
|
||||
"statusPageCreateSettingsCheckboxLabel": "Published and visible to the public",
|
||||
"statusPageCreateTabsContent": "Status page servers",
|
||||
"statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.",
|
||||
"statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page",
|
||||
"statusPageStatusServiceStatus": "Service status",
|
||||
"submit": "Submit",
|
||||
"SupportedFormats": "Supported formats",
|
||||
|
||||
Reference in new issue
Block a user