Compare commits

..
Author SHA1 Message Date
gorkem-bwl 79aca01bbf Fix formatting issues 2026-01-15 22:31:05 -05:00
gorkem-bwl a87ca648dc Restore refresh token functionality for seamless session renewal
## Summary
Restores refresh token functionality that was lost during a previous refactor.
This enables seamless session renewal without forcing users to re-login when
their access token expires.

## Server changes
- Add refresh token configuration to settingsService (7d TTL by default)
- Add issueRefreshToken method to userService
- Update login/register/resetPassword to return refreshToken
- Add refreshAuthToken method to userService for token refresh
- Add refreshAuthToken controller method
- Add POST /auth/refresh route
- Update verifyJWT middleware to return 403 for expired tokens (triggers refresh)
  and 401 for invalid tokens (triggers logout)

## Client changes
- Store refreshToken in Redux auth state
- Clear refreshToken on logout
- Add setAuthToken action for updating token after refresh
- Update NetworkService to handle 403 responses:
  - Call /auth/refresh endpoint to get new token
  - Retry failed request with new token
  - Queue concurrent requests during refresh
  - Redirect to login if refresh fails
2026-01-15 22:28:07 -05:00
210 changed files with 1887 additions and 3608 deletions

No files matched your search

-145
View File
@@ -1,145 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
## Development Commands
### Client (React/Vite)
```bash
cd client
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # TypeScript check + production build
npm run lint # ESLint (strict, max-warnings 0)
npm run format # Prettier formatting
npm run format-check # Check formatting
```
### Server (Node.js/Express)
```bash
cd server
npm install
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
npm run build # TypeScript compile + path alias resolution
npm run test # Run Mocha tests with c8 coverage
npm run lint # ESLint v9
npm run lint-fix # Auto-fix lint issues
npm run format # Prettier formatting
```
### Docker Development
```bash
cd docker/dev
./build_images.sh
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
```
## Environment Setup
### Server `.env` (minimum required)
```env
CLIENT_HOST="http://localhost:5173"
JWT_SECRET="my_secret_key_change_this"
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
TOKEN_TTL="99d"
ORIGIN="localhost"
LOG_LEVEL="debug"
```
### Client `.env`
```env
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
VITE_APP_LOG_LEVEL="debug"
```
## Architecture
### Monorepo Structure
- `/client` - React 18 + TypeScript + Vite + MUI frontend
- `/server` - Node.js 20+ + Express + TypeScript backend
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
### Backend Layers
```
server/src/
├── controllers/ # Route handlers (authController, monitorController, etc.)
├── service/ # Business logic
│ ├── business/ # Core monitoring logic
│ ├── infrastructure/ # Server/system utilities
│ └── system/ # App-level settings
├── db/
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
│ ├── migration/ # Database migrations (run on startup)
│ └── modules/ # Database-specific modules
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
├── routes/v1/ # API route definitions
├── validation/ # Joi input validation schemas
└── repositories/ # Data access layer
```
### Frontend Structure
```
client/src/
├── Components/ # Reusable UI components
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
├── Features/ # Redux slices (Auth, UI)
├── Hooks/ # Custom React hooks
├── Utils/ # Utilities (NetworkService.js is main API client)
├── Validation/ # Input validation
└── locales/ # i18n translations
```
### API
- Base URL: `/api/v1`
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
- OpenAPI spec: `/server/openapi.json`
### Key Technologies
- **State Management**: Redux Toolkit + Redux-Persist
- **Data Fetching**: SWR + Axios
- **Database**: MongoDB with Mongoose ODM
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
- **i18n**: i18next + react-i18next (translations via PoEditor)
## Code Conventions
### Internationalization
All user-facing strings must use the translation function:
```javascript
t('your.key') // Never hardcode UI strings
```
### Branching
- Always branch from `develop` (not master)
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
- PRs target `develop` branch
### Formatting
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
- Both use ESLint with strict settings
### Testing
Server tests use Mocha + Chai + Sinon:
```bash
npm test # Run all tests with coverage
npm test -- --grep "pattern" # Run specific tests
```
Test files: `server/tests/**/*.test.js`
## Database Models
Key Mongoose models in `/server/src/db/models/`:
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
- **Check** - Individual monitoring check results
- **Incident** - Downtime incidents
- **User** - User accounts
- **Team** - Team/workspace management
- **StatusPage** - Public status pages
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
- **MaintenanceWindow** - Scheduled maintenance periods
- **AppSettings** - Global application settings
+42 -25
View File
@@ -12,6 +12,7 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -24,7 +25,6 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
@@ -1189,6 +1189,32 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz",
"integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.4.11",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/lab": {
"version": "6.0.0-dev.240424162023-9968b4889d",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
@@ -2024,9 +2050,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -5271,9 +5297,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -5438,15 +5464,6 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6112,12 +6129,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.2"
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -6127,13 +6144,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
+1 -1
View File
@@ -17,6 +17,7 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -29,7 +30,6 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
@@ -2,7 +2,7 @@
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Icon from "../Icon";
import Settings from "../../../assets/icons/settings-bold.svg?react";
import Dialog from "../Dialog/index.jsx";
// Utils
@@ -84,10 +84,7 @@ const ActionsMenu = ({
},
}}
>
<Icon
name="Settings"
size={20}
/>
<Settings />
</IconButton>
<Menu
+11 -22
View File
@@ -1,7 +1,10 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
import Icon from "../Icon";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
import "./index.css";
/**
@@ -10,24 +13,9 @@ import "./index.css";
*/
const icons = {
info: (
<Icon
name="Info"
size={24}
/>
),
error: (
<Icon
name="AlertCircle"
size={24}
/>
),
warning: (
<Icon
name="AlertTriangle"
size={24}
/>
),
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
};
/**
@@ -109,9 +97,10 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
},
}}
>
<Icon
name="X"
size={20}
<CloseIcon
sx={{
fontSize: "20px",
}}
/>
</IconButton>
)}
+9 -10
View File
@@ -1,28 +1,27 @@
import Icon from "../Icon";
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
import PropTypes from "prop-types";
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<Icon
name="ChevronsLeft"
color={color}
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<Icon
name="ArrowLeft"
color={color}
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<Icon
name="ChevronLeft"
color={color}
<LeftArrow
style={{ color }}
{...props}
/>
);
@@ -1,20 +1,19 @@
import Icon from "../Icon";
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
import PropTypes from "prop-types";
const ArrowRight = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<Icon
name="ChevronsRight"
color={color}
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<Icon
name="ChevronRight"
color={color}
<RightArrow
style={{ color }}
{...props}
/>
);
@@ -154,10 +154,7 @@ export const InfrastructureTooltip = ({
yKey,
yIdx = -1,
yLabel,
yKey2,
yLabel2,
dotColor,
dotColor2,
dateRange,
formatter = getFormattedPercentage,
}) => {
@@ -219,37 +216,7 @@ export const InfrastructureTooltip = ({
</Typography>
</Stack>
</Box>
{yKey2 && yLabel2 && (
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor2 || dotColor}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{`${yLabel2} ${formatter(payload[0].payload[yKey2])}`}
</Typography>
</Stack>
</Box>
)}
{/* Display original value */}
</Box>
);
}
@@ -267,10 +234,7 @@ InfrastructureTooltip.propTypes = {
yKey: PropTypes.string,
yIdx: PropTypes.number,
yLabel: PropTypes.string,
yKey2: PropTypes.string,
yLabel2: PropTypes.string,
dotColor: PropTypes.string,
dotColor2: PropTypes.string,
dateRange: PropTypes.string,
};
+11 -10
View File
@@ -1,6 +1,7 @@
import "./check.css";
import PropTypes from "prop-types";
import Icon from "../Icon";
import CheckGrey from "../../../assets/icons/check.svg?react";
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -34,16 +35,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
alignItems="center"
>
{outlined ? (
<Icon
name="CheckCircle"
size={20}
/>
<CheckOutlined alt="check" />
) : (
<Icon
name="Check"
size={16}
color={colors[variant]}
/>
<Box
lineHeight={0}
sx={{
"& svg > path": { fill: colors[variant] },
}}
>
<CheckGrey alt="form checks" />
</Box>
)}
<Typography
component="span"
+4 -11
View File
@@ -8,7 +8,8 @@ import IconButton from "@mui/material/IconButton";
import Container from "@mui/material/Container";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import Icon from "../Icon";
import MenuIcon from "@mui/icons-material/Menu";
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import ThemeSwitch from "../ThemeSwitch/index.jsx";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
@@ -152,11 +153,7 @@ const AppAppBar = () => {
aria-label="Menu button"
onClick={toggleDrawer(true)}
>
<Icon
name="Menu"
size={24}
color={theme.palette.text.primary}
/>
<MenuIcon sx={{ color: theme.palette.text.primary }} />
</IconButton>
<Drawer
anchor="top"
@@ -179,11 +176,7 @@ const AppAppBar = () => {
}}
>
<IconButton onClick={toggleDrawer(false)}>
<Icon
name="X"
size={24}
color={theme.palette.text.primary}
/>
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
</IconButton>
</Box>
@@ -1,7 +1,7 @@
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import Icon from "../Icon";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
/**
* A reusable filter header component that displays a dropdown menu with selectable options.
@@ -29,10 +29,9 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
<Select
multiple={multiple}
IconComponent={(props) => (
<Icon
<AddCircleOutlineIcon
{...props}
name="PlusCircle"
size={18}
sx={{ fontSize: "medium" }}
/>
)}
displayEmpty
+5 -5
View File
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
direction="row"
position="relative"
alignItems="center"
gap={theme.spacing(4)}
gap={theme.spacing(5)}
>
<Typography
component="span"
variant="h6"
sx={{
fontWeight: 500,
fontWeight: 600,
}}
>
{title}
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
<>
<Dot />
<Typography
component="span"
variant="h6"
sx={{
fontWeight: 500,
fontWeight: 600,
color: percentageColor,
}}
>
-120
View File
@@ -1,120 +0,0 @@
/**
* Maps legacy icon names to Lucide icon names
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
*/
export const iconMap = {
// Navigation/Sidebar icons
monitors: "Globe",
incidents: "AlertTriangle",
integrations: "Link",
"page-speed": "Gauge",
settings: "Settings",
notifications: "Bell",
maintenance: "Wrench",
"status-pages": "Wifi",
docs: "FileText",
discussions: "MessageCircle",
support: "HelpCircle",
changeLog: "Code",
logs: "Database",
// User icons
user: "User",
"user-two": "Users",
"user-edit": "UserPen",
groups: "Users",
lock: "Lock",
key: "Key",
logout: "LogOut",
// Action icons
edit: "Pencil",
"trash-bin": "Trash2",
search: "Search",
"close-icon": "X",
check: "Check",
"check-icon": "CheckCircle",
"check-outlined": "CheckCircle",
// Arrow icons
"left-arrow": "ChevronLeft",
"right-arrow": "ChevronRight",
"up-arrow": "ChevronUp",
"down-arrow": "ChevronDown",
"left-arrow-double": "ChevronsLeft",
"right-arrow-double": "ChevronsRight",
"left-arrow-long": "ArrowLeft",
"top-right-arrow": "ArrowUpRight",
"open-in-new-page": "ExternalLink",
"selector-vertical": "ChevronsUpDown",
// Status/Alert icons
"alert-icon": "Bell",
"warning-icon": "AlertCircle",
"history-icon": "History",
// Data/Chart icons
dashboard: "LayoutGrid",
"monitor-graph-line": "TrendingUp",
"response-time-icon": "TrendingUp",
"uptime-icon": "Activity",
"speedometer-icon": "Gauge",
"spedometer-icon": "Gauge",
"ruler-icon": "Ruler",
"performance-report": "Layers",
"average-response-icon": "BarChart3",
"distributed-uptime": "Network",
// Misc icons
calendar: "Calendar",
"calendar-check": "CalendarCheck",
"clock-snooze": "Clock",
mail: "Mail",
email: "Mail",
folder: "Folder",
"dots-vertical": "MoreVertical",
"pause-icon": "PauseCircle",
"resume-icon": "PlayCircle",
"checkbox-filled": "CheckSquare",
"checkbox-outline": "Square",
"radio-checked": "Circle",
"cpu-chip": "Cpu",
certificate: "Award",
"interval-check": "RefreshCw",
};
/**
* MUI icon to Lucide mapping
*/
export const muiToLucideMap = {
AddCircleOutline: "PlusCircle",
ArrowDownwardRounded: "ArrowDown",
ArrowUpwardRounded: "ArrowUp",
ArrowDropDown: "ChevronDown",
ArrowOutward: "ExternalLink",
CheckCircle: "CheckCircle2",
Clear: "X",
Close: "X",
CloseRounded: "X",
CloudUpload: "Upload",
Delete: "Trash2",
DeleteOutlineRounded: "Trash2",
Email: "Mail",
ErrorOutline: "AlertTriangle",
ErrorOutlineOutlined: "AlertTriangle",
Image: "ImageIcon",
InfoOutlined: "Info",
KeyboardArrowDown: "ChevronDown",
Menu: "Menu",
PauseCircleOutline: "PauseCircle",
PauseOutlined: "Pause",
PlayArrowOutlined: "Play",
PlayCircleOutlineRounded: "PlayCircle",
RadioButtonChecked: "Circle",
ReorderRounded: "GripVertical",
SettingsOutlined: "Settings",
Share: "Share2",
Visibility: "Eye",
VisibilityOff: "EyeOff",
WarningAmberOutlined: "AlertTriangle",
};
-196
View File
@@ -1,196 +0,0 @@
import { useTheme } from "@mui/material";
import PropTypes from "prop-types";
import {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image as ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
} from "lucide-react";
/**
* Map of icon names to Lucide icon components.
* Only icons explicitly imported here will be included in the bundle (tree-shaking).
* To add a new icon: import it above and add it to this object.
*/
const iconComponents = {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image: ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
};
/**
* Theme-aware icon component wrapping Lucide icons
*
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
* @param {number} size - Icon size in pixels (default: 20)
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
* @param {number} strokeWidth - Stroke width (default: 1.5)
* @param {string} fill - Fill color (default: "none")
*/
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
const theme = useTheme();
// Resolve color from theme path or use direct value
const resolveColor = (colorValue) => {
if (!colorValue) {
return theme.palette.primary.contrastTextTertiary; // Default icon color
}
// If it's a theme path like "primary.contrastText"
if (typeof colorValue === "string" && colorValue.includes(".")) {
const parts = colorValue.split(".");
let resolved = theme.palette;
for (const part of parts) {
resolved = resolved?.[part];
}
return resolved || colorValue;
}
// Direct color value
return colorValue;
};
const LucideIcon = iconComponents[name];
if (!LucideIcon) {
console.warn(`Icon "${name}" not found in Icon component`);
return null;
}
return (
<LucideIcon
size={size}
color={resolveColor(color)}
strokeWidth={strokeWidth}
fill={fill}
{...props}
/>
);
};
Icon.propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.number,
color: PropTypes.string,
strokeWidth: PropTypes.number,
fill: PropTypes.string,
};
export default Icon;
@@ -1,7 +1,8 @@
import PropTypes from "prop-types";
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
/**
* Checkbox Component
@@ -64,8 +65,8 @@ const Checkbox = ({
name={name}
value={value}
onChange={onChange}
icon={<Icon name="Square" />}
checkedIcon={<Icon name="CheckSquare" />}
icon={<CheckboxOutline />}
checkedIcon={<CheckboxFilled />}
inputProps={{
"aria-label": "controlled checkbox",
id: id,
@@ -1,10 +1,11 @@
// Components
import { Box, Stack, Typography } from "@mui/material";
import Icon from "../../Icon";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import Image from "../../Image/index.jsx";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import ProgressUpload from "../../ProgressBars/index.jsx";
import ImageIcon from "@mui/icons-material/Image";
// Utils
import PropTypes from "prop-types";
@@ -175,10 +176,7 @@ const ImageUpload = ({
boxShadow: theme.shape.boxShadow,
}}
>
<Icon
name="Upload"
size={24}
/>
<CloudUploadIcon />
</IconButton>
<Typography
component="h2"
@@ -205,12 +203,7 @@ const ImageUpload = ({
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={
<Icon
name="Image"
size={20}
/>
}
icon={<ImageIcon />}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
import RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
import "./index.css";
/**
@@ -47,12 +47,7 @@ const Radio = ({
<MUIRadio
id={id}
size={size}
checkedIcon={
<Icon
name="CircleDot"
size={16}
/>
}
checkedIcon={<RadioChecked />}
sx={{
color: "transparent",
width: 16,
@@ -8,7 +8,7 @@ import {
Checkbox,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
import SearchIcon from "../../../../assets/icons/search.svg?react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import FieldWrapper from "../FieldWrapper/index.jsx";
@@ -31,13 +31,20 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box height={16}>
<Icon
name="Search"
size={16}
color="primary.contrastTextTertiary"
strokeWidth={1.2}
/>
<Box
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
};
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
import Icon from "../../Icon";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import FieldWrapper from "../FieldWrapper/index.jsx";
import "./index.css";
@@ -98,13 +98,7 @@ const Select = ({
error={error}
name={name}
inputProps={{ id: id }}
IconComponent={(props) => (
<Icon
name="ChevronDown"
size={20}
{...props}
/>
)}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{ disableScrollLock: true }}
sx={{
fontSize: 13,
@@ -1,7 +1,10 @@
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import Icon from "../../../Icon";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react";
export const HttpAdornment = ({ https }) => {
const theme = useTheme();
@@ -53,17 +56,7 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
},
}}
>
{fieldType === "password" ? (
<Icon
name="EyeOff"
size={20}
/>
) : (
<Icon
name="Eye"
size={20}
/>
)}
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
);
@@ -3,8 +3,6 @@
min-height: 100vh;
margin: 0 auto;
padding: 0;
overflow-x: hidden;
width: 100%;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
@@ -32,6 +30,4 @@
max-width: 1400px;
margin: 0 auto;
flex: 1;
min-width: 0;
overflow-x: hidden;
}
@@ -1,7 +1,7 @@
import * as React from "react";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import Icon from "../Icon";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Grow from "@mui/material/Grow";
import Paper from "@mui/material/Paper";
@@ -70,10 +70,7 @@ const MonitorActions = ({ isLoading }) => {
aria-haspopup="menu"
onClick={handleToggle}
>
<Icon
name="ChevronDown"
size={20}
/>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
@@ -3,7 +3,10 @@ import Status from "./status.jsx";
import Skeleton from "./skeleton.jsx";
import Button from "@mui/material/Button";
import { Tooltip } from "@mui/material";
import Icon from "../Icon";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
import EmailIcon from "@mui/icons-material/Email";
// Utils
import PropTypes from "prop-types";
@@ -72,12 +75,7 @@ const MonitorDetailsControlHeader = ({
variant="contained"
color="secondary"
loading={isSending}
startIcon={
<Icon
name="Mail"
size={18}
/>
}
startIcon={<EmailIcon />}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?.id });
@@ -105,17 +103,7 @@ const MonitorDetailsControlHeader = ({
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? (
<Icon
name="Pause"
size={18}
/>
) : (
<Icon
name="Play"
size={18}
/>
)
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={() => {
pauseMonitor({
@@ -131,12 +119,7 @@ const MonitorDetailsControlHeader = ({
<Button
variant="contained"
color="secondary"
startIcon={
<Icon
name="Settings"
size={18}
/>
}
startIcon={<SettingsOutlinedIcon />}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
Configure
@@ -2,7 +2,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import Icon from "../Icon";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import Search from "../Inputs/Search/index.jsx";
// Utils
@@ -102,13 +102,11 @@ const NotificationConfig = ({
>
{notification.notificationName}
</Typography>
<Icon
name="Trash2"
size={20}
<DeleteOutlineRoundedIcon
onClick={() => {
handleDelete(notification._id);
}}
style={{ cursor: "pointer" }}
sx={{ cursor: "pointer" }}
/>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
@@ -1,7 +1,8 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
import Icon from "../Icon";
import CloseIcon from "@mui/icons-material/Close";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import "./index.css";
/**
@@ -50,10 +51,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
alignItems={error ? "center" : "flex-start"}
>
{error ? (
<Icon
name="AlertTriangle"
size={20}
/>
<ErrorOutlineOutlinedIcon />
) : icon ? (
<Box
sx={{
@@ -129,9 +127,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
}
}
>
<Icon
name="X"
size={20}
<CloseIcon
sx={{
fontSize: "20px",
}}
/>
</IconButton>
</Stack>
@@ -1,22 +1,15 @@
import React from "react";
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
import Icon from "../Icon";
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
import CheckCircle from "@mui/icons-material/CheckCircle";
import PropTypes from "prop-types";
const CustomStepIcon = (props) => {
const { completed, active } = props;
return completed ? (
<Icon
name="CheckCircle2"
size={24}
color="accent.main"
/>
<CheckCircle color="accent" />
) : (
<Icon
name="Circle"
size={24}
color={active ? "accent.main" : "text.disabled"}
/>
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
);
};
@@ -1,5 +1,5 @@
import html2canvas from "html2canvas";
import Icon from "../Icon";
import ShareIcon from "@mui/icons-material/Share";
import { Button } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -55,13 +55,7 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
return (
<Button
variant="outlined"
startIcon={
<Icon
name="Share2"
size={18}
color="success.main"
/>
}
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
color="success"
onClick={captureAndShare}
>
@@ -7,7 +7,8 @@ import Avatar from "../../Avatar/index.jsx";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider";
import Icon from "../../Icon";
import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
@@ -80,10 +81,6 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
{item.icon}
@@ -163,14 +160,19 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
ml: "50px",
"&:focus": { outline: "none" },
alignSelf: "center",
"& svg": {
width: "22px",
height: "22px",
},
"& svg path": {
/* Vertical three dots */
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={(event) => openPopup(event)}
>
<Icon
name="MoreVertical"
size={22}
color="primary.contrastTextTertiary"
/>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
@@ -254,13 +256,12 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Icon
name="LogOut"
size={20}
color="primary.contrastTextTertiary"
/>
<LogoutSvg />
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
@@ -52,11 +52,12 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
<ListItemIcon
sx={{
minWidth: 0,
color: iconStroke,
"& svg": {
height: 20,
width: 20,
opacity: 0.81,
},
"& svg path": {
stroke: iconStroke,
},
}}
+36 -18
View File
@@ -8,7 +8,22 @@ import NavItem from "./components/navItem.jsx";
import AuthFooter from "./components/authFooter.jsx";
import StarPrompt from "../StarPrompt/index.jsx";
import Icon from "../Icon";
import LockSvg from "../../../assets/icons/lock.svg?react";
import UserSvg from "../../../assets/icons/user.svg?react";
import TeamSvg from "../../../assets/icons/user-two.svg?react";
import Support from "../../../assets/icons/support.svg?react";
import Maintenance from "../../../assets/icons/maintenance.svg?react";
import Monitors from "../../../assets/icons/monitors.svg?react";
import Incidents from "../../../assets/icons/incidents.svg?react";
import Integrations from "../../../assets/icons/integrations.svg?react";
import PageSpeed from "../../../assets/icons/page-speed.svg?react";
import Settings from "../../../assets/icons/settings.svg?react";
import ChangeLog from "../../../assets/icons/changeLog.svg?react";
import Docs from "../../../assets/icons/docs.svg?react";
import StatusPages from "../../../assets/icons/status-pages.svg?react";
import Discussions from "../../../assets/icons/discussions.svg?react";
import Notifications from "../../../assets/icons/notifications.svg?react";
import Logs from "../../../assets/icons/logs.svg?react";
// Utils
import { useTheme } from "@mui/material/styles";
@@ -24,41 +39,44 @@ const URL_MAP = {
};
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: t("menu.notifications"),
path: "notifications",
icon: <Icon name="Bell" />,
icon: <Notifications />,
},
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
{ name: t("menu.checks"), path: "checks", icon: <Docs /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
icon: <Icon name="Settings" />,
icon: <Settings />,
path: "settings",
},
];
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
{ name: t("menu.support"), path: "support", icon: <Support /> },
{
name: t("menu.discussions"),
path: "discussions",
icon: <Icon name="MessageCircle" />,
icon: <Discussions />,
},
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
];
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
];
const Sidebar = () => {
@@ -1,6 +1,6 @@
import React from "react";
import { Typography, IconButton, Stack, Box } from "@mui/material";
import Icon from "../Icon";
import CloseIcon from "@mui/icons-material/Close";
import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
@@ -67,10 +67,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
},
}}
>
<Icon
name="X"
size={20}
/>
<CloseIcon sx={{ fontSize: "1.25rem" }} />
</IconButton>
</Stack>
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions/index.jsx";
import Icon from "../../Icon";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
Pagination.propTypes = {
paginationLabel: PropTypes.string, // Label for the pagination.
@@ -100,12 +100,7 @@ function Pagination({
},
},
inputProps: { id: "pagination-dropdown" },
IconComponent: () => (
<Icon
name="ChevronsUpDown"
size={16}
/>
),
IconComponent: SelectorVertical,
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
+9 -27
View File
@@ -3,31 +3,19 @@ import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ToastBody from "./body.jsx";
import Icon from "../Icon";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const icons = {
info: (
<Icon
name="Info"
size={24}
/>
),
error: (
<Icon
name="AlertCircle"
size={24}
/>
),
warning: (
<Icon
name="AlertTriangle"
size={24}
/>
),
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
};
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
@@ -62,10 +50,7 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
)}
{title && (
<IconButton onClick={onClick}>
<Icon
name="X"
size={20}
/>
<CloseIcon />
</IconButton>
)}
</Stack>
@@ -78,10 +63,7 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
<ToastBody body={body} />
{!title && (
<IconButton onClick={onClick}>
<Icon
name="X"
size={20}
/>
<CloseIcon />
</IconButton>
)}
</Stack>
+7 -1
View File
@@ -5,6 +5,7 @@ import axios from "axios";
const initialState = {
isLoading: false,
authToken: "",
refreshToken: "",
user: "",
success: null,
msg: null,
@@ -138,6 +139,7 @@ const handleAuthFulfilled = (state, action) => {
state.success = action.payload.success;
state.msg = action.payload.msg;
state.authToken = action.payload.data.token;
state.refreshToken = action.payload.data.refreshToken || "";
state.user = action.payload.data.user;
};
const handleAuthRejected = (state, action) => {
@@ -188,11 +190,15 @@ const authSlice = createSlice({
reducers: {
clearAuthState: (state) => {
state.authToken = "";
state.refreshToken = "";
state.user = "";
state.isLoading = false;
state.success = true;
state.msg = "Logged out successfully";
},
setAuthToken: (state, action) => {
state.authToken = action.payload;
},
},
extraReducers: (builder) => {
// Register thunk
@@ -246,4 +252,4 @@ const authSlice = createSlice({
});
export default authSlice.reducer;
export const { clearAuthState } = authSlice.actions;
export const { clearAuthState, setAuthToken } = authSlice.actions;
+2 -5
View File
@@ -243,7 +243,6 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
const [monitorStats, setMonitorStats] = useState(undefined);
useEffect(() => {
const fetchMonitor = async () => {
@@ -255,8 +254,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
monitorId: monitorId,
dateRange: dateRange,
});
setMonitor(response.data.data.monitor);
setMonitorStats(response.data.data.monitorStats);
setMonitor(response.data.data);
} catch (error) {
setNetworkError(true);
} finally {
@@ -265,7 +263,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
};
fetchMonitor();
}, [monitorId, dateRange, updateTrigger]);
return [monitor, monitorStats, isLoading, networkError];
return [monitor, isLoading, networkError];
};
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
@@ -386,7 +384,6 @@ export const useUpdateMonitor = () => {
thresholds: monitor.thresholds,
secret: monitor.secret,
selectedDisks: monitor.selectedDisks,
enableProxmoxContainers: monitor.enableProxmoxContainers,
}),
};
await networkService.updateMonitor({
@@ -3,7 +3,7 @@ import Button from "@mui/material/Button";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useTheme } from "@emotion/react";
import Icon from "@/Components/v1/Icon";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import { useTranslation } from "react-i18next";
import Proptypes from "prop-types";
@@ -24,13 +24,7 @@ const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
<Button
variant="contained"
color="accent"
endIcon={
<Icon
name="ChevronDown"
size={20}
color="secondary.light"
/>
}
endIcon={<ArrowDropDownIcon sx={{ color: theme.palette.secondary.light }} />}
onClick={handleClick}
>
{t("teamPanel.addTeamMember.addMemberMenu")}
@@ -1,6 +1,6 @@
import Typography from "@mui/material/Typography";
import DataTable from "@/Components/v1/Table/index.jsx";
import Icon from "@/Components/v1/Icon";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import { useTranslation } from "react-i18next";
import { ROLES } from "../../../../Utils/roleUtils.js";
@@ -21,13 +21,11 @@ const RoleTable = ({ roles, handleDeleteRole }) => {
render: (row) => {
if (row === ROLES.SUPERADMIN) return null;
return (
<Icon
name="Trash2"
size={20}
<DeleteOutlineRoundedIcon
onClick={() => {
handleDeleteRole(row);
}}
style={{ cursor: "pointer" }}
sx={{ cursor: "pointer" }}
/>
);
},
+2 -2
View File
@@ -7,9 +7,9 @@ import { createToast } from "../../Utils/toastUtils.jsx";
import { forgotPassword } from "../../Features/Auth/authSlice.js";
import { Trans, useTranslation } from "react-i18next";
import Background from "@/assets/Images/background-grid.svg?react";
import EmailIcon from "@/assets/icons/email.svg?react";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import IconBox from "@/Components/v1/IconBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import "./index.css";
const CheckEmail = () => {
@@ -160,7 +160,7 @@ const CheckEmail = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Icon name="Mail" />
<EmailIcon alt={t("auth.forgotPassword.imageAlts.email")} />
</IconBox>
</Stack>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
+2 -2
View File
@@ -8,9 +8,9 @@ import { newOrChangedCredentials } from "../../Validation/validation.js";
import { useNavigate } from "react-router-dom";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Key from "@/assets/icons/key.svg?react";
import Background from "@/assets/Images/background-grid.svg?react";
import IconBox from "@/Components/v1/IconBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
@@ -165,7 +165,7 @@ const ForgotPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Icon name="Key" />
<Key alt={t("auth.forgotPassword.imageAlts.passwordKey")} />
</IconBox>
</Stack>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
@@ -4,9 +4,9 @@ import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice.js";
import Background from "@/assets/Images/background-grid.svg?react";
import ConfirmIcon from "@/assets/icons/check-outlined.svg?react";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import IconBox from "@/Components/v1/IconBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
@@ -94,7 +94,7 @@ const NewPasswordConfirmed = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Icon name="CheckCircle" />
<ConfirmIcon alt={t("auth.forgotPassword.imageAlts.passwordConfirm")} />
</IconBox>
</Stack>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
+2 -2
View File
@@ -11,9 +11,9 @@ import Check from "@/Components/v1/Check/Check.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import IconBox from "@/Components/v1/IconBox/index.jsx";
import LockIcon from "@/assets/icons/lock.svg?react";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Background from "@/assets/Images/background-grid.svg?react";
import Icon from "@/Components/v1/Icon";
import "./index.css";
import { useValidatePassword } from "./hooks/useValidatePassword.jsx";
import { Trans, useTranslation } from "react-i18next";
@@ -140,7 +140,7 @@ const SetNewPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Icon name="Lock" />
<LockIcon alt={t("auth.forgotPassword.imageAlts.lock")} />
</IconBox>
</Stack>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
@@ -2,7 +2,10 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Background from "@/assets/Images/background-grid.svg?react";
import Icon from "@/Components/v1/Icon";
import AlertIcon from "@/assets/icons/alert-icon.svg?react";
import CheckIcon from "@/assets/icons/check-icon.svg?react";
import CloseIcon from "@/assets/icons/close-icon.svg?react";
import WarningIcon from "@/assets/icons/warning-icon.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
@@ -23,40 +26,28 @@ const StatusBox = ({ title, value, status }) => {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="CheckCircle"
size={20}
/>
<CheckIcon />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="X"
size={20}
/>
<CloseIcon />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="AlertTriangle"
size={20}
/>
<WarningIcon />
</Box>
);
} else {
color = theme.palette.accent.main;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="Bell"
size={20}
/>
<AlertIcon />
</Box>
);
}
@@ -3,7 +3,8 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import Icon from "@/Components/v1/Icon";
import Incidents from "@/assets/icons/incidents.svg?react";
import CheckIcon from "@/assets/icons/check-icon.svg?react";
import SummaryCard from "../SummaryCard/index.jsx";
/**
@@ -42,39 +43,39 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
if (!totalCount || totalCount === 0) {
return (
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
<SummaryCard>
<Stack
direction="column"
alignItems="center"
justifyContent="center"
padding={theme.spacing(6)}
gap={theme.spacing(2)}
padding={theme.spacing(10)}
gap={theme.spacing(4)}
sx={{ flex: 1 }}
>
<Box
sx={{
color: theme.palette.success.main,
"& svg": {
width: 24,
height: 24,
width: 60,
height: 60,
"& path": { stroke: "currentColor", strokeWidth: 2 },
},
mb: theme.spacing(1),
mb: theme.spacing(2),
}}
>
<Icon
name="CheckCircle"
size={60}
/>
<CheckIcon />
</Box>
<Typography
variant="h1"
color={theme.palette.primary.contrastTextSecondary}
sx={{
fontSize: 13,
textTransform: "uppercase",
fontWeight: 500,
fontWeight: 600,
textAlign: "center",
color: theme.palette.success.lowContrast,
letterSpacing: theme.spacing(0.4),
}}
>
{t("incidentsPage.allSystemsAreOperational")}
@@ -85,40 +86,50 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
}
return (
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
<SummaryCard isHighPriority={true}>
<Stack
direction="column"
alignItems="center"
justifyContent="center"
gap={theme.spacing(2)}
sx={{ flex: 1 }}
spacing={theme.spacing(4)}
>
<Box
sx={{
color: theme.palette.error.lowContrast,
padding: theme.spacing(2),
"& svg": {
width: 24,
height: 24,
width: 60,
height: 60,
"& path": { stroke: "currentColor", strokeWidth: 2 },
},
}}
>
<Icon
name="AlertTriangle"
size={60}
/>
<Incidents />
</Box>
<Typography
variant="h1"
sx={{
fontSize: 32,
fontWeight: 600,
fontSize: `calc(${theme.typography.h1.fontSize} * 2.5)`,
fontWeight: 700,
color: theme.palette.error.lowContrast,
lineHeight: 1,
}}
>
{totalCount}
</Typography>
<Typography
variant="h2"
sx={{
textTransform: "uppercase",
fontWeight: 700,
letterSpacing: theme.spacing(0.4),
paddingTop: theme.spacing(3),
}}
>
{t("incidentsPage.incidentsActivePanelTitle")}
</Typography>
</Stack>
</SummaryCard>
);
@@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => {
<>
<Grid
container
spacing={"16px"}
spacing={3}
>
<Grid
item
@@ -3,7 +3,8 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import Icon from "@/Components/v1/Icon";
import Monitors from "@/assets/icons/monitors.svg?react";
import AverageResponseIcon from "@/assets/icons/status-pages.svg?react";
import { useTranslation } from "react-i18next";
const IncidentItem = ({ incident }) => {
@@ -36,68 +37,101 @@ const IncidentItem = ({ incident }) => {
const duration = calculateDuration();
const iconWrapperStyle = {
px: theme.spacing(2),
display: "flex",
justifyContent: "center",
mx: theme.spacing(2),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
width: 18,
height: 18,
},
"& svg path": {
stroke: "currentColor",
strokeWidth: 1.5,
},
};
return (
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 100px 80px",
alignItems: "center",
gap: theme.spacing(2),
cursor: "pointer",
width: "100%",
py: theme.spacing(0.5),
"&:hover": { opacity: 0.8 },
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
direction="column"
gap={theme.spacing(1.5)}
>
<Box sx={iconWrapperStyle}>
<Icon
name="Globe"
size={18}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
noWrap
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(1)}
>
{incident.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
<Box
sx={{
...iconWrapperStyle,
}}
>
<Monitors />
</Box>
<Stack
direction="row"
gap={theme.spacing(4)}
alignItems="baseline"
>
<Typography variant="body1">
{t("incidentsPage.incidentItemMonitor")}:
</Typography>
<Typography
variant="body1"
fontWeight={600}
>
{incident.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
</Stack>
</Stack>
<Stack
direction="row"
alignItems="center"
width="100%"
marginTop={theme.spacing(1)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
>
<Box sx={{ ...iconWrapperStyle }}>
<AverageResponseIcon />
</Box>
<Stack
direction="row"
alignItems="center"
spacing={2}
>
<Typography variant="body1">
{t("incidentsPage.incidentItemStatus")}:
</Typography>
</Stack>
<StatusLabel
status={isActive ? "down" : "up"}
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
customStyles={{
textTransform: "capitalize",
}}
/>
</Stack>
<Box sx={{ flexGrow: 1 }} />
<Typography
variant="body1"
fontWeight={500}
>
{duration}
</Typography>
</Stack>
</Stack>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<StatusLabel
status={isActive ? "down" : "up"}
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
customStyles={{
textTransform: "capitalize",
}}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
textAlign="right"
>
{duration}
</Typography>
</Box>
);
};
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import IncidentItem from "./IncidentItem.jsx";
import SummaryCard from "../SummaryCard/index.jsx";
import Icon from "@/Components/v1/Icon";
/**
* LatestIncidentsPanel Component
@@ -52,35 +51,21 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
return (
<SummaryCard title={t("incidentsPage.incidentsLatestPanelTitle")}>
{!incidents || incidents.length === 0 ? (
<Stack
direction="column"
alignItems="center"
justifyContent="center"
padding={theme.spacing(6)}
gap={theme.spacing(2)}
sx={{ flex: 1 }}
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexGrow: 1,
}}
>
<Box sx={{ mb: theme.spacing(1) }}>
<Icon
name="Check"
size={24}
color={theme.palette.success.main}
strokeWidth={2}
/>
</Box>
<Typography
sx={{
fontSize: 13,
textTransform: "uppercase",
fontWeight: 500,
textAlign: "center",
color: theme.palette.success.lowContrast,
}}
variant="body2"
textAlign="center"
>
{t("incidentsPage.incidentsLatestPanelEmpty")}
</Typography>
</Stack>
</Box>
) : (
<Stack gap={theme.spacing(4)}>
{incidents.map((incident, index) => (
@@ -1,9 +1,13 @@
import PropTypes from "prop-types";
import { Box, Stack, Typography, Divider } from "@mui/material";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
import { useTranslation } from "react-i18next";
import Icon from "@/Components/v1/Icon";
import { Divider } from "@mui/material";
import Clock from "@/assets/icons/maintenance.svg?react";
import Incidents from "@/assets/icons/incidents.svg?react";
import ResolutionItem from "@/assets/icons/interval-check.svg?react";
import NotificationIcon from "@/assets/icons/notifications.svg?react";
import SummaryCard from "../SummaryCard/index.jsx";
/**
@@ -31,19 +35,18 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
const { t } = useTranslation();
const totalResolutions =
(summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0);
const automaticPercentage =
totalResolutions > 0
? (summary?.totalAutomaticResolutions / totalResolutions) * 100
: 0;
const iconWrapperStyle = {
display: "flex",
justifyContent: "center",
mx: theme.spacing(2),
mx: theme.spacing(3),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
width: 18,
height: 18,
},
"& svg path": {
stroke: "currentColor",
strokeWidth: 1.5,
},
};
if (isLoading) {
@@ -73,87 +76,150 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
);
}
const getMostAffectedMonitor = () => {
if (!summary.total || summary.total === 0) {
return t("incidentsPage.none");
}
return summary.topMonitor?.monitorName || t("incidentsPage.none");
};
const rowStyle = {
py: theme.spacing(0.5),
minHeight: 32,
};
return (
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
<Stack gap={theme.spacing(4)}>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={rowStyle}
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
>
<Box
sx={{
...iconWrapperStyle,
}}
>
<Box sx={iconWrapperStyle}>
<Icon
name="Bell"
size={18}
/>
</Box>
<NotificationIcon />
</Box>
<Box>
<Typography
variant="body1"
fontWeight={500}
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.totalIncidents")}: {summary.total || 0}
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
</Typography>
</Stack>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={rowStyle}
>
<Box sx={iconWrapperStyle}>
<Icon
name="AlertTriangle"
size={18}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
>
{t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()}
</Typography>
</Stack>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={rowStyle}
>
<Box sx={iconWrapperStyle}>
<Icon
name="Wrench"
size={18}
<Incidents />
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.mostAffectedMonitor")} :{" "}
{summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
</Typography>
</Box>
</Stack>
<Divider />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box sx={iconWrapperStyle}>
<Clock />
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 600,
}}
>
{t("incidentsPage.avgResolutionTime")} :{" "}
{summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
</Typography>
</Box>
</Stack>
<Divider />
<Box padding={theme.spacing(2)}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
mb={theme.spacing(1.5)}
>
<Box sx={iconWrapperStyle}>
<ResolutionItem />
</Box>
<Box>
<Typography
variant="body1"
sx={{
fontWeight: 500,
lineHeight: 1.2,
}}
>
{t("incidentsPage.resolutions")}: {totalResolutions}
</Typography>
</Box>
</Stack>
<Box
sx={{
display: "flex",
height: 8,
borderRadius: 4,
overflow: "hidden",
bgcolor: theme.palette.accent.main,
width: "100%",
marginTop: theme.spacing(4),
}}
>
<Box
sx={{
width: `${automaticPercentage}%`,
bgcolor: theme.palette.warningSecondary.lowContrast,
height: "100%",
}}
/>
</Box>
<Typography
variant="body1"
fontWeight={500}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
paddingTop: theme.spacing(4),
width: "100%",
}}
>
{t("incidentsPage.avgResolutionTime")}:{" "}
{summary.total > 0
? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}`
: "N/A"}
</Typography>
</Stack>
{summary.totalAutomaticResolutions > 0 && (
<Typography
variant="body2"
fontWeight={500}
color={theme.palette.warningSecondary.contrastText}
>
{t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions})
</Typography>
)}
{summary.totalManualResolutions > 0 && (
<Typography
variant="body2"
fontWeight={500}
color={theme.palette.accent.main}
>
{t("incidentsPage.manual")} ({summary.totalManualResolutions})
</Typography>
)}
</Box>
</Box>
</Stack>
</SummaryCard>
);
@@ -8,14 +8,17 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
<Paper
elevation={0}
sx={{
padding: "16px",
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(4),
borderRadius: 3,
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
backgroundColor: theme.palette.primary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
border: isHighPriority
? `${theme.spacing(1.5)} solid ${theme.palette.error.lowContrast}`
: `${theme.spacing(1)} solid ${theme.palette.divider}`,
boxShadow: theme.palette.tertiary.cardShadow,
color: theme.palette.primary.contrastTextTertiary,
fontSize: theme.typography.body1.fontSize,
...sx,
@@ -30,12 +33,13 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
}}
>
<Typography
component="h2"
variant="h6"
sx={{
textTransform: "uppercase",
fontWeight: 500,
fontSize: 13,
color: theme.palette.primary.contrastTextSecondary,
fontWeight: 700,
fontSize: theme.typography.h2.fontSize,
letterSpacing: theme.spacing(0.5),
}}
>
{title}
@@ -45,9 +49,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
</Box>
)}
<Stack
mt={theme.spacing(2)}
paddingTop={theme.spacing(2)}
gap={theme.spacing(2)}
mt={theme.spacing(4)}
paddingTop={theme.spacing(5)}
gap={theme.spacing(4)}
sx={{ height: "100%" }}
>
{children}
+2 -2
View File
@@ -87,8 +87,8 @@ const Incidents2 = () => {
useEffect(() => {
const lookup = monitors?.reduce((acc, monitor) => {
acc[monitor.id] = {
_id: monitor.id,
acc[monitor._id] = {
_id: monitor._id,
name: monitor.name,
type: monitor.type,
};
@@ -2,7 +2,8 @@ import { useState } from "react";
import { Box, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import Icon from "@/Components/v1/Icon";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import PropTypes from "prop-types";
@@ -35,18 +36,12 @@ const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) =>
>
{monitor?.isActive ? (
<>
<Icon
name="PauseCircle"
size={20}
/>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<Icon
name="PlayCircle"
size={20}
/>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
@@ -18,7 +18,6 @@ const useInfrastructureMonitorForm = () => {
usage_temperature: "",
secret: "",
selectedDisks: [],
enableProxmoxContainers: false,
});
const onChangeForm = (name, value) => {
@@ -51,7 +50,6 @@ const useInfrastructureMonitorForm = () => {
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
selectedDisks: [],
enableProxmoxContainers: false,
}));
}, []);
@@ -86,7 +84,6 @@ const useInfrastructureMonitorForm = () => {
: "",
secret: monitor.secret || "",
selectedDisks: monitor.selectedDisks || [],
enableProxmoxContainers: monitor.enableProxmoxContainers ?? false,
}));
}, []);
return {
@@ -31,7 +31,6 @@ const useInfrastructureSubmit = () => {
: {}),
secret: infrastructureMonitor.secret,
selectedDisks: infrastructureMonitor.selectedDisks,
enableProxmoxContainers: infrastructureMonitor.enableProxmoxContainers,
};
return form;
};
@@ -51,7 +50,6 @@ const useInfrastructureSubmit = () => {
temperature,
usage_temperature,
selectedDisks,
enableProxmoxContainers,
...rest
} = form;
@@ -63,13 +61,12 @@ const useInfrastructureSubmit = () => {
};
const finalForm = {
...(isCreate ? {} : { id: monitorId }),
...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
selectedDisks,
enableProxmoxContainers,
thresholds,
};
// Handle create or update
@@ -5,7 +5,7 @@ import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx";
import Link from "@/Components/v1/Link/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { Box, Stack, Typography, Button, ButtonGroup, Switch, FormControlLabel } from "@mui/material";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx";
import MonitorActionButtons from "./Components/MonitorActionButtons.jsx";
@@ -22,9 +22,9 @@ import { useTranslation } from "react-i18next";
import {
useDeleteMonitor,
useFetchGlobalSettings,
useFetchMonitorById,
useFetchHardwareMonitorById,
usePauseMonitor,
} from "@/Hooks/monitorHooks.js";
} from "../../../Hooks/monitorHooks.js";
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx";
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx";
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx";
@@ -37,16 +37,14 @@ const CreateInfrastructureMonitor = () => {
const { t } = useTranslation();
// State
const [monitor, setMonitor] = useState(null);
const [https, setHttps] = useState(false);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [availableDisks, setAvailableDisks] = useState([]);
// Fetch monitor details if editing
const [isLoading] = useFetchMonitorById({
const [monitor, isLoading] = useFetchHardwareMonitorById({
monitorId,
setMonitor,
updateTrigger: true,
updateTrigger,
});
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
@@ -152,7 +150,8 @@ const CreateInfrastructureMonitor = () => {
const handleRemove = async (event) => {
event.preventDefault();
await deleteMonitor({ monitor, redirect: "/infrastructure" });
const TEMP_MONITOR = { id: monitor._id };
await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/infrastructure" });
};
const isBusy =
@@ -368,34 +367,6 @@ const CreateInfrastructureMonitor = () => {
/>
)}
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("proxmoxContainerMonitoring")}
</Typography>
<Typography component="p">
{t("proxmoxContainerMonitoringDescription")}
</Typography>
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: theme.spacing(0) }}
control={
<Switch
name="enableProxmoxContainers"
checked={infrastructureMonitor.enableProxmoxContainers}
onChange={handleCheckboxChange}
sx={{ mr: theme.spacing(2) }}
/>
}
label={t("enableProxmoxContainers")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
@@ -1,141 +0,0 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx";
import {
TzTick,
InfrastructureTooltip,
NetworkTick,
PercentTick,
} from "@/Components/v1/Charts/Utils/chartUtils.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
const ContainerCharts = ({ containerData, dateRange }) => {
const theme = useTheme();
const { t } = useTranslation();
const { formatBytesPerSecondString } = useHardwareUtils();
if (!containerData?.length) {
return <Typography>{t("noContainerStatsAvailable")}</Typography>;
}
const formatPercentage = (value) => `${(value * 100).toFixed(1)}%`;
const configs = [
{
type: "container-cpu",
data: containerData,
dataKeys: ["cpuUsage"],
heading: t("cpuUsage"),
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
yLabel: t("usage"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <PercentTick />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"cpuUsage"}
yLabel={t("cpuUsage") + ": "}
dateRange={dateRange}
formatter={formatPercentage}
/>
),
},
{
type: "container-memory",
data: containerData,
dataKeys: ["memoryUsage"],
heading: t("memoryUsage"),
strokeColor: theme.palette.accent.main,
gradientStartColor: theme.palette.accent.main,
yLabel: t("usage"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <PercentTick />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.accent.main}
yKey={"memoryUsage"}
yLabel={t("memoryUsage") + ": "}
dateRange={dateRange}
formatter={formatPercentage}
/>
),
},
{
type: "container-disk-io",
data: containerData,
dataKeys: ["diskRead", "diskWrite"],
heading: t("diskIO"),
// Don't pass strokeColor - let chart use distinct colors from palette
yLabel: t("rate"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <NetworkTick formatter={formatBytesPerSecondString} />,
toolTip: (
<InfrastructureTooltip
dotColor={"#3182bd"}
yKey={"diskRead"}
yLabel={t("diskRead") + ": "}
dotColor2={"#6baed6"}
yKey2={"diskWrite"}
yLabel2={t("diskWrite") + ": "}
dateRange={dateRange}
formatter={formatBytesPerSecondString}
/>
),
},
{
type: "container-network-io",
data: containerData,
dataKeys: ["netIn", "netOut"],
heading: t("networkIO"),
// Don't pass strokeColor - let chart use distinct colors from palette
yLabel: t("rate"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <NetworkTick formatter={formatBytesPerSecondString} />,
toolTip: (
<InfrastructureTooltip
dotColor={"#3182bd"}
yKey={"netIn"}
yLabel={t("networkIn") + ": "}
dotColor2={"#6baed6"}
yKey2={"netOut"}
yLabel2={t("networkOut") + ": "}
dateRange={dateRange}
formatter={formatBytesPerSecondString}
/>
),
},
];
return (
<Stack
direction={"row"}
gap={theme.spacing(8)}
flexWrap="wrap"
sx={{
"& > *": {
flexBasis: `calc(50% - ${theme.spacing(8)})`,
maxWidth: `calc(50% - ${theme.spacing(8)})`,
},
}}
>
{configs.map((config) => (
<InfraAreaChart
key={config.type}
config={config}
/>
))}
</Stack>
);
};
ContainerCharts.propTypes = {
containerData: PropTypes.array.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default ContainerCharts;
@@ -1,130 +0,0 @@
import PropTypes from "prop-types";
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import StatBox from "@/Components/v1/StatBox/index.jsx";
import { Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
const formatUptime = (seconds) => {
if (!seconds || seconds === 0) return "0s";
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m`;
}
return `${Math.floor(seconds)}s`;
};
const formatPercentage = (value) => {
if (value === undefined || value === null) return "0%";
return `${(value * 100).toFixed(1)}%`;
};
// Map container status to Checkmate monitor status for theming
const containerStatusToMonitorStatus = (status) => {
if (status === "running") return "up";
if (status === "stopped") return "down";
return "pending";
};
const ContainerStatBoxes = ({ shouldRender, container }) => {
const { t } = useTranslation();
const { formatBytes } = useHardwareUtils();
if (!container) {
return <Typography>{t("noContainerSelected")}</Typography>;
}
const monitorStatus = containerStatusToMonitorStatus(container.status);
return (
<StatusBoxes
shouldRender={shouldRender}
flexWrap="wrap"
>
<StatBox
gradient={true}
status={monitorStatus}
heading={t("status")}
subHeading={container.status || "unknown"}
/>
<StatBox
heading={t("type")}
subHeading={
<Typography component="span">
{(container.type || "lxc").toUpperCase()}
</Typography>
}
/>
<StatBox
heading={t("uptime")}
subHeading={formatUptime(container.uptime)}
/>
<StatBox
heading={t("cpuUsage")}
subHeading={
<>
{formatPercentage(container.cpu_usage)}
<Typography component="span">
({container.cpu_cores || 0} {t("cores")})
</Typography>
</>
}
/>
<StatBox
heading={t("memoryUsage")}
subHeading={
<>
{formatBytes(container.memory_used)}
<Typography component="span"> / </Typography>
{formatBytes(container.memory_total)}
</>
}
/>
<StatBox
heading={t("diskUsage")}
subHeading={
<>
{formatBytes(container.disk_used)}
<Typography component="span"> / </Typography>
{formatBytes(container.disk_total)}
</>
}
/>
</StatusBoxes>
);
};
ContainerStatBoxes.propTypes = {
shouldRender: PropTypes.bool.isRequired,
container: PropTypes.shape({
vmid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
node: PropTypes.string,
status: PropTypes.string,
type: PropTypes.string,
uptime: PropTypes.number,
cpu_cores: PropTypes.number,
cpu_usage: PropTypes.number,
memory_used: PropTypes.number,
memory_total: PropTypes.number,
memory_usage: PropTypes.number,
swap_used: PropTypes.number,
swap_total: PropTypes.number,
disk_used: PropTypes.number,
disk_total: PropTypes.number,
disk_read: PropTypes.number,
disk_write: PropTypes.number,
network_in: PropTypes.number,
network_out: PropTypes.number,
}),
};
export default ContainerStatBoxes;
@@ -1,125 +0,0 @@
import PropTypes from "prop-types";
import { useState, useEffect } from "react";
import { Box, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import ContainerStatBoxes from "./ContainerStatBoxes.jsx";
import ContainerCharts from "./ContainerCharts.jsx";
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
const getAvailableContainers = (containers) => {
return (containers || [])
.filter((c) => c.vmid && c.name)
.map((c) => ({
vmid: String(c.vmid),
name: c.name,
node: c.node,
status: c.status,
type: c.type,
}));
};
const getContainerData = (checks, vmid) => {
if (!vmid) return [];
return (checks || [])
.map((check) => {
const container = (check.containers || []).find(
(c) => String(c.vmid) === String(vmid)
);
if (!container) return null;
return {
_id: check._id,
cpuUsage: container.avgCpuUsage ?? 0,
memoryUsage: container.avgMemoryUsage ?? 0,
diskRead: container.deltaDiskRead ?? 0,
diskWrite: container.deltaDiskWrite ?? 0,
netIn: container.deltaNetIn ?? 0,
netOut: container.deltaNetOut ?? 0,
};
})
.filter(Boolean);
};
const ContainerStats = ({ containers, checks, isLoading, dateRange, setDateRange }) => {
const { t } = useTranslation();
const theme = useTheme();
const availableContainers = getAvailableContainers(containers);
const [selectedContainer, setSelectedContainer] = useState("");
useEffect(() => {
if (availableContainers.length > 0 && !selectedContainer) {
setSelectedContainer(availableContainers[0].vmid);
}
}, [availableContainers, selectedContainer]);
const containerData = getContainerData(checks, selectedContainer);
const currentContainer = containers?.find(
(c) => String(c.vmid) === String(selectedContainer)
);
if (!containers?.length) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={200}
>
<Typography color={theme.palette.primary.contrastTextTertiary}>
{t("noContainersAvailable")}
</Typography>
</Box>
);
}
return (
<>
<ContainerStatBoxes
shouldRender={!isLoading}
container={currentContainer}
/>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
>
{availableContainers.length > 0 && (
<Select
name="container"
label={t("container")}
value={selectedContainer}
onChange={(e) => setSelectedContainer(e.target.value)}
items={availableContainers.map((c) => ({
_id: c.vmid,
name: `${c.name} (${c.vmid}) - ${c.node}`,
}))}
sx={{ minWidth: 250 }}
/>
)}
<MonitorTimeFrameHeader
isLoading={isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
</Box>
<ContainerCharts
containerData={containerData}
dateRange={dateRange}
/>
</>
);
};
ContainerStats.propTypes = {
containers: PropTypes.array,
checks: PropTypes.array,
isLoading: PropTypes.bool.isRequired,
dateRange: PropTypes.string.isRequired,
setDateRange: PropTypes.func.isRequired,
};
export default ContainerStats;
@@ -8,7 +8,6 @@ import GaugeBoxes from "./Components/GaugeBoxes/index.jsx";
import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import NetworkStats from "./Components/NetworkStats/index.jsx";
import ContainerStats from "./Components/ContainerStats/index.jsx";
import CustomTabList from "@/Components/v1/Tab/index.jsx";
import TabContext from "@mui/lab/TabContext";
@@ -103,12 +102,6 @@ const InfrastructureDetails = () => {
label={t("network")}
value="network"
/>
{monitor?.enableProxmoxContainers && (
<Tab
label={t("containers")}
value="containers"
/>
)}
</CustomTabList>
{tab === "details" && (
<>
@@ -141,15 +134,6 @@ const InfrastructureDetails = () => {
setDateRange={setDateRange}
/>
)}
{tab === "containers" && monitor?.enableProxmoxContainers && (
<ContainerStats
containers={monitor?.stats?.aggregateData?.latestCheck?.containers || []}
isLoading={isLoading}
checks={monitor?.stats?.checks}
dateRange={dateRange}
setDateRange={setDateRange}
/>
)}
</TabContext>
</Stack>
);
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import ClearIcon from "@mui/icons-material/Clear";
import { useTranslation } from "react-i18next";
/**
@@ -68,12 +68,7 @@ const Filter = ({
color={theme.palette.primary.contrastText}
onClick={handleReset}
variant="contained"
endIcon={
<Icon
name="X"
size={18}
/>
}
endIcon={<ClearIcon />}
sx={{
visibility: isFilterActive ? "visible" : "hidden",
}}
@@ -7,7 +7,7 @@ import { Stack } from "@mui/material";
import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx";
import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx";
// Assets
import Icon from "@/Components/v1/Icon";
import CPUChipIcon from "@/assets/icons/cpu-chip.svg?react";
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
// Utils
@@ -67,9 +67,9 @@ const MonitorsTable = ({
alignItems={"center"}
gap=".25rem"
>
<Icon
name="Cpu"
size={20}
<CPUChipIcon
width={20}
height={20}
/>
{row.processor}
</Stack>
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "@/Utils/toastUtils.jsx";
import { IconButton, Menu, MenuItem } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import Settings from "@/assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import { networkService } from "@/Utils/NetworkService.js";
@@ -100,10 +100,7 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
}}
ref={anchor}
>
<Icon
name="Settings"
size={20}
/>
<Settings />
</IconButton>
<Menu
@@ -12,16 +12,13 @@ import { Box } from "@mui/material";
const BaseContainer = ({children}) => {
const theme = useTheme()
return(
<Box
<Box
sx={{
padding: theme.spacing(3),
borderRadius: 4,
border: `1px solid ${theme.palette.primary.lowContrast}`,
minWidth: 200,
width: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
[theme.breakpoints.down("md")]: {
width: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
},
borderRadius: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
minWidth: 250,
width: "fit-content",
}}>
{children}
</Box>
@@ -102,7 +99,7 @@ const Gauges = ({ diagnostics, isLoading }) => {
return (
<Stack
direction="row"
gap={theme.spacing(8)}
spacing={theme.spacing(8)}
flexWrap="wrap"
>
<InfrastructureStyleGauge
+47 -121
View File
@@ -2,17 +2,14 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import Typography from "@mui/material/Typography";
import DataTable from "@/Components/v1/Table/index.jsx";
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import Divider from "@mui/material/Divider";
import { useFetchLogs } from "../../../Hooks/logHooks.js";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
const LevelBadge = ({ level }) => {
const theme = useTheme();
const formatLog = (theme, log, idx) => {
const levelColors = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
@@ -20,38 +17,31 @@ const LevelBadge = ({ level }) => {
debug: theme.palette.accent.main,
};
const color = levelColors[level] || theme.palette.primary.contrastText;
const color = levelColors[log.level] || theme.palette.primary.contrastText;
return (
<Box
component="span"
sx={{
color: color,
fontWeight: 600,
textTransform: "uppercase",
fontSize: 12,
}}
>
{level}
</Box>
<span key={idx}>
<span>[{log.timestamp}]</span>{" "}
<span style={{ color, fontWeight: "bold" }}>{log.level.toUpperCase()}</span>
{": "}
{`(${log.service})`}
{`(${log.method})`}
{": "}
{log.message}
<br />
</span>
);
};
const formatTimestamp = (timestamp) => {
if (!timestamp) return "-";
const date = new Date(timestamp);
return date.toLocaleString();
};
const Logs = () => {
// Local state
const [logLevel, setLogLevel] = useState("all");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(15);
// Hooks
const theme = useTheme();
const { t } = useTranslation();
const [logs, isLoading, error] = useFetchLogs();
// Setup
const LOG_LEVELS = [
{ _id: "all", name: t("logsPage.logLevelSelect.values.all") },
{ _id: "info", name: t("logsPage.logLevelSelect.values.info") },
@@ -59,116 +49,52 @@ const Logs = () => {
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
];
const headers = [
{
id: "timestamp",
content: t("logsPage.table.timestamp"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>
{formatTimestamp(row.timestamp)}
</Typography>
),
},
{
id: "level",
content: t("logsPage.table.level"),
render: (row) => <LevelBadge level={row.level} />,
},
{
id: "service",
content: t("logsPage.table.service"),
render: (row) => (
<Typography sx={{ fontSize: 13 }}>{row.service || "-"}</Typography>
),
},
{
id: "method",
content: t("logsPage.table.method"),
render: (row) => (
<Typography sx={{ fontSize: 13, fontFamily: "monospace" }}>{row.method || "-"}</Typography>
),
},
{
id: "message",
content: t("logsPage.table.message"),
render: (row) => (
<Typography
sx={{
fontSize: 13,
maxWidth: 400,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{row.message || "-"}
</Typography>
),
},
];
const filteredLogs = logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => ({ ...log, id: idx }));
const paginatedLogs = filteredLogs?.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleLogLevelChange = (e) => {
setLogLevel(e.target.value);
setPage(0);
};
return (
<Stack gap={theme.spacing(4)}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
sx={{
position: "sticky",
top: theme.spacing(17),
backdropFilter: "blur(10px)",
paddingTop: theme.spacing(4),
paddingLeft: theme.spacing(6),
}}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
items={LOG_LEVELS}
value={logLevel}
onChange={handleLogLevelChange}
onChange={(e) => {
setLogLevel(e.target.value);
}}
/>
</Stack>
<DataTable
shouldRender={!isLoading}
headers={headers}
data={paginatedLogs || []}
config={{
emptyView: t("logsPage.noLogs"),
<Box
component="pre"
sx={{
fontFamily: "monospace",
color: theme.palette.primary.contrastText,
padding: 2,
borderRadius: 1,
overflowX: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
}}
/>
{filteredLogs?.length > 0 && (
<Pagination
paginationLabel={t("logsPage.table.logs")}
itemCount={filteredLogs?.length || 0}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
)}
>
<code>
{logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => formatLog(theme, log, idx))}
</code>
</Box>
</Stack>
);
};
@@ -1,6 +1,6 @@
// Components
import { Stack, Typography } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import DeleteIcon from "@mui/icons-material/Delete";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
@@ -14,10 +14,8 @@ const MonitorListItem = ({ monitor, onDelete }) => {
width="100%"
>
<Typography flexGrow={1}>{monitor.name}</Typography>
<Icon
name="Trash2"
size={20}
style={{ cursor: "pointer" }}
<DeleteIcon
sx={{ cursor: "pointer" }}
onClick={() => onDelete(monitor)}
/>
</Stack>
@@ -16,7 +16,7 @@ import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Icon from "@/Components/v1/Icon";
import CalendarIcon from "@/assets/icons/calendar.svg?react";
import "./index.css";
import Search from "@/Components/v1/Inputs/Search/index.jsx";
import { logger } from "@/Utils/Logger.js";
@@ -217,14 +217,7 @@ const CreateMaintenance = () => {
disablePast
disableHighlightToday
value={form.startDate}
slots={{
openPickerIcon: () => (
<Icon
name="Calendar"
size={20}
/>
),
}}
slots={{ openPickerIcon: () => <CalendarIcon /> }}
slotProps={{
switchViewButton: { sx: { display: "none" } },
nextIconButton: { sx: { ml: theme.spacing(2) } },
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { IconButton, Menu, MenuItem } from "@mui/material";
import { logger } from "@/Utils/Logger.js";
import Icon from "@/Components/v1/Icon";
import Settings from "@/assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import { networkService } from "../../../../main.jsx";
import { createToast } from "@/Utils/toastUtils.jsx";
@@ -98,10 +98,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
},
}}
>
<Icon
name="Settings"
size={20}
/>
<Settings />
</IconButton>
<Menu
@@ -2,7 +2,8 @@ import PropTypes from "prop-types";
import { Box } from "@mui/material";
import DataTable from "@/Components/v1/Table/index.jsx";
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import Icon from "@/Components/v1/Icon";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import ActionsMenu from "./ActionsMenu/index.jsx";
import { memo } from "react";
import { useDispatch, useSelector } from "react-redux";
@@ -67,15 +68,9 @@ const MaintenanceTable = ({
}}
>
{sort.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
<ArrowUpwardRoundedIcon />
) : (
<Icon
name="ArrowDown"
size={18}
/>
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
@@ -94,15 +89,9 @@ const MaintenanceTable = ({
}}
>
{sort.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
<ArrowUpwardRoundedIcon />
) : (
<Icon
name="ArrowDown"
size={18}
/>
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
@@ -1,7 +1,7 @@
// Components
import Menu from "@mui/material/Menu";
import IconButton from "@mui/material/IconButton";
import Icon from "@/Components/v1/Icon";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import MenuItem from "@mui/material/MenuItem";
// Utils
@@ -49,10 +49,7 @@ const ActionMenu = ({ notification, onDelete }) => {
onClick={handleClick}
onMouseDown={(e) => e.stopPropagation()}
>
<Icon
name="Settings"
size={20}
/>
<SettingsOutlinedIcon />
</IconButton>
<Menu
+4 -9
View File
@@ -3,9 +3,10 @@ import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/mater
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Icon from "@/Components/v1/Icon";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import PulseDot from "@/Components/v1/Animated/PulseDot.jsx";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton.jsx";
import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
import Dialog from "@/Components/v1/Dialog/index.jsx";
@@ -312,18 +313,12 @@ const PageSpeedSetup = () => {
>
{monitor?.isActive ? (
<>
<Icon
name="PauseCircle"
size={20}
/>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<Icon
name="PlayCircle"
size={20}
/>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
@@ -1,6 +1,6 @@
import { Box, Typography, Divider } from "@mui/material";
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
import Icon from "@/Components/v1/Icon";
import MetricsIcon from "@/assets/icons/ruler-icon.svg?react";
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -10,12 +10,7 @@ const AreaChartLegend = ({ metrics, handleMetrics }) => {
const { t } = useTranslation();
return (
<LegendBox
icon={
<Icon
name="Ruler"
size={20}
/>
}
icon={<MetricsIcon />}
header="Metrics"
>
<Box>
@@ -1,7 +1,7 @@
import { Stack, Box, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import SpeedometerIcon from "@/assets/icons/speedometer-icon.svg?react";
import PropTypes from "prop-types";
const PieChartLegend = ({ audits }) => {
@@ -9,12 +9,7 @@ const PieChartLegend = ({ audits }) => {
return (
<LegendBox
icon={
<Icon
name="Gauge"
size={20}
/>
}
icon={<SpeedometerIcon />}
header="Performance metrics"
sx={{ flex: 1 }}
>
@@ -2,7 +2,7 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import AreaChart from "../Charts/AreaChart.jsx";
import AreaChartLegend from "../Charts/AreaChartLegend.jsx";
import SkeletonLayout from "./skeleton.jsx";
import Icon from "@/Components/v1/Icon";
import ScoreIcon from "@/assets/icons/monitor-graph-line.svg?react";
import { Stack } from "@mui/material";
import PropTypes from "prop-types";
@@ -24,12 +24,7 @@ const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) =
>
<ChartBox
justifyContent="flex-start"
icon={
<Icon
name="TrendingUp"
size={20}
/>
}
icon={<ScoreIcon />}
header="Score history"
height="100%"
borderRadiusRight={16}
@@ -4,21 +4,9 @@ import { Typography } from "@mui/material";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js";
import { useTranslation } from "react-i18next";
const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => {
const lastChecked = monitorStats?.lastCheckTimestamp
? Date.now() - monitorStats.lastCheckTimestamp
: 0;
// Determine time since last failure
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt);
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
const time = getHumanReadableDuration(lastChecked);
const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
const uptimeDuration = getHumanReadableDuration(monitor?.uptimeDuration);
const time = getHumanReadableDuration(monitor?.lastChecked);
const { t } = useTranslation();
@@ -28,7 +16,7 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => {
heading="checks since"
subHeading={
<>
{streakTime}
{uptimeDuration}
<Typography component="span">{t("ago")}</Typography>
</>
}
@@ -1,5 +1,5 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import PerformanceIcon from "@/assets/icons/performance-report.svg?react";
import PieChart from "../Charts/PieChart.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -17,12 +17,7 @@ const PerformanceReport = ({ shouldRender, audits }) => {
return (
<ChartBox
icon={
<Icon
name="Layers"
size={20}
/>
}
icon={<PerformanceIcon />}
header="Performance report"
Legend={<PieChartLegend audits={audits} />}
borderRadiusRight={16}
+2 -2
View File
@@ -35,7 +35,7 @@ const PageSpeedDetails = () => {
});
const [trigger, setTrigger] = useState(false);
// Network
const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({
const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({
monitorId,
dateRange: "day",
updateTrigger: trigger,
@@ -95,7 +95,7 @@ const PageSpeedDetails = () => {
/>
<PageSpeedStatusBoxes
shouldRender={!isLoading}
monitorStats={monitorStats}
monitor={monitor}
/>
<MonitorTimeFrameHeader
shouldRender={!isLoading}
@@ -1,5 +1,6 @@
import PropTypes from "prop-types";
import Icon from "@/Components/v1/Icon";
// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react";
import PageSpeedIcon from "@/assets/icons/page-speed.svg?react";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
@@ -240,10 +241,7 @@ const Card = ({ monitor }) => {
}}
>
<IconBox>
<Icon
name="Gauge"
size={20}
/>
<PageSpeedIcon />
</IconBox>
<Typography
component="h2"
@@ -1,6 +1,7 @@
// Components
import { Stack, Typography } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
import DeleteIcon from "@mui/icons-material/Delete";
// Utils
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
@@ -27,15 +28,10 @@ const MonitorListItem = ({
justifyContent={"start"}
border={`1px solid ${theme.palette.primary.lowContrast}`}
>
<Icon
name="GripVertical"
size={20}
/>
<ReorderRoundedIcon />
<Typography>{monitor.name}</Typography>
<Icon
name="Trash2"
size={20}
style={{ marginLeft: "auto", cursor: "pointer" }}
<DeleteIcon
sx={{ marginLeft: "auto" }}
onClick={() => {
onDelete(monitor);
}}
@@ -1,6 +1,6 @@
import { Button, Box } from "@mui/material";
import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx";
import Icon from "@/Components/v1/Icon";
import ImageIcon from "@mui/icons-material/Image";
import { useTranslation } from "react-i18next";
import { formatBytes } from "../../../../../Utils/fileUtils.js";
const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => {
@@ -8,12 +8,7 @@ const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors
if (isLoading) {
return (
<ProgressUpload
icon={
<Icon
name="Image"
size={20}
/>
}
icon={<ImageIcon />}
label={logo?.name}
size={formatBytes(logo?.size)}
progress={progressValue}
@@ -1,8 +1,9 @@
// Components
import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "@/Components/v1/Image/index.jsx";
import Icon from "@/Components/v1/Icon";
import SettingsIcon from "@/assets/icons/settings-bold.svg?react";
import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
@@ -45,12 +46,7 @@ const Controls = ({ url, type }) => {
},
}}
>
<Icon
name="Settings"
size={18}
style={{ marginRight: "8px" }}
/>{" "}
{t("configure")}
<SettingsIcon /> {t("configure")}
</Button>
</Box>
</Stack>
@@ -115,10 +111,7 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => {
}}
>
<Typography>{t("publicLink")}</Typography>
<Icon
name="ExternalLink"
size={18}
/>
<ArrowOutwardIcon />
</Stack>
)}
</Stack>
@@ -1,6 +1,7 @@
// Components
import { Stack, Typography } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
// Utils
import { useTheme } from "@mui/material/styles";
@@ -9,10 +10,8 @@ import PropTypes from "prop-types";
const getMonitorStatus = (monitors, theme) => {
const monitorsStatus = {
icon: (
<Icon
name="AlertTriangle"
size={24}
color="primary.contrastTextSecondaryDarkBg"
<ErrorOutlineIcon
sx={{ color: theme.palette.primary.contrastTextSecondaryDarkBg }}
/>
),
};
@@ -20,10 +19,8 @@ const getMonitorStatus = (monitors, theme) => {
monitorsStatus.msg = "All systems operational";
monitorsStatus.color = theme.palette.success.lowContrast;
monitorsStatus.icon = (
<Icon
name="CheckCircle2"
size={24}
color="primary.contrastTextSecondaryDarkBg"
<CheckCircleIcon
sx={{ color: theme.palette.primary.contrastTextSecondaryDarkBg }}
/>
);
}
@@ -2,7 +2,7 @@ import DataTable from "@/Components/v1/Table/index.jsx";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import Icon from "@/Components/v1/Icon";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
import { Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
const StatusPagesTable = ({ data }) => {
@@ -48,12 +48,7 @@ const StatusPagesTable = ({ data }) => {
}}
>
<Typography>{content}</Typography>
{row.isPublished && (
<Icon
name="ExternalLink"
size={18}
/>
)}
{row.isPublished && <ArrowOutwardIcon />}
</Stack>
);
},
+5 -12
View File
@@ -28,7 +28,10 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { monitorValidation } from "../../../Validation/validation.js";
import { createToast } from "../../../Utils/toastUtils.jsx";
import Icon from "@/Components/v1/Icon";
import {
PauseOutlined as PauseOutlinedIcon,
PlayArrowOutlined as PlayArrowOutlinedIcon,
} from "@mui/icons-material";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js";
import { useParams } from "react-router-dom";
@@ -426,17 +429,7 @@ const UptimeCreate = ({ isClone = false }) => {
color="secondary"
loading={isBusy}
startIcon={
monitor?.isActive ? (
<Icon
name="Pause"
size={18}
/>
) : (
<Icon
name="Play"
size={18}
/>
)
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={handlePause}
>
@@ -1,7 +1,9 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import UptimeIcon from "@/assets/icons/uptime-icon.svg?react";
import IncidentsIcon from "@/assets/icons/incidents.svg?react";
import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
import UpBarChart from "../Charts/UpBarChart.jsx";
import DownBarChart from "../Charts/DownBarChart.jsx";
import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx";
@@ -36,12 +38,7 @@ const ChartBoxes = ({
gap={theme.spacing(8)}
>
<ChartBox
icon={
<Icon
name="Activity"
size={20}
/>
}
icon={<UptimeIcon />}
header="Uptime"
isEmpty={monitorData?.groupedUpChecks?.length === 0}
>
@@ -92,12 +89,7 @@ const ChartBoxes = ({
/>
</ChartBox>
<ChartBox
icon={
<Icon
name="AlertTriangle"
size={20}
/>
}
icon={<IncidentsIcon />}
header="Incidents"
noDataMessage={noIncidentsMessage}
isEmpty={monitorData?.groupedDownChecks?.length === 0}
@@ -131,12 +123,7 @@ const ChartBoxes = ({
/>
</ChartBox>
<ChartBox
icon={
<Icon
name="BarChart3"
size={20}
/>
}
icon={<AverageResponseIcon />}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitorData?.groupedAvgResponseTime ?? 0} />
@@ -1,6 +1,6 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx";
import Icon from "@/Components/v1/Icon";
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx";
import PropTypes from "prop-types";
@@ -11,12 +11,7 @@ const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange })
return (
<ChartBox
icon={
<Icon
name="TrendingUp"
size={20}
/>
}
icon={<ResponseTimeIcon />}
header="Response Times"
>
<MonitorDetailsAreaChart
@@ -1,6 +1,6 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import PropTypes from "prop-types";
import Icon from "@/Components/v1/Icon";
import HistoryIcon from "@/assets/icons/history-icon.svg?react";
import Table from "@/Components/v1/Table/index.jsx";
import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
@@ -184,12 +184,7 @@ const ResponseTable = ({
return (
<ChartBox
icon={
<Icon
name="History"
size={20}
/>
}
icon={<HistoryIcon />}
header="Response Times"
height="100%"
>
@@ -18,10 +18,7 @@ const UptimeStatusBoxes = ({
// Determine time since last failure
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt);
const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0;
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp;
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import ClearIcon from "@mui/icons-material/Clear";
import { useTranslation } from "react-i18next";
/**
@@ -131,12 +131,7 @@ const Filter = ({
color={theme.palette.primary.contrastText}
onClick={handleReset}
variant="contained"
endIcon={
<Icon
name="X"
size={18}
/>
}
endIcon={<ClearIcon />}
sx={{
visibility: isFilterActive ? "visible" : "hidden",
}}
@@ -1,8 +1,9 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import Arrow from "@/assets/icons/top-right-arrow.svg?react";
import Background from "@/assets/Images/background-grid.svg?react";
import ClockSnooze from "@/assets/icons/clock-snooze.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
@@ -19,30 +20,21 @@ const StatusBox = ({ title, value, status }) => {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
<Icon
name="ArrowUpRight"
size={20}
/>
<Arrow />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
<Icon
name="ArrowUpRight"
size={20}
/>
<Arrow />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="Clock"
size={20}
/>
<ClockSnooze />
</Box>
);
}
@@ -1,7 +1,8 @@
// Components
import { Box, Stack } from "@mui/material";
import DataTable from "@/Components/v1/Table/index.jsx";
import Icon from "@/Components/v1/Icon";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import Host from "@/Components/v1/Host/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import BarChart from "@/Components/v1/Charts/BarChart/index.jsx";
@@ -91,15 +92,9 @@ const UptimeDataTable = ({
}}
>
{sort?.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
<ArrowUpwardRoundedIcon />
) : (
<Icon
name="ArrowDown"
size={18}
/>
<ArrowDownwardRoundedIcon />
)}
</Stack>
</Stack>
@@ -133,15 +128,9 @@ const UptimeDataTable = ({
}}
>
{sort?.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
<ArrowUpwardRoundedIcon fontSize="18px" />
) : (
<Icon
name="ArrowDown"
size={18}
/>
<ArrowDownwardRoundedIcon fontSize="18px" />
)}
</Stack>
</Stack>
+83 -4
View File
@@ -2,7 +2,7 @@ import axios from "axios";
import i18next from "i18next";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
import { clearAuthState, setAuthToken } from "../Features/Auth/authSlice";
class NetworkService {
constructor(store, dispatch, navigate) {
this.store = store;
@@ -26,7 +26,7 @@ class NetworkService {
(config) => {
const currentLanguage = i18next.language || "en";
const { authToken } = store.getState().auth;
const { authToken, refreshToken } = store.getState().auth;
config.headers = {
Authorization: `Bearer ${authToken}`,
@@ -34,15 +34,36 @@ class NetworkService {
...config.headers,
};
// Only send refresh token to the refresh endpoint
if (refreshToken && config.url?.includes("/auth/refresh")) {
config.headers["x-refresh-token"] = refreshToken;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Track if we're currently refreshing to prevent multiple refresh calls
this.isRefreshing = false;
this.failedQueue = [];
const processQueue = (error, token = null) => {
this.failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
this.failedQueue = [];
};
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
async (error) => {
// Handle network errors (server unreachable)
if (error.code === "ERR_NETWORK") {
// Navigate to server unreachable page
@@ -51,8 +72,66 @@ class NetworkService {
return Promise.reject(error);
}
// Handle authentication errors
const originalRequest = error.config;
// Handle 403 - server is telling us to request a new access token
// Skip refresh logic for the refresh endpoint itself to prevent circular dependency
const isRefreshEndpoint = originalRequest.url?.includes("/auth/refresh");
if (
error.response &&
error.response.status === 403 &&
!originalRequest._retry &&
!isRefreshEndpoint
) {
// Check if we have a refresh token before attempting refresh
const { refreshToken } = store.getState().auth;
if (!refreshToken) {
dispatch(clearAuthState());
navigate("/login");
return Promise.reject(error);
}
if (this.isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers["Authorization"] = `Bearer ${token}`;
return this.axiosInstance(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
// Call refresh endpoint
const response = await this.axiosInstance.post("/auth/refresh");
const newToken = response.data.data.token;
// Update the auth token in store
dispatch(setAuthToken(newToken));
// Process queued requests
processQueue(null, newToken);
// Retry original request with new token
originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
return this.axiosInstance(originalRequest);
} catch (refreshError) {
// Refresh failed, clear auth and redirect to login
processQueue(refreshError, null);
dispatch(clearAuthState());
navigate("/login");
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
// Handle 401 - authentication failed (invalid/expired refresh token or no token)
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
navigate("/login");
-1
View File
@@ -450,7 +450,6 @@ const infrastructureMonitorValidation = joi.object({
}),
notifications: joi.array().items(joi.string()),
selectedDisks: joi.array().items(joi.string()).optional(),
enableProxmoxContainers: joi.boolean(),
});
const notificationValidation = joi.object({
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.35395 21C10.0591 21.6224 10.9853 22 11.9998 22C13.0142 22 13.9405 21.6224 14.6456 21M17.9998 8C17.9998 6.4087 17.3676 4.88258 16.2424 3.75736C15.1172 2.63214 13.5911 2 11.9998 2C10.4085 2 8.88235 2.63214 7.75713 3.75736C6.63192 4.88258 5.99977 6.4087 5.99977 8C5.99977 11.0902 5.22024 13.206 4.34944 14.6054C3.6149 15.7859 3.24763 16.3761 3.2611 16.5408C3.27601 16.7231 3.31463 16.7926 3.46155 16.9016C3.59423 17 4.19237 17 5.38863 17H18.6109C19.8072 17 20.4053 17 20.538 16.9016C20.6849 16.7926 20.7235 16.7231 20.7384 16.5408C20.7519 16.3761 20.3846 15.7859 19.6501 14.6054C18.7793 13.206 17.9998 11.0902 17.9998 8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 20V13M12 20V10M4 20L4 16M13.4067 5.0275L18.5751 6.96567M10.7988 5.40092L5.20023 9.59983M21.0607 6.43934C21.6464 7.02513 21.6464 7.97487 21.0607 8.56066C20.4749 9.14645 19.5251 9.14645 18.9393 8.56066C18.3536 7.97487 18.3536 7.02513 18.9393 6.43934C19.5251 5.85355 20.4749 5.85355 21.0607 6.43934ZM5.06066 9.43934C5.64645 10.0251 5.64645 10.9749 5.06066 11.5607C4.47487 12.1464 3.52513 12.1464 2.93934 11.5607C2.35355 10.9749 2.35355 10.0251 2.93934 9.43934C3.52513 8.85355 4.47487 8.85355 5.06066 9.43934ZM13.0607 3.43934C13.6464 4.02513 13.6464 4.97487 13.0607 5.56066C12.4749 6.14645 11.5251 6.14645 10.9393 5.56066C10.3536 4.97487 10.3536 4.02513 10.9393 3.43934C11.5251 2.85355 12.4749 2.85355 13.0607 3.43934Z" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 915 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#1570EF"/>
<path d="M4.43466 24V9.45455H10.0028C11.054 9.45455 11.9276 9.62026 12.6236 9.9517C13.3243 10.2784 13.8475 10.7259 14.1932 11.294C14.5436 11.8622 14.7188 12.5062 14.7188 13.2259C14.7188 13.8177 14.6051 14.3243 14.3778 14.7457C14.1506 15.1624 13.8452 15.5009 13.4616 15.7614C13.0781 16.0218 12.6496 16.2088 12.1761 16.3224V16.4645C12.6922 16.4929 13.187 16.6515 13.6605 16.9403C14.1387 17.2244 14.5294 17.6269 14.8324 18.1477C15.1354 18.6686 15.2869 19.2983 15.2869 20.0369C15.2869 20.7898 15.1046 21.4669 14.7401 22.0682C14.3755 22.6648 13.8262 23.1359 13.0923 23.4815C12.3584 23.8272 11.4351 24 10.3224 24H4.43466ZM7.0696 21.7983H9.90341C10.8598 21.7983 11.5488 21.616 11.9702 21.2514C12.3963 20.8821 12.6094 20.4086 12.6094 19.831C12.6094 19.4001 12.5028 19.0118 12.2898 18.6662C12.0767 18.3158 11.7737 18.0412 11.3807 17.8423C10.9877 17.6387 10.5189 17.5369 9.97443 17.5369H7.0696V21.7983ZM7.0696 15.6406H9.67614C10.1307 15.6406 10.5402 15.5578 10.9048 15.392C11.2694 15.2216 11.5559 14.9825 11.7642 14.6747C11.9773 14.3622 12.0838 13.9929 12.0838 13.5668C12.0838 13.0033 11.8849 12.5393 11.4872 12.1747C11.0942 11.8101 10.5095 11.6278 9.73295 11.6278H7.0696V15.6406ZM25.5482 9.45455H28.1832V18.9574C28.1832 19.9991 27.937 20.9152 27.4445 21.706C26.9568 22.4967 26.2703 23.1146 25.3849 23.5597C24.4995 24 23.4649 24.2202 22.2812 24.2202C21.0927 24.2202 20.0558 24 19.1704 23.5597C18.285 23.1146 17.5984 22.4967 17.1107 21.706C16.623 20.9152 16.3792 19.9991 16.3792 18.9574V9.45455H19.0141V18.7372C19.0141 19.3433 19.1467 19.883 19.4119 20.3565C19.6817 20.83 20.0605 21.2017 20.5482 21.4716C21.0359 21.7367 21.6136 21.8693 22.2812 21.8693C22.9488 21.8693 23.5264 21.7367 24.0141 21.4716C24.5066 21.2017 24.8853 20.83 25.1505 20.3565C25.4156 19.883 25.5482 19.3433 25.5482 18.7372V9.45455Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 10.5V7.8C19 6.11984 19 5.27976 18.673 4.63803C18.3854 4.07354 17.9265 3.6146 17.362 3.32698C16.7202 3 15.8802 3 14.2 3H5.8C4.11984 3 3.27976 3 2.63803 3.32698C2.07354 3.6146 1.6146 4.07354 1.32698 4.63803C1 5.27976 1 6.11984 1 7.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H10.5M19 9H1M14 1V5M6 1V5M16 20V14M13 17H19" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 8.33342H1.5M12.3333 1.66675V5.00008M5.66667 1.66675V5.00008M5.5 18.3334H12.5C13.9001 18.3334 14.6002 18.3334 15.135 18.0609C15.6054 17.8212 15.9878 17.4388 16.2275 16.9684C16.5 16.4336 16.5 15.7335 16.5 14.3334V7.33342C16.5 5.93328 16.5 5.23322 16.2275 4.69844C15.9878 4.22803 15.6054 3.84558 15.135 3.6059C14.6002 3.33341 13.9001 3.33341 12.5 3.33341H5.5C4.09987 3.33341 3.3998 3.33341 2.86502 3.6059C2.39462 3.84558 2.01217 4.22803 1.77248 4.69844C1.5 5.23322 1.5 5.93328 1.5 7.33341V14.3334C1.5 15.7335 1.5 16.4336 1.77248 16.9684C2.01217 17.4388 2.39462 17.8212 2.86502 18.0609C3.3998 18.3334 4.09987 18.3334 5.5 18.3334Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 830 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 20H5C3.89543 20 3 19.1046 3 18V4C3 2.89543 3.89543 2 5 2H19C20.1046 2 21 2.89543 21 4V18C21 19.1046 20.1046 20 19 20H17.5M12 19C13.6569 19 15 17.6569 15 16C15 14.3431 13.6569 13 12 13C10.3431 13 9 14.3431 9 16C9 17.6569 10.3431 19 12 19ZM12 19L12.0214 18.9998L8.82867 22.1926L6.00024 19.3641L9.01965 16.3447M12 19L15.1928 22.1926L18.0212 19.3641L15.0018 16.3447M9 6H15M7 9.5H17" stroke="#475466" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 578 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 17L22 12L17 7M7 7L2 12L7 17M14 3L10 21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 12L10.5 15L16.5 9M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.24935 10.0001L8.74935 12.5001L13.7493 7.50008M18.3327 10.0001C18.3327 14.6025 14.6017 18.3334 9.99935 18.3334C5.39698 18.3334 1.66602 14.6025 1.66602 10.0001C1.66602 5.39771 5.39698 1.66675 9.99935 1.66675C14.6017 1.66675 18.3327 5.39771 18.3327 10.0001Z" stroke="#1570EF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10Z" fill="#D0D5DD"/>
<path d="M6.25 10L8.75 12.5L13.75 7.5" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 363 B

Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more