Merge branch 'bluewave-labs:develop' into Fix-for-status-boxes

This commit is contained in:
Owaise Imdad
2025-03-30 03:36:46 +05:30
committed by GitHub
21 changed files with 370 additions and 219 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
minWidth: 400,
bgcolor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
+1
View File
@@ -35,6 +35,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) =>
overflow="hidden"
sx={{
borderStyle: "dashed",
minHeight: "calc(100vh - var(--env-var-spacing-2) * 2)",
}}
>
<Stack
+10 -5
View File
@@ -1,9 +1,8 @@
.home-layout {
position: relative;
min-height: 100vh;
max-width: 1400px;
margin: 0 auto;
padding: var(--env-var-spacing-2);
padding: 0;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
@@ -15,10 +14,9 @@
.home-layout aside {
position: sticky;
top: var(--env-var-spacing-2);
top: 0;
left: 0;
height: calc(100vh - var(--env-var-spacing-2) * 2);
height: 100vh;
max-width: var(--env-var-side-bar-width);
}
@@ -26,3 +24,10 @@
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
flex: 1;
}
.home-content-wrapper {
padding: var(--env-var-spacing-2);
max-width: 1400px;
margin: 0 auto;
flex: 1;
}
+3 -1
View File
@@ -12,7 +12,9 @@ const HomeLayout = () => {
gap={14}
>
<Sidebar />
<Outlet />
<Stack className="home-content-wrapper">
<Outlet />
</Stack>
</Stack>
);
};
@@ -1,5 +1,6 @@
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import {
Dialog,
@@ -49,11 +50,11 @@ const NotificationIntegrationModal = ({
// Helper to get the field state key with error handling
const getFieldKey = (typeId, fieldId) => {
if (typeof typeId !== 'string' || typeId === '') {
throw new Error('Invalid typeId provided to getFieldKey');
throw new Error(t('errorInvalidTypeId'));
}
if (typeof fieldId !== 'string' || fieldId === '') {
throw new Error('Invalid fieldId provided to getFieldKey');
throw new Error(t('errorInvalidFieldId'));
}
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
@@ -182,43 +183,64 @@ const NotificationIntegrationModal = ({
await sendTestNotification(type, config);
};
const handleSave = () => {
//notifications array for selected integrations
const notifications = [...(monitor?.notifications || [])];
// Get all notification types IDs
const existingTypes = activeNotificationTypes.map(type => type.id);
// Filter out notifications that are configurable in this modal
const filteredNotifications = notifications.filter(
notification => !existingTypes.includes(notification.type)
);
// In NotificationIntegrationModal.jsx, update the handleSave function:
// Add each enabled notification with its configured fields
activeNotificationTypes.forEach(type => {
if (integrations[type.id]) {
const notificationObject = {
type: type.id
};
// Add each field value to the notification object
type.fields.forEach(field => {
const fieldKey = getFieldKey(type.id, field.id);
notificationObject[field.id] = integrations[fieldKey];
});
filteredNotifications.push(notificationObject);
const handleSave = () => {
// Get existing notifications
const notifications = [...(monitor?.notifications || [])];
// Get all notification types IDs
const existingTypes = activeNotificationTypes.map(type => type.id);
// Filter out notifications that are configurable in this modal
const filteredNotifications = notifications.filter(
notification => {
if (notification.platform) {
return !existingTypes.includes(notification.platform);
}
});
return !existingTypes.includes(notification.type);
}
);
// Update monitor with new notifications
setMonitor(prev => ({
...prev,
notifications: filteredNotifications
}));
onClose();
};
// Add each enabled notification with its configured fields
activeNotificationTypes.forEach(type => {
if (integrations[type.id]) {
let notificationObject = {
type: "webhook",
platform: type.id, // Set platform to identify the specific service
config: {}
};
// Configure based on notification type
switch(type.id) {
case "slack":
case "discord":
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'webhook')];
break;
case "telegram":
notificationObject.config.botToken = integrations[getFieldKey(type.id, 'token')];
notificationObject.config.chatId = integrations[getFieldKey(type.id, 'chatId')];
break;
case "webhook":
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'url')];
break;
}
filteredNotifications.push(notificationObject);
}
});
// Update monitor with new notifications
setMonitor(prev => ({
...prev,
notifications: filteredNotifications
}));
onClose();
};
return (
<Dialog
@@ -320,4 +342,12 @@ const NotificationIntegrationModal = ({
);
};
NotificationIntegrationModal.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
monitor: PropTypes.object.isRequired,
setMonitor: PropTypes.func.isRequired,
notificationTypes: PropTypes.array
};
export default NotificationIntegrationModal;
+5 -2
View File
@@ -205,9 +205,10 @@ function Sidebar() {
*/
sx={{
position: "relative",
border: 1,
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
borderRadius: 0,
backgroundColor: theme.palette.primary.main,
"& :is(p, span, .MuiListSubheader-root)": {
/*
Text color for unselected menu items and menu headings
@@ -399,6 +400,7 @@ function Sidebar() {
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -653,6 +655,7 @@ function Sidebar() {
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon} </ListItemIcon>
+26 -3
View File
@@ -10,6 +10,7 @@ import { useSelector } from "react-redux";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -20,7 +21,6 @@ import DataTable from "../../Table/";
const TeamPanel = () => {
const theme = useTheme();
const SPACING_GAP = theme.spacing(12);
const [toInvite, setToInvite] = useState({
@@ -34,6 +34,8 @@ const TeamPanel = () => {
const [errors, setErrors] = useState({});
const [isSendingInvite, setIsSendingInvite] = useState(false);
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
const headers = [
{
id: "name",
@@ -124,6 +126,10 @@ const TeamPanel = () => {
});
};
const handleGetToken = async () => {
await getInviteToken({ email: toInvite.email, role: toInvite.role });
};
const handleInviteMember = async () => {
if (!toInvite.email) {
setErrors((prev) => ({ ...prev, email: "Email is required." }));
@@ -146,7 +152,7 @@ const TeamPanel = () => {
}
try {
await networkService.requestInvitationToken({
await networkService.sendInvitationToken({
email: toInvite.email,
role: toInvite.role,
});
@@ -165,6 +171,7 @@ const TeamPanel = () => {
const closeInviteModal = () => {
setIsOpen(false);
clearToken();
setToInvite({ email: "", role: ["0"] });
setErrors({});
};
@@ -275,6 +282,13 @@ const TeamPanel = () => {
{ _id: "user", name: "User" },
]}
/>
{token && <Typography>Invite link</Typography>}
{token && (
<TextInput
id="invite-token"
value={token}
/>
)}
<Stack
direction="row"
gap={theme.spacing(4)}
@@ -289,6 +303,15 @@ const TeamPanel = () => {
>
Cancel
</Button>
<Button
variant="contained"
color="accent"
onClick={handleGetToken}
loading={isSendingInvite}
disabled={isDisabled}
>
Get token
</Button>
<Button
variant="contained"
color="accent"
@@ -296,7 +319,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
Send invite
E-mail token
</Button>
</Stack>
</GenericDialog>
+46
View File
@@ -0,0 +1,46 @@
import { useState } from "react";
import { networkService } from "../main";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
const useGetInviteToken = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const [token, setToken] = useState(undefined);
const clearToken = () => {
setToken(undefined);
};
const getInviteToken = async ({ email, role }) => {
try {
const response = await networkService.requestInvitationToken({
email,
role,
});
const token = response?.data?.data?.token;
if (typeof token === "undefined") {
throw new Error(t("inviteNoTokenFound"));
}
let inviteLink = token;
if (typeof CLIENT_HOST !== "undefined") {
inviteLink = `${CLIENT_HOST}/register/${token}`;
}
setToken(inviteLink);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return [getInviteToken, clearToken, isLoading, error, token];
};
export { useGetInviteToken };
+1
View File
@@ -27,6 +27,7 @@ const useSubscribeToDepinDetails = ({ monitorId, isPublic, isPublished, dateRang
monitorId,
dateRange: dateRange,
normalize: true,
isPublic,
});
const responseData = res?.data?.data;
@@ -32,7 +32,7 @@ const PerformanceReport = ({ shouldRender, audits }) => {
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
color: theme.palette.primary.contrastTextTertiary,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
+4 -4
View File
@@ -69,9 +69,9 @@ const Configure = () => {
];
const expectedValuePlaceholders = {
regex: "^[\w.-]+@gmail.com$",
equal: "janet@gmail.com",
include: "@gmail.com",
regex: "^(success|ok)$",
equal: "success",
include: "ok",
};
useEffect(() => {
@@ -490,7 +490,7 @@ const Configure = () => {
id="json-path"
label="JSON Path"
isOptional={true}
placeholder="data.email"
placeholder="data.status"
value={monitor.jsonPath}
onChange={(event) => handleChange(event, "jsonPath")}
error={errors["jsonPath"] ? true : false}
+6 -6
View File
@@ -41,9 +41,9 @@ const CreateMonitor = () => {
];
const expectedValuePlaceholders = {
regex: "^[\w.-]+@gmail.com$",
equal: "janet@gmail.com",
include: "@gmail.com",
regex: "^(success|ok)$",
equal: "success",
include: "ok",
};
const monitorTypeMaps = {
@@ -415,7 +415,7 @@ const CreateMonitor = () => {
onChange={(event) => handleNotifications(event, "email")}
/>
<Box mt={theme.spacing(2)}>
{/* <Box mt={theme.spacing(2)}>
<Button
variant="contained"
color="accent"
@@ -423,7 +423,7 @@ const CreateMonitor = () => {
>
{t("notifications.integrationButton")}
</Button>
</Box>
</Box> */}
</Stack>
</ConfigBox>
<ConfigBox>
@@ -477,7 +477,7 @@ const CreateMonitor = () => {
id="json-path"
label="JSON Path"
isOptional={true}
placeholder="data.email"
placeholder="data.status"
value={monitor.jsonPath}
onChange={(event) => handleChange(event, "jsonPath")}
error={errors["jsonPath"] ? true : false}
@@ -149,7 +149,11 @@ const UptimeDataTable = ({
{
id: "responseTime",
content: t("responseTime"),
render: (row) => <BarChart checks={row.monitor.checks.slice().reverse()} />,
render: (row) => (
<Box display="flex" justifyContent="center">
<BarChart checks={row.monitor.checks.slice().reverse()} />
</Box>
),
},
{
id: "type",
+29 -3
View File
@@ -45,7 +45,7 @@ class NetworkService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (!error.request && error.response && error.response.status === 401) {
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
navigate("/login");
@@ -263,6 +263,9 @@ class NetworkService {
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
jsonPath: monitor.jsonPath,
};
return this.axiosInstance.put(`/monitors/${monitorId}`, payload, {
headers: {
@@ -530,6 +533,24 @@ class NetworkService {
async requestInvitationToken(config) {
return this.axiosInstance.post(`/invite`, { email: config.email, role: config.role });
}
/**
* ************************************
* Sends an invitation token
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.email - The email of the user to be invited.
* @param {string} config.role - The role of the user to be invited.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async sendInvitationToken(config) {
return this.axiosInstance.post(`/invite/send`, {
email: config.email,
role: config.role,
});
}
/**
* ************************************
@@ -926,10 +947,15 @@ class NetworkService {
getDistributedUptimeDetails(config) {
const params = new URLSearchParams();
const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config;
const { monitorId, dateRange, normalize, isPublic } = config;
if (dateRange) params.append("dateRange", dateRange);
if (normalize) params.append("normalize", normalize);
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`;
let url;
if (isPublic) {
url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/public/${monitorId}/initial?${params.toString()}`;
} else {
url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`;
}
return this.axiosInstance.get(url);
}
+10 -1
View File
@@ -359,7 +359,9 @@ const baseTheme = (palette) => ({
root: ({ theme }) => ({
fontSize: theme.typography.fontSize - 1,
color: theme.palette.tertiary.contrastText,
backgroundColor: theme.palette.tertiary.main,
backgroundColor: theme.palette.primary.main,
height: '34px',
borderRadius: 0,
textTransform: "none",
minWidth: "fit-content",
padding: `${theme.spacing(6)}px ${theme.spacing(4)}px`,
@@ -378,6 +380,7 @@ const baseTheme = (palette) => ({
color: theme.palette.secondary.contrastText,
borderColor: theme.palette.secondary.contrastText,
borderRightColor: theme.palette.primary.lowContrast,
borderRadius: 0,
},
"&:hover": {
borderColor: theme.palette.primary.lowContrast,
@@ -428,6 +431,12 @@ const baseTheme = (palette) => ({
MuiTabs: {
styleOverrides: {
root: ({ theme }) => ({
display: 'inline-flex',
borderTop: '1px solid',
borderLeft: '1px solid',
borderRight: '1px solid',
borderColor: theme.palette.primary.lowContrast,
borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0 0`,
"& .MuiTabs-indicator": {
backgroundColor: theme.palette.tertiary.contrastText,
},
+4 -1
View File
@@ -372,5 +372,8 @@
"infrastructureEditYour": "Edit your",
"infrastructureEditMonitor": "Save Infrastructure Monitor",
"infrastructureMonitorCreated": "Infrastructure monitor created successfully!",
"infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!"
"infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!",
"errorInvalidTypeId": "Invalid notification type provided",
"errorInvalidFieldId": "Invalid field ID provided",
"inviteNoTokenFound": "No invite token found"
}
+4 -1
View File
@@ -372,5 +372,8 @@
"infrastructureEditYour": "",
"infrastructureEditMonitor": "",
"infrastructureMonitorCreated": "",
"infrastructureMonitorUpdated": ""
"infrastructureMonitorUpdated": "",
"errorInvalidTypeId": "",
"errorInvalidFieldId": "",
"inviteNoTokenFound": ""
}