Merge pull request #839 from bluewave-labs/feat/fe/ttl

Feat/fe/ttl
This commit is contained in:
Alexander Holliday
2024-09-16 17:16:32 -07:00
committed by GitHub
8 changed files with 162 additions and 43 deletions

View File

@@ -67,7 +67,7 @@ export const update = createAsyncThunk(
fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser(token, user._id, fd);
console.log(res);
return res.data;
} catch (error) {
if (error.response && error.response.data) {

View File

@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, styled, Typography } from "@mui/material";
import { Box, Stack, Typography } from "@mui/material";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
@@ -12,20 +12,92 @@ import {
addDemoMonitors,
deleteAllMonitors,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { update } from "../../Features/Auth/authSlice";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState } from "react";
import { ConfigBox } from "./styled";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
const SECONDS_PER_DAY = 86400;
const Settings = ({ isAdmin }) => {
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
const { timezone } = useSelector((state) => state.ui);
const [checksIsLoading, setChecksIsLoading] = useState(false);
const [form, setForm] = useState({
ttl: (checkTTL / SECONDS_PER_DAY).toString(),
});
const [errors, setErrors] = useState({});
const dispatch = useDispatch();
const handleChange = (event) => {
const { value, id } = event.target;
const { error } = settingsValidation.validate(
{ [id]: value },
{
abortEarly: false,
}
);
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
console.log(newErrors);
logger.error("Validation errors:", error.details);
}
let inputValue = value;
id === "ttl" && (inputValue = value.replace(/[^0-9]/g, ""));
setForm((prev) => ({
...prev,
[id]: inputValue,
}));
};
// TODO Handle saving
const handleSave = async () => {};
const handleSave = async () => {
try {
setChecksIsLoading(true);
await networkService.updateChecksTTL(authToken, form.ttl);
const updatedUser = { ...user, checkTTL: form.ttl };
const action = await dispatch(
update({ authToken, localData: updatedUser })
);
if (action.payload.success) {
createToast({
body: "Settings saved successfully",
});
} else {
if (action.payload) {
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
}
} catch (error) {
console.log(error);
createToast({ body: "Failed to save settings" });
} finally {
setChecksIsLoading(false);
}
};
const handleClearStats = async () => {
try {
@@ -72,33 +144,6 @@ const Settings = ({ isAdmin }) => {
}
};
const ConfigBox = styled("div")({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
});
return (
<Box
className="settings"
@@ -143,21 +188,19 @@ const Settings = ({ isAdmin }) => {
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="history-monitoring"
id="ttl"
label="The days you want to keep monitoring history."
isOptional={true}
optionalLabel="0 for infinite"
placeholder="90"
value=""
onChange={() => logger.warn("Disabled")}
value={form.ttl}
onChange={handleChange}
error={errors.ttl}
/>
<Box>
<Typography>Clear all stats. This is irreversible.</Typography>
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleClearStats}
sx={{ mt: theme.spacing(4) }}
>
@@ -181,7 +224,7 @@ const Settings = ({ isAdmin }) => {
<LoadingButton
variant="contained"
color="primary"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleInsertDemoMonitors}
sx={{ mt: theme.spacing(4) }}
>
@@ -193,7 +236,7 @@ const Settings = ({ isAdmin }) => {
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleDeleteAllMonitors}
sx={{ mt: theme.spacing(4) }}
>
@@ -223,7 +266,8 @@ const Settings = ({ isAdmin }) => {
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<LoadingButton
loading={false}
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}

View File

@@ -0,0 +1,28 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
}));

View File

@@ -582,6 +582,19 @@ class NetworkService {
}
);
}
async updateChecksTTL(authToken, ttl) {
return this.axiosInstance.put(
`/checks/ttl`,
{ ttl },
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
}
export default NetworkService;

View File

@@ -146,9 +146,16 @@ const maintenanceWindowValidation = joi.object({
}),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
}),
});
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
settingsValidation,
};

View File

@@ -10,6 +10,8 @@ const {
updateChecksTTLBodyValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const SERVICE_NAME = "checkController";
const createCheck = async (req, res, next) => {
@@ -144,6 +146,8 @@ const deleteChecksByTeamId = async (req, res, next) => {
};
const updateChecksTTL = async (req, res, next) => {
const SECONDS_PER_DAY = 86400;
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
@@ -157,8 +161,11 @@ const updateChecksTTL = async (req, res, next) => {
}
try {
const ttl = req.body.ttl;
await req.db.updateChecksTTL(1, ttl);
// Get user's teamId
const token = getTokenFromHeaders(req.headers);
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await req.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,

View File

@@ -1,5 +1,6 @@
const Check = require("../../../models/Check");
const Monitor = require("../../../models/Monitor");
const User = require("../../../models/user");
const logger = require("../../../utils/logger");
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
@@ -248,6 +249,14 @@ const deleteChecksByTeamId = async (teamId) => {
const updateChecksTTL = async (teamId, ttl) => {
try {
await Check.collection.dropIndex("expiry_1");
} catch (error) {
logger.error("Failed to drop index", {
service: SERVICE_NAME,
method: "updateChecksTTL",
});
}
try {
await Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
@@ -257,6 +266,14 @@ const updateChecksTTL = async (teamId, ttl) => {
error.method = "updateChecksTTL";
throw error;
}
// Update user
try {
await User.updateMany({ teamId: teamId }, { checkTTL: ttl });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
};
module.exports = {

View File

@@ -45,6 +45,9 @@ const UserSchema = mongoose.Schema(
ref: "Team",
immutable: true,
},
checkTTL: {
type: Number,
},
},
{
timestamps: true,