Merge branch 'develop' into feat/be/create-hardware-monitor
This commit is contained in:
commit
49fe463e95
19 files changed
+776
-562
No files matched your search
@@ -38,6 +38,7 @@ const SearchAdornment = () => {
|
||||
);
|
||||
};
|
||||
|
||||
//TODO keep search state inside of component
|
||||
const Search = ({
|
||||
id,
|
||||
options,
|
||||
|
||||
@@ -12,45 +12,47 @@ import { useNavigate } from "react-router";
|
||||
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { advancedSettingsValidation } from "../../Validation/validation";
|
||||
import { buildErrors, hasValidationErrors } from "../../Validation/error";
|
||||
|
||||
const AdvancedSettings = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload?.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
@@ -72,189 +74,210 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
const { error } = advancedSettingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors))
|
||||
return;
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for sending
|
||||
system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
error={errors.apiBaseUrl}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
error={errors.logLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for
|
||||
sending system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailPort}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailAddress}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailPassword}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
error={errors.jwtTTL}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
error={errors.dbType}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
error={errors.redisHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
error={errors.redisPort}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
error={errors.pagespeedApiKey}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
|
||||
>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react";
|
||||
import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Empty = ({ styles, mode }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box sx={{ ...styles }}>
|
||||
<Box
|
||||
textAlign="center"
|
||||
pb={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
No incidents recorded yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Empty.propTypes = {
|
||||
styles: PropTypes.object,
|
||||
mode: PropTypes.string,
|
||||
};
|
||||
|
||||
export { Empty };
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Skeleton /* , Stack */ } from "@mui/material";
|
||||
const IncidentSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
<Skeleton
|
||||
animation={"wave"}
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
<Skeleton
|
||||
animation={"wave"}
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { IncidentSkeleton };
|
||||
@@ -24,6 +24,8 @@ import { useTheme } from "@emotion/react";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
|
||||
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
import { Empty } from "./Empty/Empty";
|
||||
import { IncidentSkeleton } from "./Skeleton/Skeleton";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
@@ -37,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
@@ -51,6 +54,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
let res;
|
||||
if (selectedMonitor === "0") {
|
||||
res = await networkService.getChecksByTeam({
|
||||
@@ -79,6 +83,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
@@ -129,24 +135,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
p: theme.spacing(30),
|
||||
};
|
||||
|
||||
const hasChecks = checks?.length === 0;
|
||||
const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0";
|
||||
const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0";
|
||||
|
||||
return (
|
||||
<>
|
||||
{checks?.length === 0 && selectedMonitor === "0" ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box
|
||||
textAlign="center"
|
||||
pb={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
No incidents recorded yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : checks?.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<IncidentSkeleton />
|
||||
) : noIncidentsRecordedYet ? (
|
||||
<Empty
|
||||
mode={mode}
|
||||
styles={sharedStyles}
|
||||
/>
|
||||
) : noIncidentsForThatMonitor ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box
|
||||
textAlign="center"
|
||||
|
||||
@@ -17,56 +17,62 @@ const Incidents = () => {
|
||||
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken: authState.authToken,
|
||||
teamId: authState.user.teamId,
|
||||
limit: -1,
|
||||
types: null,
|
||||
status: null,
|
||||
checkOrder: null,
|
||||
normalize: null,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
field: null,
|
||||
order: null,
|
||||
});
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res?.data?.data?.monitors?.length > 0) {
|
||||
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
monitorId !== undefined && setSelectedMonitor(monitorId);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken: authState.authToken,
|
||||
teamId: authState.user.teamId,
|
||||
limit: -1,
|
||||
types: null,
|
||||
status: null,
|
||||
checkOrder: null,
|
||||
normalize: null,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
field: null,
|
||||
order: null,
|
||||
});
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res?.data?.data?.monitors?.length > 0) {
|
||||
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
monitorId !== undefined && setSelectedMonitor(monitorId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchMonitors();
|
||||
}, [authState]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
useEffect(() => {}, [monitors]);
|
||||
|
||||
const handleSelect = (event) => {
|
||||
setSelectedMonitor(event.target.value);
|
||||
};
|
||||
|
||||
const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="incidents table-container"
|
||||
className="incidents"
|
||||
pt={theme.spacing(6)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
{loading ? (
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
MS_PER_WEEK,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
@@ -145,46 +146,36 @@ const CreateMaintenance = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
authToken: authToken,
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
authToken: authToken,
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearch(value);
|
||||
@@ -223,29 +214,17 @@ const CreateMaintenance = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { error } = maintenanceWindowValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
// If errors, return early
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
const handleSubmit = async () => {
|
||||
if(hasValidationErrors(form, maintenanceWindowValidation, setErrors))
|
||||
return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
|
||||
// Get repeat value in milliseconds
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Search from "../../../../Components/Inputs/Search";
|
||||
import MemoizedMonitorTable from "../MonitorTable";
|
||||
import { useState } from "react";
|
||||
import useDebounce from "../../../../Utils/debounce";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const [search, setSearch] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const debouncedFilter = useDebounce(search, 500);
|
||||
const handleSearch = (value) => {
|
||||
setIsSearching(true);
|
||||
setSearch(value);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
flex={1}
|
||||
px={theme.spacing(10)}
|
||||
py={theme.spacing(8)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
letterSpacing={-0.2}
|
||||
>
|
||||
Actively monitoring
|
||||
</Typography>
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{totalMonitors}
|
||||
</Box>
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<MemoizedMonitorTable
|
||||
isAdmin={isAdmin}
|
||||
filter={debouncedFilter}
|
||||
setIsSearching={setIsSearching}
|
||||
isSearching={isSearching}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CurrentMonitoring.propTypes = {
|
||||
totalMonitors: PropTypes.number,
|
||||
monitors: PropTypes.array,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export { CurrentMonitoring };
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Skeleton, TableCell, TableRow } from "@mui/material";
|
||||
const ROWS_NUMBER = 7;
|
||||
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
|
||||
|
||||
const TableBodySkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
{ROWS_ARRAY.map((row) => (
|
||||
<TableRow key={row}>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { TableBodySkeleton };
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
@@ -34,6 +35,7 @@ import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
import ActionsMenu from "../actionsMenu";
|
||||
import useUtils from "../../utils";
|
||||
import { TableBodySkeleton } from "./Skeleton";
|
||||
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
@@ -107,17 +109,17 @@ TablePaginationActions.propTypes = {
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { determineState } = useUtils();
|
||||
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [page, setPage] = useState(0);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [sort, setSort] = useState({});
|
||||
const prevFilter = useRef(filter);
|
||||
@@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
});
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [authState, page, rowsPerPage, filter, sort, setLoading]);
|
||||
}, [authState, page, rowsPerPage, filter, sort, setIsSearching]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPage();
|
||||
}, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]);
|
||||
}, [
|
||||
updateTrigger,
|
||||
authState,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
sort,
|
||||
setIsSearching,
|
||||
fetchPage,
|
||||
]);
|
||||
|
||||
// Listen for changes in filter, if new value reset the page
|
||||
useEffect(() => {
|
||||
@@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
{/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */}
|
||||
{monitors.length > 0 ? (
|
||||
monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/monitors/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
params={params}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/monitors/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
params={params}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBodySkeleton />
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
filter: PropTypes.string,
|
||||
setLoading: PropTypes.func,
|
||||
setIsSearching: PropTypes.func,
|
||||
isSearching: PropTypes.bool,
|
||||
};
|
||||
|
||||
const MemoizedMonitorTable = memo(MonitorTable);
|
||||
|
||||
@@ -1,79 +1,74 @@
|
||||
import "./index.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "./fallback";
|
||||
import StatusBox from "./StatusBox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Greeting from "../../../Utils/greeting";
|
||||
import MonitorTable from "./MonitorTable";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import useDebounce from "../../../Utils/debounce";
|
||||
import { CurrentMonitoring } from "./CurrentMonitoring";
|
||||
|
||||
const Monitors = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = useSelector((state) => state.uptimeMonitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const dispatch = useDispatch({});
|
||||
const debouncedFilter = useDebounce(search, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setIsSearching(true);
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch]);
|
||||
|
||||
let loading =
|
||||
monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0;
|
||||
//TODO bring fetching to this component, like on pageSpeed
|
||||
|
||||
const loading = monitorState?.isLoading;
|
||||
|
||||
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
|
||||
|
||||
const hasMonitors = totalMonitors > 0;
|
||||
const noMonitors = !hasMonitors;
|
||||
const canAddMonitor = isAdmin && hasMonitors;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="monitors table-container"
|
||||
className="monitors"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Box>
|
||||
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Greeting type="uptime" />
|
||||
{canAddMonitor && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{noMonitors && <Fallback isAdmin={isAdmin} />}
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="uptime" />
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
|
||||
<Fallback isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
{hasMonitors && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
@@ -93,88 +88,11 @@ const Monitors = ({ isAdmin }) => {
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
<Box
|
||||
flex={1}
|
||||
px={theme.spacing(10)}
|
||||
py={theme.spacing(8)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
letterSpacing={-0.2}
|
||||
>
|
||||
Actively monitoring
|
||||
</Typography>
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
|
||||
</Box>
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
>
|
||||
<Search
|
||||
options={monitorState?.monitorsSummary?.monitors ?? []}
|
||||
filteredBy="name"
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<MonitorTable
|
||||
isAdmin={isAdmin}
|
||||
filter={debouncedFilter}
|
||||
setLoading={setIsSearching}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<CurrentMonitoring
|
||||
isAdmin={isAdmin}
|
||||
monitors={monitorState.monitorsSummary.monitors}
|
||||
totalMonitors={totalMonitors}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByTeamId(authToken));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const hasValidationErrors = (form, validation, setErrors) => {
|
||||
const { error } = validation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message?? "Validation error";
|
||||
});
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export { buildErrors, hasValidationErrors };
|
||||
@@ -128,10 +128,43 @@ const maintenanceWindowValidation = joi.object({
|
||||
monitors: joi.array().min(1),
|
||||
});
|
||||
|
||||
const advancedSettingsValidation = joi.object({
|
||||
apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({
|
||||
"string.empty": "API base url is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
|
||||
systemEmailHost: joi.string().allow(''),
|
||||
systemEmailPort: joi.string().allow(''),
|
||||
systemEmailAddress: joi
|
||||
.string().allow(''),
|
||||
systemEmailPassword: joi.string().allow(''),
|
||||
jwtTTL: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "JWT TTL is required."
|
||||
}),
|
||||
dbType: joi.string().trim().messages({
|
||||
"string.empty": "DB type is required",
|
||||
}),
|
||||
redisHost: joi.string().trim().messages({
|
||||
"string.empty": "Redis host is required",
|
||||
}),
|
||||
redisPort: joi.string().allow('').custom((value, helpers) => {
|
||||
if(value && isNaN(parseInt(value))){
|
||||
return helpers.message("Redit port must be a number")
|
||||
}
|
||||
return value
|
||||
}),
|
||||
pagespeedApiKey: joi.string().allow('')
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
monitorValidation,
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation,
|
||||
};
|
||||
Vendored
+19
-17
@@ -1,24 +1,26 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/dist/client.Dockerfile"
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile"
|
||||
["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
|
||||
["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile"
|
||||
["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile"
|
||||
)
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/dist/mongoDB.Dockerfile"
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Redis
|
||||
redis="./Docker/dist/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/dist/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t dist_uptime_client .
|
||||
docker build -f $mongoDB -t dist_uptime_database_mongo .
|
||||
docker build -f $redis -t dist_uptime_redis .
|
||||
docker build -f $server -t dist_uptime_server .
|
||||
|
||||
echo "All images built"
|
||||
echo "All images built successfully"
|
||||
+19
-17
@@ -1,24 +1,26 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/prod/client.Dockerfile"
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./Docker/prod/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile"
|
||||
["uptime_redis"]="./Docker/prod/redis.Dockerfile"
|
||||
["uptime_server"]="./Docker/prod/server.Dockerfile"
|
||||
)
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/prod/mongoDB.Dockerfile"
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Redis
|
||||
redis="./Docker/prod/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/prod/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t uptime_client .
|
||||
docker build -f $mongoDB -t uptime_database_mongo .
|
||||
docker build -f $redis -t uptime_redis .
|
||||
docker build -f $server -t uptime_server .
|
||||
|
||||
echo "All images built"
|
||||
echo "All images built successfully"
|
||||
@@ -46,8 +46,8 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
default: () => ({}),
|
||||
},
|
||||
disk: {
|
||||
type: discSchema,
|
||||
default: () => ({}),
|
||||
type: [discSchema],
|
||||
default: () => [],
|
||||
},
|
||||
host: {
|
||||
type: hostSchema,
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "bluewave-uptime",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user