Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79aca01bbf | ||
|
|
a87ca648dc |
No files matched your search
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |