diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 402f5b15e..22252c301 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,7 +16,7 @@ Mention the issue number(s) this PR addresses (e.g., #123). - [ ] I have labelled the PR correctly. - [ ] The issue I am working on is assigned to me. - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). -- [ ] I made sure font sizes, color choices etc are all referenced from the theme. +- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions. - [ ] My PR is granular and targeted to one specific feature. - [ ] I took a screenshot or a video and attached to this PR if there is a UI change. diff --git a/package-lock.json b/package-lock.json index 29e06bd5d..8921c721d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", - "maplibre-gl": "5.1.1", + "maplibre-gl": "5.2.0", "mui-color-input": "^5.0.1", "react": "18.3.1", "react-dnd": "^16.0.1", @@ -14113,9 +14113,9 @@ } }, "node_modules/maplibre-gl": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.1.1.tgz", - "integrity": "sha512-0Z6ODzyFu/grwT6K1eIBpv6MZE4xnJD1AV+Yq1hPzOh/YCY36r9BlSaU7d7n2/HJOaoKOy0b2YF8cS4dD+iEVQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.2.0.tgz", + "integrity": "sha512-9zZKD0M80qtDsqBet+EDuAhoCeA/cnAuZAA0p3hcGKGbyjM/SH+R6wQvnBEgvJz9UhDynnkoKdUwhI+fUkHoXQ==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -15642,9 +15642,9 @@ } }, "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 9684ff111..86d69c015 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", - "maplibre-gl": "5.1.1", + "maplibre-gl": "5.2.0", "mui-color-input": "^5.0.1", "react": "18.3.1", "react-dnd": "^16.0.1", diff --git a/src/Components/Alert/index.jsx b/src/Components/Alert/index.jsx index a52761ca4..0b32fe2b1 100644 --- a/src/Components/Alert/index.jsx +++ b/src/Components/Alert/index.jsx @@ -36,8 +36,8 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { */ const text = theme.palette.secondary.contrastText; - const bg = theme.palette.secondary.main; - const border = theme.palette.secondary.contrastText; + const border = theme.palette.alert.contrastText; + const bg = theme.palette.alert.main; const icon = icons[variant]; return ( diff --git a/src/Components/Charts/CustomGauge/index.jsx b/src/Components/Charts/CustomGauge/index.jsx index d29663004..3c7e13610 100644 --- a/src/Components/Charts/CustomGauge/index.jsx +++ b/src/Components/Charts/CustomGauge/index.jsx @@ -62,6 +62,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = className="radial-chart" width={radius} height={radius} + sx={{ backgroundColor: theme.palette.primary.main, borderRadius: "50%" }} > {`${progressWithinRange.toFixed(1)}%`} diff --git a/src/Components/Sidebar/index.jsx b/src/Components/Sidebar/index.jsx index ee0071d4f..3b78d9f51 100644 --- a/src/Components/Sidebar/index.jsx +++ b/src/Components/Sidebar/index.jsx @@ -63,35 +63,29 @@ const menu = [ { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, - // { name: "Integrations", path: "integrations", icon: }, - { - name: "Account", - icon: , - nested: [ - { name: "Profile", path: "account/profile", icon: }, - { name: "Password", path: "account/password", icon: }, - { name: "Team", path: "account/team", icon: }, - ], - }, + { name: "Integrations", path: "integrations", icon: }, { name: "Settings", icon: , path: "settings", }, +]; + +const otherMenuItems = [ + { name: "Support", path: "support", icon: }, { - name: "Other", - icon: , - nested: [ - { name: "Support", path: "support", icon: }, - { - name: "Discussions", - path: "discussions", - icon: , - }, - { name: "Docs", path: "docs", icon: }, - { name: "Changelog", path: "changelog", icon: }, - ], + name: "Discussions", + path: "discussions", + icon: , }, + { name: "Docs", path: "docs", icon: }, + { name: "Changelog", path: "changelog", icon: }, +]; + +const accountMenuItems = [ + { name: "Profile", path: "account/profile", icon: }, + { name: "Password", path: "account/password", icon: }, + { name: "Team", path: "account/team", icon: }, ]; /* TODO this could be a key in nested Path would be the link */ @@ -133,12 +127,38 @@ function Sidebar() { (state) => state.ui.distributedUptimeEnabled ); - const accountMenuItem = menu.find((item) => item.name === "Account"); - if (user.role?.includes("demo") && accountMenuItem) { - accountMenuItem.nested = accountMenuItem.nested.filter((item) => { - return item.name !== "Password"; - }); - } + const renderAccountMenuItems = () => { + let filteredAccountMenuItems = [...accountMenuItems]; + + // If the user is in demo mode, remove the "Password" option + if (user.role?.includes("demo")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Password" + ); + } + + // If the user is NOT a superadmin, remove the "Team" option + if (user.role && !user.role.includes("superadmin")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Team" + ); + } + + return filteredAccountMenuItems.map((item) => ( + navigate(item.path)} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; const openPopup = (event, id) => { setAnchorEl(event.currentTarget); @@ -260,7 +280,6 @@ function Sidebar() { > {collapsed ? : } - {/* TODO Alignment done using padding. Use single source of truth to that*/} - + + {otherMenuItems.map((item) => { + return item.path ? ( + + { + const url = URL_MAP[item.path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${item.path}`); + } + }} + sx={{ + height: "37px", + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), + }} + > + {item.icon} + {item.name} {" "} + + + ) : null; + })} + + {collapsed && ( {authState.user?.firstName} {authState.user?.lastName} - + {authState.user?.role} @@ -741,7 +825,8 @@ function Sidebar() { )} {/* TODO Do we need two dividers? */} {collapsed && } - + {/* */} + {renderAccountMenuItems()} {heading} - {subHeading} + {subHeading} ); diff --git a/src/Pages/Account/index.jsx b/src/Pages/Account/index.jsx index 00d805f34..48b0984f5 100644 --- a/src/Pages/Account/index.jsx +++ b/src/Pages/Account/index.jsx @@ -57,10 +57,6 @@ const Account = ({ open = "profile" }) => { className="account" px={theme.spacing(20)} py={theme.spacing(12)} - backgroundColor={theme.palette.primary.main} - border={1} - borderColor={theme.palette.primary.lowContrast} - borderRadius={theme.shape.borderRadius} > { const manufacturers = { diff --git a/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx b/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx index ebe0fb94a..fa8593cf1 100644 --- a/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx +++ b/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx @@ -20,7 +20,6 @@ const MonitorTable = ({ isLoading, monitors }) => { const theme = useTheme(); const navigate = useNavigate(); const { determineState } = useMonitorUtils(); - const headers = [ { id: "name", diff --git a/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx b/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx index 7c498d753..ffca50cd7 100644 --- a/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx +++ b/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx @@ -20,11 +20,52 @@ const useSubscribeToMonitors = (page, rowsPerPage) => { const { getMonitorWithPercentage } = useMonitorUtils(); useEffect(() => { + const fetchInitialData = async () => { + try { + const initialDataRes = await networkService.getDistributedUptimeMonitors({ + teamId: user.teamId, + limit: 25, + types: [ + typeof import.meta.env.VITE_DEPIN_TESTING === "undefined" + ? "distributed_http" + : "distributed_test", + ], + page, + rowsPerPage, + }); + const responseData = initialDataRes?.data?.data; + if (typeof responseData === "undefined") throw new Error("No data"); + + const { monitors, filteredMonitors, summary } = responseData; + + const mappedMonitors = filteredMonitors?.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + + setMonitors(monitors); + setMonitorsSummary(summary); + setFilteredMonitors(mappedMonitors); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchInitialData(); + try { const cleanup = networkService.subscribeToDistributedUptimeMonitors({ teamId: user.teamId, limit: 25, - types: ["distributed_http"], + types: [ + typeof import.meta.env.VITE_DEPIN_TESTING === "undefined" + ? "distributed_http" + : "distributed_test", + ], page, rowsPerPage, filter: null, diff --git a/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/src/Pages/Incidents/Components/OptionsHeader/index.jsx index 967acfed5..c3a15963d 100644 --- a/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -20,6 +20,13 @@ const OptionsHeader = ({ const theme = useTheme(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + // The stacks below which are three in number have the same style so + const stackStyles = { + direction: "row", + alignItems: "center", + gap: theme.spacing(6), + }; + if (!shouldRender) return ; return ( @@ -27,17 +34,13 @@ const OptionsHeader = ({ direction="row" justifyContent="space-between" > - + - Incidents for + Incidents for: - + Filter by: - + - + Show: - + ); diff --git a/src/Pages/Settings/index.jsx b/src/Pages/Settings/index.jsx index c33042772..4bbe2fbf9 100644 --- a/src/Pages/Settings/index.jsx +++ b/src/Pages/Settings/index.jsx @@ -219,7 +219,8 @@ const Settings = () => { {t("settingsGeneralSettings")} - {t("settingsDisplayTimezone")}- {t("settingsDisplayTimezoneDescription")} + {t("settingsDisplayTimezone")}-{" "} + {t("settingsDisplayTimezoneDescription")} @@ -242,17 +243,17 @@ const Settings = () => { - diff --git a/src/Utils/NetworkService.js b/src/Utils/NetworkService.js index 73a54136d..1f6a36726 100644 --- a/src/Utils/NetworkService.js +++ b/src/Utils/NetworkService.js @@ -807,6 +807,31 @@ class NetworkService { ); } + getDistributedUptimeMonitors(config) { + const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config; + + const params = new URLSearchParams(); + + if (limit) params.append("limit", limit); + if (types) { + types.forEach((type) => { + params.append("type", type); + }); + } + if (page) params.append("page", page); + if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (order) params.append("order", order); + + if (this.eventSource) { + this.eventSource.close(); + } + + const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}/initial?${params.toString()}`; + return this.axiosInstance.get(url); + } + subscribeToDistributedUptimeMonitors(config) { const { teamId, diff --git a/src/Utils/Theme/constants.js b/src/Utils/Theme/constants.js index cd2b2e316..915acedc5 100644 --- a/src/Utils/Theme/constants.js +++ b/src/Utils/Theme/constants.js @@ -40,7 +40,6 @@ const paletteColors = { blueGray150: "#667085", blueGray200: "#475467", blueGray400: "#344054", - blueGray900: "#1c2130", blueBlueWave: "#1570EF", blue700: "#4E5BA6", purple300: "#664EFF", @@ -89,11 +88,14 @@ const newColors = { gray100: "#F3F3F3", gray200: "#EFEFEF", gray500: "#A2A3A3", + gray900: "#1c1c1c", blueGray50: "#E8F0FE", blueGray500: "#475467", blueGray600: "#344054", blueGray800: "#1C2130", + blueGray900: "#515151", blueBlueWave: "#1570EF", + lightBlueWave: "#CDE2FF", /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */ green100: "#67cd78", green200: "#4B9B77", @@ -171,6 +173,10 @@ const newSemanticColors = { light: newColors.gray200, dark: "#313131" /* newColors.blueGray600 */, }, + light: { + light: newColors.lightBlueWave, + dark: newColors.lightBlueWave, + }, contrastText: { light: newColors.blueGray600, dark: newColors.gray200, @@ -290,6 +296,16 @@ const newSemanticColors = { dark: "#C084FC", }, }, + alert: { + main: { + light: newColors.gray200, + dark: newColors.gray900, + }, + contrastText: { + light: newColors.blueGray600, + dark: newColors.blueGray900, + }, + }, }; export { typographyLevels, semanticColors as colors, newSemanticColors }; diff --git a/src/Utils/Theme/globalTheme.js b/src/Utils/Theme/globalTheme.js index f2136b125..fa0dad9ef 100644 --- a/src/Utils/Theme/globalTheme.js +++ b/src/Utils/Theme/globalTheme.js @@ -382,6 +382,18 @@ const baseTheme = (palette) => ({ }), }, }, + MuiButtonGroup: { + styleOverrides: { + root: ({ theme }) => ({ + ml: "auto", + "& .MuiButtonBase-root, & .MuiButtonBase-root:hover": { + borderColor: theme.palette.primary.lowContrast, + width: "auto", + whiteSpace: "nowrap", + }, + }), + }, + }, // This code is added for clock in maintenance page // code starts from here. diff --git a/src/Utils/stringUtils.js b/src/Utils/stringUtils.js index 0bf7aaf11..bb62d0169 100644 --- a/src/Utils/stringUtils.js +++ b/src/Utils/stringUtils.js @@ -3,8 +3,8 @@ * @param {string} str String whose first letter is to be capitalized * @returns A string with first letter capitalized */ -export const capitalizeFirstLetter = (str) => { - if (str === null || str === undefined) { +export const capitalizeFirstLetter = (str) => { + if (str === null || str === undefined) { return ""; } if (typeof str !== "string") { @@ -12,7 +12,7 @@ export const capitalizeFirstLetter = (str) => { } if (str.length === 0) { return ""; - } + } return str.charAt(0).toUpperCase() + str.slice(1); }; @@ -22,15 +22,25 @@ export const capitalizeFirstLetter = (str) => { * @returns A string with first letter lower cased */ -export const toLowerCaseFirstLetter = (str) => { +export const toLowerCaseFirstLetter = (str) => { if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toLowerCase() + str.slice(1); - }; \ No newline at end of file + return ""; + } + if (typeof str !== "string") { + throw new TypeError("Input must be a string"); + } + if (str.length === 0) { + return ""; + } + return str.charAt(0).toLowerCase() + str.slice(1); +}; + +/** + * Checks if a string is null, undefined, or empty (including strings with only whitespace). + * @param {string} str - The string to check. + * @returns {boolean} - Returns true if the string is null, undefined, or empty. + */ +export const isEmpty = (str) => { + // Check if string is null, undefined, or empty (including whitespace only) + return str === null || typeof str === "undefined" || str.trim().length === 0; +}; diff --git a/src/Validation/validation.js b/src/Validation/validation.js index a243b37e8..ed0aaf6b8 100644 --- a/src/Validation/validation.js +++ b/src/Validation/validation.js @@ -222,7 +222,14 @@ const statusPageValidation = joi.object({ .string() .trim() .messages({ "string.empty": "Company name is required." }), - url: joi.string().trim().messages({ "string.empty": "URL is required." }), + url: joi + .string() + .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen + .required() + .messages({ + "string.pattern.base": + "URL can only contain letters, numbers, underscores, and hyphens", + }), timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }), color: joi.string().trim().messages({ "string.empty": "Color is required." }), theme: joi.string(),