Merge branch 'develop' into incidents-feat-modal
This commit is contained in:
commit
eb8c9bea76
311 files changed
+9539
-6966
No files matched your search
@@ -0,0 +1,145 @@
|
||||
# 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
|
||||
Generated
+25
-42
@@ -12,7 +12,6 @@
|
||||
"@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",
|
||||
@@ -25,6 +24,7 @@
|
||||
"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,32 +1189,6 @@
|
||||
"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",
|
||||
@@ -2050,9 +2024,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -5297,9 +5271,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -5464,6 +5438,15 @@
|
||||
"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",
|
||||
@@ -6129,12 +6112,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0"
|
||||
"@remix-run/router": "1.23.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -6144,13 +6127,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"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==",
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0",
|
||||
"react-router": "6.30.1"
|
||||
"@remix-run/router": "1.23.2",
|
||||
"react-router": "6.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
+1
-1
@@ -17,7 +17,6 @@
|
||||
"@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",
|
||||
@@ -30,6 +29,7 @@
|
||||
"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",
|
||||
|
||||
@@ -16,7 +16,7 @@ const langMap = {
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
const { language = "en" } = useSelector((state) => state.ui);
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event) => {
|
||||
const newLang = event.target.value;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import Dialog from "../Dialog/index.jsx";
|
||||
|
||||
// Utils
|
||||
@@ -32,7 +32,6 @@ const ActionsMenu = ({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let monitor = { id: actions.id };
|
||||
console.log(monitor);
|
||||
await deleteMonitor({ monitor });
|
||||
updateRowCallback();
|
||||
};
|
||||
@@ -84,7 +83,10 @@ const ActionsMenu = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
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 Icon from "../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -13,9 +10,24 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
info: (
|
||||
<Icon
|
||||
name="Info"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<Icon
|
||||
name="AlertCircle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
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 Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronsLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ArrowLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
|
||||
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronsRight"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
<Icon
|
||||
name="ChevronRight"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "./check.css";
|
||||
import PropTypes from "prop-types";
|
||||
import CheckGrey from "../../../assets/icons/check.svg?react";
|
||||
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
|
||||
import Icon from "../Icon";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
||||
alignItems="center"
|
||||
>
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
<Icon
|
||||
name="CheckCircle"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
lineHeight={0}
|
||||
sx={{
|
||||
"& svg > path": { fill: colors[variant] },
|
||||
}}
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
</Box>
|
||||
<Icon
|
||||
name="Check"
|
||||
size={16}
|
||||
color={colors[variant]}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
component="span"
|
||||
|
||||
@@ -8,8 +8,7 @@ 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 MenuIcon from "@mui/icons-material/Menu";
|
||||
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
|
||||
import Icon from "../Icon";
|
||||
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -153,7 +152,11 @@ const AppAppBar = () => {
|
||||
aria-label="Menu button"
|
||||
onClick={toggleDrawer(true)}
|
||||
>
|
||||
<MenuIcon sx={{ color: theme.palette.text.primary }} />
|
||||
<Icon
|
||||
name="Menu"
|
||||
size={24}
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
</IconButton>
|
||||
<Drawer
|
||||
anchor="top"
|
||||
@@ -176,7 +179,11 @@ const AppAppBar = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer(false)}>
|
||||
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
|
||||
<Icon
|
||||
name="X"
|
||||
size={24}
|
||||
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 AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
||||
import Icon from "../Icon";
|
||||
|
||||
/**
|
||||
* A reusable filter header component that displays a dropdown menu with selectable options.
|
||||
@@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
|
||||
<Select
|
||||
multiple={multiple}
|
||||
IconComponent={(props) => (
|
||||
<AddCircleOutlineIcon
|
||||
<Icon
|
||||
{...props}
|
||||
sx={{ fontSize: "medium" }}
|
||||
name="PlusCircle"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
displayEmpty
|
||||
|
||||
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
||||
direction="row"
|
||||
position="relative"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(5)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
||||
<>
|
||||
<Dot />
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
color: percentageColor,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
const I18nLoader = () => {
|
||||
const language = useSelector((state) => state.ui.language);
|
||||
const language = useSelector((state) => state.ui.language ?? "en");
|
||||
|
||||
useEffect(() => {
|
||||
if (language && i18n.language !== language) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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",
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
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,8 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
/**
|
||||
* Checkbox Component
|
||||
@@ -65,8 +64,8 @@ const Checkbox = ({
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
icon={<Icon name="Square" />}
|
||||
checkedIcon={<Icon name="CheckSquare" />}
|
||||
inputProps={{
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import Icon from "../../Icon";
|
||||
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";
|
||||
@@ -176,7 +175,10 @@ const ImageUpload = ({
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
<Icon
|
||||
name="Upload"
|
||||
size={24}
|
||||
/>
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
@@ -203,7 +205,12 @@ const ImageUpload = ({
|
||||
</Box>
|
||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="Image"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
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 RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,12 @@ const Radio = ({
|
||||
<MUIRadio
|
||||
id={id}
|
||||
size={size}
|
||||
checkedIcon={<RadioChecked />}
|
||||
checkedIcon={
|
||||
<Icon
|
||||
name="CircleDot"
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
width: 16,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../../assets/icons/search.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
@@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
<Box height={16}>
|
||||
<Icon
|
||||
name="Search"
|
||||
size={16}
|
||||
color="primary.contrastTextTertiary"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</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 KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import Icon from "../../Icon";
|
||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||
|
||||
import "./index.css";
|
||||
@@ -98,7 +98,13 @@ const Select = ({
|
||||
error={error}
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
IconComponent={(props) => (
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size={20}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
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";
|
||||
import Icon from "../../../Icon";
|
||||
|
||||
export const HttpAdornment = ({ https }) => {
|
||||
const theme = useTheme();
|
||||
@@ -56,7 +53,17 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
||||
{fieldType === "password" ? (
|
||||
<Icon
|
||||
name="EyeOff"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="Eye"
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
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 */
|
||||
@@ -12,15 +14,7 @@
|
||||
}
|
||||
} */
|
||||
|
||||
/* .home-layout aside {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
} */
|
||||
|
||||
.home-layout > div {
|
||||
.home-layout > .home-content-wrapper {
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
}
|
||||
@@ -30,4 +24,6 @@
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -1,17 +1,27 @@
|
||||
import Sidebar from "../../Sidebar/index.jsx";
|
||||
import { Outlet } from "react-router";
|
||||
import { Stack } from "@mui/material";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const HomeLayout = () => {
|
||||
const { width, transition } = useSidebar();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="home-layout"
|
||||
flexDirection="row"
|
||||
gap={14}
|
||||
>
|
||||
<Sidebar />
|
||||
{/* Spacer for fixed sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
width,
|
||||
flexShrink: 0,
|
||||
transition,
|
||||
}}
|
||||
/>
|
||||
<Stack className="home-content-wrapper">
|
||||
<Outlet />
|
||||
</Stack>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import Icon from "../Icon";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Grow from "@mui/material/Grow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
@@ -70,7 +70,10 @@ const MonitorActions = ({ isLoading }) => {
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
|
||||
@@ -3,10 +3,7 @@ import Status from "./status.jsx";
|
||||
import Skeleton from "./skeleton.jsx";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Tooltip } from "@mui/material";
|
||||
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";
|
||||
import Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
@@ -66,7 +63,7 @@ const MonitorDetailsControlHeader = ({
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
key={monitor?._id}
|
||||
key={monitor?.id}
|
||||
placement="bottom"
|
||||
title={tooltipTitle}
|
||||
>
|
||||
@@ -75,10 +72,15 @@ const MonitorDetailsControlHeader = ({
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isSending}
|
||||
startIcon={<EmailIcon />}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Mail"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
disabled={isTestNotificationsDisabled}
|
||||
onClick={() => {
|
||||
testAllNotifications({ monitorId: monitor?._id });
|
||||
testAllNotifications({ monitorId: monitor?.id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
@@ -92,7 +94,7 @@ const MonitorDetailsControlHeader = ({
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={(e) => {
|
||||
navigate(`/incidents/${monitor?._id}`);
|
||||
navigate(`/incidents/${monitor?.id}`);
|
||||
}}
|
||||
>
|
||||
{t("menu.incidents")}
|
||||
@@ -103,11 +105,21 @@ const MonitorDetailsControlHeader = ({
|
||||
color="secondary"
|
||||
loading={isPausing}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
monitor?.isActive ? (
|
||||
<Icon
|
||||
name="Pause"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name="Play"
|
||||
size={18}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
pauseMonitor({
|
||||
monitorId: monitor?._id,
|
||||
monitorId: monitor?.id,
|
||||
triggerUpdate,
|
||||
});
|
||||
}}
|
||||
@@ -119,8 +131,13 @@ const MonitorDetailsControlHeader = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<SettingsOutlinedIcon />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import Icon from "../Icon";
|
||||
import Search from "../Inputs/Search/index.jsx";
|
||||
|
||||
// Utils
|
||||
@@ -102,11 +102,13 @@ const NotificationConfig = ({
|
||||
>
|
||||
{notification.notificationName}
|
||||
</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
<Icon
|
||||
name="Trash2"
|
||||
size={20}
|
||||
onClick={() => {
|
||||
handleDelete(notification._id);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
{index < selectedNotifications.length - 1 && <Divider />}
|
||||
</Stack>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import Icon from "../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -51,7 +50,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
alignItems={error ? "center" : "flex-start"}
|
||||
>
|
||||
{error ? (
|
||||
<ErrorOutlineOutlinedIcon />
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={20}
|
||||
/>
|
||||
) : icon ? (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -127,10 +129,9 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
}
|
||||
}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from "react";
|
||||
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
||||
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
|
||||
import CheckCircle from "@mui/icons-material/CheckCircle";
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CustomStepIcon = (props) => {
|
||||
const { completed, active } = props;
|
||||
return completed ? (
|
||||
<CheckCircle color="accent" />
|
||||
<Icon
|
||||
name="CheckCircle2"
|
||||
size={24}
|
||||
color="accent.main"
|
||||
/>
|
||||
) : (
|
||||
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
|
||||
<Icon
|
||||
name="Circle"
|
||||
size={24}
|
||||
color={active ? "accent.main" : "text.disabled"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import html2canvas from "html2canvas";
|
||||
import ShareIcon from "@mui/icons-material/Share";
|
||||
import Icon from "../Icon";
|
||||
import { Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -55,7 +55,13 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
|
||||
startIcon={
|
||||
<Icon
|
||||
name="Share2"
|
||||
size={18}
|
||||
color="success.main"
|
||||
/>
|
||||
}
|
||||
color="success"
|
||||
onClick={captureAndShare}
|
||||
>
|
||||
|
||||
@@ -7,8 +7,7 @@ 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 DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
|
||||
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -81,6 +80,10 @@ 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}
|
||||
@@ -160,19 +163,14 @@ 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)}
|
||||
>
|
||||
<DotsVertical />
|
||||
<Icon
|
||||
name="MoreVertical"
|
||||
size={22}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
@@ -256,12 +254,13 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
<Icon
|
||||
name="LogOut"
|
||||
size={20}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -52,12 +52,11 @@ 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,28 +8,13 @@ import NavItem from "./components/navItem.jsx";
|
||||
import AuthFooter from "./components/authFooter.jsx";
|
||||
|
||||
import StarPrompt from "../StarPrompt/index.jsx";
|
||||
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";
|
||||
import Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||
|
||||
const URL_MAP = {
|
||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||
@@ -39,52 +24,48 @@ const URL_MAP = {
|
||||
};
|
||||
|
||||
const getMenu = (t) => [
|
||||
{ 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.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.notifications"),
|
||||
path: "notifications",
|
||||
icon: <Notifications />,
|
||||
icon: <Icon name="Bell" />,
|
||||
},
|
||||
{ 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.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.settings"),
|
||||
icon: <Settings />,
|
||||
icon: <Icon name="Settings" />,
|
||||
path: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
const getOtherMenuItems = (t) => [
|
||||
{ name: t("menu.support"), path: "support", icon: <Support /> },
|
||||
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
|
||||
{
|
||||
name: t("menu.discussions"),
|
||||
path: "discussions",
|
||||
icon: <Discussions />,
|
||||
icon: <Icon name="MessageCircle" />,
|
||||
},
|
||||
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
|
||||
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
|
||||
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
|
||||
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
|
||||
];
|
||||
|
||||
const getAccountMenuItems = (t) => [
|
||||
{ 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 /> },
|
||||
{ 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" /> },
|
||||
];
|
||||
|
||||
const Sidebar = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
// Redux state
|
||||
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||
const { collapsed, width, transition } = useSidebar();
|
||||
|
||||
const menu = getMenu(t);
|
||||
const otherMenuItems = getOtherMenuItems(t);
|
||||
@@ -93,20 +74,19 @@ const Sidebar = () => {
|
||||
return (
|
||||
<Stack
|
||||
height="100vh"
|
||||
width={
|
||||
collapsed
|
||||
? "var(--env-var-side-bar-collapsed-width)"
|
||||
: "var(--env-var-side-bar-width)"
|
||||
}
|
||||
width={width}
|
||||
component="aside"
|
||||
position="sticky"
|
||||
position="fixed"
|
||||
top={0}
|
||||
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
left={0}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||
transition,
|
||||
backgroundColor: "#000000",
|
||||
borderRight: "1px solid #344054",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Icon from "../Icon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -67,7 +67,10 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "1.25rem" }} />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</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 SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||
import Icon from "../../Icon";
|
||||
|
||||
Pagination.propTypes = {
|
||||
paginationLabel: PropTypes.string, // Label for the pagination.
|
||||
@@ -100,7 +100,12 @@ function Pagination({
|
||||
},
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
IconComponent: () => (
|
||||
<Icon
|
||||
name="ChevronsUpDown"
|
||||
size={16}
|
||||
/>
|
||||
),
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
|
||||
@@ -3,19 +3,31 @@ import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ToastBody from "./body.jsx";
|
||||
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 Icon from "../Icon";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
info: (
|
||||
<Icon
|
||||
name="Info"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<Icon
|
||||
name="AlertCircle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
@@ -50,7 +62,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
)}
|
||||
{title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -63,7 +78,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
<ToastBody body={body} />
|
||||
{!title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -10,21 +10,24 @@ const initialState = {
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const register = createAsyncThunk("auth/register", async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.registerUser(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
export const register = createAsyncThunk(
|
||||
"auth/register",
|
||||
async ({ user, token }, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.registerUser(user, token);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
try {
|
||||
|
||||
@@ -58,6 +58,9 @@ const uiSlice = createSlice({
|
||||
state.showURL = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
if (!state.greeting) {
|
||||
state.greeting = { index: 0, lastUpdate: null };
|
||||
}
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
},
|
||||
|
||||
@@ -185,36 +185,9 @@ const useResolveIncident = () => {
|
||||
return [resolveIncident, isLoading];
|
||||
};
|
||||
|
||||
const useAcknowledgeChecks = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const acknowledge = async (setUpdateTrigger, monitorId = null) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (monitorId) {
|
||||
await networkService.updateMonitorChecksStatus({ monitorId, ack: true });
|
||||
} else {
|
||||
await networkService.updateAllChecksStatus({ ack: true });
|
||||
}
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
} catch (error) {
|
||||
const toastMessage = monitorId
|
||||
? t("checkHooks.failureResolveMonitor")
|
||||
: t("checkHooks.failureResolveAll");
|
||||
createToast({ body: toastMessage });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { acknowledge, isLoading };
|
||||
};
|
||||
|
||||
export {
|
||||
useFetchChecksByMonitor,
|
||||
useFetchChecksTeam,
|
||||
useFetchChecksSummaryByTeamId,
|
||||
useResolveIncident,
|
||||
useAcknowledgeChecks,
|
||||
};
|
||||
@@ -50,6 +50,7 @@ export const useFetchMonitorsWithChecks = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [count, setCount] = useState(undefined);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [summary, setSummary] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
@@ -68,10 +69,11 @@ export const useFetchMonitorsWithChecks = ({
|
||||
order,
|
||||
});
|
||||
|
||||
const { count, monitors } = res?.data?.data ?? {};
|
||||
const { count, monitors, summary } = res?.data?.data ?? {};
|
||||
const mappedMonitors = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
setSummary(summary);
|
||||
setMonitors(mappedMonitors);
|
||||
setCount(count || 0);
|
||||
} catch (error) {
|
||||
@@ -97,7 +99,7 @@ export const useFetchMonitorsWithChecks = ({
|
||||
types,
|
||||
monitorUpdateTrigger,
|
||||
]);
|
||||
return [monitors, count, isLoading, networkError];
|
||||
return [summary, monitors, count, isLoading, networkError];
|
||||
};
|
||||
|
||||
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
|
||||
@@ -241,6 +243,7 @@ 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 () => {
|
||||
@@ -252,7 +255,8 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
|
||||
monitorId: monitorId,
|
||||
dateRange: dateRange,
|
||||
});
|
||||
setMonitor(response.data.data);
|
||||
setMonitor(response.data.data.monitor);
|
||||
setMonitorStats(response.data.data.monitorStats);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
@@ -261,7 +265,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, dateRange, updateTrigger]);
|
||||
return [monitor, isLoading, networkError];
|
||||
return [monitor, monitorStats, isLoading, networkError];
|
||||
};
|
||||
|
||||
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||
@@ -340,7 +344,6 @@ export const useDeleteMonitor = () => {
|
||||
const deleteMonitor = async ({ monitor, redirect }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log(monitor);
|
||||
await networkService.deleteMonitorById({ monitorId: monitor.id });
|
||||
createToast({ body: "Monitor deleted successfully!" });
|
||||
if (redirect) {
|
||||
@@ -385,7 +388,7 @@ export const useUpdateMonitor = () => {
|
||||
}),
|
||||
};
|
||||
await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
updatedFields,
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const useSaveSettings = ({
|
||||
setEmailPasswordHasBeenReset(false);
|
||||
}
|
||||
setSettingsData(settingsResponse.data.data);
|
||||
|
||||
createToast({ body: t("settingsSuccessSaved") });
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToSave") });
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// CSS variable names for sidebar widths
|
||||
const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)";
|
||||
const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)";
|
||||
|
||||
// Transition timing for sidebar width changes
|
||||
const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)";
|
||||
|
||||
/**
|
||||
* Hook to get sidebar state and computed width
|
||||
* Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout
|
||||
*
|
||||
* @returns {Object} Sidebar state and styles
|
||||
* @returns {boolean} collapsed - Whether the sidebar is collapsed
|
||||
* @returns {string} width - CSS width value based on collapsed state
|
||||
* @returns {string} transition - CSS transition for width changes
|
||||
*/
|
||||
export const useSidebar = () => {
|
||||
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
|
||||
|
||||
return {
|
||||
collapsed,
|
||||
width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR,
|
||||
transition: SIDEBAR_TRANSITION,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSidebar;
|
||||
@@ -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 ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Proptypes from "prop-types";
|
||||
|
||||
@@ -24,7 +24,13 @@ const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
endIcon={<ArrowDropDownIcon sx={{ color: theme.palette.secondary.light }} />}
|
||||
endIcon={
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size={20}
|
||||
color="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 DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ROLES } from "../../../../Utils/roleUtils.js";
|
||||
@@ -21,11 +21,13 @@ const RoleTable = ({ roles, handleDeleteRole }) => {
|
||||
render: (row) => {
|
||||
if (row === ROLES.SUPERADMIN) return null;
|
||||
return (
|
||||
<DeleteOutlineRoundedIcon
|
||||
<Icon
|
||||
name="Trash2"
|
||||
size={20}
|
||||
onClick={() => {
|
||||
handleDeleteRole(row);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -104,7 +104,7 @@ const TeamPanel = () => {
|
||||
|
||||
team = team.map((member) => ({
|
||||
...member,
|
||||
id: member._id,
|
||||
id: member.id,
|
||||
role: member.role.map((role) => ROLE_MAP[role]).join(","),
|
||||
}));
|
||||
setData(team);
|
||||
@@ -268,6 +268,7 @@ const TeamPanel = () => {
|
||||
cursor: isSuperAdmin ? "pointer" : "default",
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
console.log(row);
|
||||
if (isSuperAdmin) {
|
||||
navigate(`/account/team/${row.id}`);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<EmailIcon alt={t("auth.forgotPassword.imageAlts.email")} />
|
||||
<Icon name="Mail" />
|
||||
</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)}
|
||||
>
|
||||
<Key alt={t("auth.forgotPassword.imageAlts.passwordKey")} />
|
||||
<Icon name="Key" />
|
||||
</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)}
|
||||
>
|
||||
<ConfirmIcon alt={t("auth.forgotPassword.imageAlts.passwordConfirm")} />
|
||||
<Icon name="CheckCircle" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||
|
||||
@@ -176,7 +176,7 @@ const Register = ({ superAdminExists }) => {
|
||||
|
||||
delete toSubmit.confirm;
|
||||
|
||||
const action = await dispatch(register(toSubmit));
|
||||
const action = await dispatch(register({ user: toSubmit, token }));
|
||||
if (action.payload.success) {
|
||||
navigate("/uptime");
|
||||
createToast({
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<LockIcon alt={t("auth.forgotPassword.imageAlts.lock")} />
|
||||
<Icon name="Lock" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
//Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
|
||||
//Utils
|
||||
import { formatDateWithTz } from "@/Utils/timeUtils.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js";
|
||||
import { Button, Typography, useTheme } from "@mui/material";
|
||||
import { lighten } from "@mui/material/styles";
|
||||
|
||||
const GetTooltip = (row) => {
|
||||
const theme = useTheme();
|
||||
const phases = row?.timings?.phases;
|
||||
|
||||
const phaseKeyFormattingMap = {
|
||||
firstByte: "first byte",
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
py={theme.spacing(2)}
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Status code: ${row?.statusCode}`}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Response time: ${row?.responseTime} ms`}</Typography>
|
||||
{phases && (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Request timing: `}</Typography>
|
||||
<Table
|
||||
size="small"
|
||||
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
|
||||
>
|
||||
<TableBody>
|
||||
{Object.keys(phases)?.map((phaseKey) => (
|
||||
<TableRow key={phaseKey}>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="success"
|
||||
>
|
||||
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
variant="body2"
|
||||
>{`${phases[phaseKey]} ms`}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const IncidentTable = ({
|
||||
isLoading,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
dateRange,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
//Redux state
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
//Local state
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const selectedMonitorDetails = monitors?.[selectedMonitor];
|
||||
const selectedMonitorType = selectedMonitorDetails?.type;
|
||||
|
||||
//Hooks
|
||||
const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
|
||||
type: selectedMonitorType,
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor !== "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
|
||||
useFetchChecksTeam({
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor === "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
|
||||
const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
|
||||
isLoading = isLoadingTeam || isLoadingMonitor;
|
||||
const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
//Handlers
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(event.target.value);
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => {
|
||||
return monitors?.[row.metadata?.monitorId]?.name || "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "up" : "down";
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dateTime",
|
||||
content: t("incidentsTableDateTime"),
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("incidentsTableStatusCode"),
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{ id: "message", content: t("incidentsTableMessage"), render: (row) => row.message },
|
||||
];
|
||||
|
||||
if (isLoading) return <TableSkeleton />;
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && typeof checksCount === "undefined") {
|
||||
return <GenericFallback>{t("incidentsTableNoIncidents")}</GenericFallback>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
config={{ tooltipContent: GetTooltip }}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel")}
|
||||
itemCount={checksCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.object,
|
||||
selectedMonitor: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
updateTrigger: PropTypes.bool,
|
||||
setUpdateTrigger: PropTypes.func,
|
||||
};
|
||||
export default IncidentTable;
|
||||
+9
-7
@@ -5,6 +5,7 @@ import PropTypes from "prop-types";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const OptionsHeader = ({
|
||||
@@ -14,27 +15,28 @@ const OptionsHeader = ({
|
||||
monitors,
|
||||
filter = "all",
|
||||
setFilter,
|
||||
dateRange = "all",
|
||||
dateRange = "hour",
|
||||
setDateRange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
console.log(monitors);
|
||||
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
|
||||
|
||||
const filterOptions = [
|
||||
{ _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") },
|
||||
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
|
||||
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
|
||||
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
|
||||
{ id: "all", name: t("incidentsOptionsHeaderFilterAll") },
|
||||
{ id: "down", name: t("incidentsOptionsHeaderFilterDown") },
|
||||
{ id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") },
|
||||
{ id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") },
|
||||
];
|
||||
|
||||
// The stacks below which are three in number have the same style so
|
||||
const stackStyles = {
|
||||
direction: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(6),
|
||||
};
|
||||
|
||||
if (!shouldRender) return;
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
File renamed without changes.
+17
-8
@@ -2,10 +2,7 @@ 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 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";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
@@ -26,28 +23,40 @@ const StatusBox = ({ title, value, status }) => {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<CheckIcon />
|
||||
<Icon
|
||||
name="CheckCircle"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<CloseIcon />
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<WarningIcon />
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
color = theme.palette.accent.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<AlertIcon />
|
||||
<Icon
|
||||
name="Bell"
|
||||
size={20}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,100 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import IncidentTable from "./Components/IncidentTable/index.jsx";
|
||||
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
|
||||
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
|
||||
import { Box, Button } from "@mui/material";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
//Constants
|
||||
const Checks = () => {
|
||||
// Redux state
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }];
|
||||
|
||||
// Local state
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [filter, setFilter] = useState(undefined);
|
||||
const [dateRange, setDateRange] = useState(undefined);
|
||||
const [monitorLookup, setMonitorLookup] = useState(undefined);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
//Hooks
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
|
||||
const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({
|
||||
updateTrigger,
|
||||
});
|
||||
const { monitorId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (monitorId) {
|
||||
setSelectedMonitor(monitorId);
|
||||
}
|
||||
}, [monitorId]);
|
||||
|
||||
useEffect(() => {
|
||||
const monitorLookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor.id] = {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitorLookup(monitorLookup);
|
||||
}, [monitors]);
|
||||
|
||||
if (networkError || networkErrorSummary) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
<StatusBoxes
|
||||
isLoading={isLoadingSummary}
|
||||
summary={summary}
|
||||
/>
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoading}
|
||||
monitors={monitorLookup}
|
||||
selectedMonitor={selectedMonitor}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<IncidentTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitorLookup ? monitorLookup : {}}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
dateRange={dateRange}
|
||||
updateTrigger={updateTrigger}
|
||||
setUpdateTrigger={setUpdateTrigger}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checks;
|
||||
+24
-35
@@ -3,8 +3,7 @@ import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
||||
import Incidents from "@/assets/icons/incidents.svg?react";
|
||||
import CheckIcon from "@/assets/icons/check-icon.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
@@ -43,39 +42,39 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
|
||||
|
||||
if (!totalCount || totalCount === 0) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding={theme.spacing(10)}
|
||||
gap={theme.spacing(4)}
|
||||
padding={theme.spacing(6)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.success.main,
|
||||
"& svg": {
|
||||
width: 60,
|
||||
height: 60,
|
||||
|
||||
width: 24,
|
||||
height: 24,
|
||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
||||
},
|
||||
mb: theme.spacing(2),
|
||||
mb: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
<Icon
|
||||
name="CheckCircle"
|
||||
size={60}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
textAlign: "center",
|
||||
color: theme.palette.success.lowContrast,
|
||||
letterSpacing: theme.spacing(0.4),
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.allSystemsAreOperational")}
|
||||
@@ -86,50 +85,40 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard isHighPriority={true}>
|
||||
<SummaryCard title={t("incidentsPage.incidentsActivePanelTitle")}>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={theme.spacing(4)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
color: theme.palette.error.lowContrast,
|
||||
padding: theme.spacing(2),
|
||||
"& svg": {
|
||||
width: 60,
|
||||
height: 60,
|
||||
|
||||
width: 24,
|
||||
height: 24,
|
||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Incidents />
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={60}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontSize: `calc(${theme.typography.h1.fontSize} * 2.5)`,
|
||||
fontWeight: 700,
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,406 @@
|
||||
// Components
|
||||
import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material";
|
||||
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import SummaryCard from "@/Pages/Incidents2/Components/SummaryCard";
|
||||
import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/timeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// Hooks
|
||||
import useFetchIncidents from "@/Pages/Incidents2/hooks/useFetchIncidents";
|
||||
import useGetIncidentsDuration from "@/Pages/Incidents2/hooks/useGetIncidentsDuration";
|
||||
|
||||
const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { fetchIncidentById, resolveIncident } = useFetchIncidents();
|
||||
const [incident, setIncident] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const isActive = incident?.status === true;
|
||||
const duration = useGetIncidentsDuration(incident || null, isActive);
|
||||
|
||||
const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main;
|
||||
const toCapitalLetter = (text) =>
|
||||
typeof text === "string" && text.length > 0
|
||||
? text.charAt(0).toUpperCase() + text.slice(1)
|
||||
: text;
|
||||
|
||||
const KeyValueRow = ({ label, value, sx = {} }) => (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
alignItems="flex-start"
|
||||
sx={{ ...sx }}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={4}
|
||||
>
|
||||
<Typography variant="body1">{label}</Typography>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
textAlign="right"
|
||||
>
|
||||
{value ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadIncident = async () => {
|
||||
if (!incidentId || !open) {
|
||||
setIncident(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const incidentData = await fetchIncidentById(incidentId);
|
||||
setIncident(incidentData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching incident:", error);
|
||||
setIncident(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadIncident();
|
||||
}, [incidentId, open, fetchIncidentById]);
|
||||
|
||||
const openResolveDialog = () => {
|
||||
setIsResolveDialogOpen(true);
|
||||
};
|
||||
|
||||
const closeResolveDialog = () => {
|
||||
setIsResolveDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleAfterResolve = async () => {
|
||||
if (!incidentId) return;
|
||||
const updatedIncident = await fetchIncidentById(incidentId);
|
||||
if (updatedIncident) {
|
||||
setIncident(updatedIncident);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<GenericFallback
|
||||
isLoading={true}
|
||||
message={t("incidentsPage.loadingIncidentDetails")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!incident) {
|
||||
return (
|
||||
<GenericFallback
|
||||
isLoading={false}
|
||||
message={t("incidentsPage.incidentNotFound")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={12}
|
||||
>
|
||||
<SummaryCard title={t("incidentsPage.overview")}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ mb: theme.spacing(3) }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
bgcolor: statusColor,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: statusColor,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.4,
|
||||
}}
|
||||
>
|
||||
{isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="start"
|
||||
gap={theme.spacing(3)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={2}
|
||||
sx={{ alignItems: "baseline" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ minWidth: 70 }}
|
||||
>
|
||||
{t("incidentsPage.incidentItemMonitor")}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{incident?.monitorId?.name || t("incidentsPage.unknownMonitor")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={2}
|
||||
sx={{ alignItems: "baseline" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
fontWeight={500}
|
||||
sx={{ minWidth: 70 }}
|
||||
>
|
||||
{t("incidentsPage.URL")}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ flex: 1, wordBreak: "break-word", fontFamily: "monospace" }}
|
||||
>
|
||||
{incident?.monitorId?.url || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={12}
|
||||
>
|
||||
<SummaryCard title={t("incidentsPage.incidentAnalysis")}>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
alignItems="stretch"
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
>
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontWeight={700}
|
||||
>
|
||||
{t("incidentsPage.timeline")}
|
||||
</Typography>
|
||||
<Divider sx={{ mt: 2 }} />
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.startedAt")}
|
||||
value={
|
||||
formatDateWithTz(
|
||||
incident?.startTime,
|
||||
"D MMM YYYY, h:mm A",
|
||||
uiTimezone
|
||||
) || "-"
|
||||
}
|
||||
sx={{ paddingBottom: theme.spacing(3) }}
|
||||
/>
|
||||
{!isActive && (
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.endedAt")}
|
||||
value={
|
||||
formatDateWithTz(
|
||||
incident?.endTime,
|
||||
"D MMM YYYY, h:mm A",
|
||||
uiTimezone
|
||||
) || "-"
|
||||
}
|
||||
sx={{ paddingBottom: theme.spacing(3) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.downtime")}
|
||||
value={duration || "-"}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
sm="auto"
|
||||
sx={{ display: { xs: "none", sm: "flex" } }}
|
||||
>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm
|
||||
>
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ letterSpacing: 0.6, fontWeight: 700 }}
|
||||
>
|
||||
{isActive
|
||||
? t("incidentsPage.errorDetails")
|
||||
: t("incidentsPage.resolutionDetails")}
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ mt: 2 }} />
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.statusCode")}
|
||||
value={incident?.statusCode ?? "-"}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
|
||||
wordBreak: "break-word",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{incident?.message || "-"}
|
||||
</Typography>
|
||||
|
||||
{!isActive && (
|
||||
<>
|
||||
<Divider sx={{ mb: theme.spacing(2) }} />
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.resolutionMethod")}
|
||||
value={toCapitalLetter(incident?.resolutionType) || "-"}
|
||||
/>
|
||||
{incident?.resolutionType === "manual" && (
|
||||
<>
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.resolvedBy")}
|
||||
value={incident?.resolvedBy?.email || "Unknown"}
|
||||
/>
|
||||
|
||||
<KeyValueRow
|
||||
label={t("incidentsPage.comment")}
|
||||
value={
|
||||
incident?.comment?.trim()
|
||||
? incident.comment
|
||||
: t("incidentsPage.noCommentProvided")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SummaryCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericDialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
theme={theme}
|
||||
title={t("incidentsPage.incidentDetails")}
|
||||
width={{ xs: "95%", md: "45%", lg: "40%", xl: "35%" }}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(6)}
|
||||
sx={{ maxHeight: "70vh", overflowY: "auto", pr: 1 }}
|
||||
>
|
||||
{renderContent()}
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
gap={2}
|
||||
sx={{ pt: theme.spacing(3) }}
|
||||
>
|
||||
{isActive && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={openResolveDialog}
|
||||
>
|
||||
{t("incidentsPage.incidentsTableActionResolveManually")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("close")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<ResolveIncidentDialog
|
||||
open={isResolveDialogOpen}
|
||||
incidentId={incidentId}
|
||||
onClose={closeResolveDialog}
|
||||
onResolve={resolveIncident}
|
||||
onAfterResolve={handleAfterResolve}
|
||||
onResolved={onResolved}
|
||||
/>
|
||||
</Stack>
|
||||
</GenericDialog>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentDetailsModal.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
incidentId: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onResolved: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentDetailsModal;
|
||||
@@ -0,0 +1,154 @@
|
||||
// Components
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Settings from "../../../../assets/icons/settings-bold.svg?react";
|
||||
import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx";
|
||||
import useFetchIncidents from "../../hooks/useFetchIncidents";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TypeToPathMap } from "@/Utils/monitorUtils.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { fetchIncidentById } = useFetchIncidents();
|
||||
const navigate = useNavigate();
|
||||
const openMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const openResolveDialog = (e) => {
|
||||
closeMenu(e);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = (e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const isActive = incident.status === true;
|
||||
const handleGoToDetails = (e) => {
|
||||
e.stopPropagation();
|
||||
closeMenu(e);
|
||||
if (onOpenDetails) {
|
||||
onOpenDetails(incident._id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoToMonitor = async (e) => {
|
||||
e.stopPropagation();
|
||||
closeMenu(e);
|
||||
try {
|
||||
const incidentData = await fetchIncidentById(incident._id);
|
||||
console.log(incidentData);
|
||||
if (incidentData) {
|
||||
const { type, _id: monitorId } = incidentData.monitorId;
|
||||
const path = TypeToPathMap[type];
|
||||
if (path && monitorId) {
|
||||
navigate(`/${path}/${monitorId}`);
|
||||
} else {
|
||||
console.error(`Monitor type not found`);
|
||||
}
|
||||
} else {
|
||||
console.error("Monitor information not found in incident data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching incident details:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="incident actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="incident-actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closeMenu}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": {
|
||||
p: theme.spacing(2.5),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleGoToDetails}>
|
||||
{t("incidentsPage.incidentsTableActionDetails", "Details")}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleGoToMonitor}>
|
||||
{t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")}
|
||||
</MenuItem>
|
||||
{isActive && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openResolveDialog(e);
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.incidentsTableActionResolveManually")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<ResolveIncidentDialog
|
||||
open={isDialogOpen}
|
||||
incidentId={incident._id}
|
||||
onClose={closeDialog}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentActionsMenu.propTypes = {
|
||||
incident: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
status: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
onResolve: PropTypes.func.isRequired,
|
||||
onOpenDetails: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentActionsMenu;
|
||||
@@ -1,225 +1,144 @@
|
||||
//Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import IncidentActionsMenu from "./IncidentActionsMenu.jsx";
|
||||
|
||||
//Utils
|
||||
import { formatDateWithTz } from "@/Utils/timeUtils.js";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useFetchChecksTeam,
|
||||
useFetchChecksByMonitor,
|
||||
useResolveIncident,
|
||||
} from "@/Hooks/checkHooks.js";
|
||||
import { Button, Typography, useTheme } from "@mui/material";
|
||||
import { lighten } from "@mui/material/styles";
|
||||
|
||||
const GetTooltip = (row) => {
|
||||
const theme = useTheme();
|
||||
const phases = row?.timings?.phases;
|
||||
|
||||
const phaseKeyFormattingMap = {
|
||||
firstByte: "first byte",
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
py={theme.spacing(2)}
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Status code: ${row?.statusCode}`}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Response time: ${row?.responseTime} ms`}</Typography>
|
||||
{phases && (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Request timing: `}</Typography>
|
||||
<Table
|
||||
size="small"
|
||||
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
|
||||
>
|
||||
<TableBody>
|
||||
{Object.keys(phases)?.map((phaseKey) => (
|
||||
<TableRow key={phaseKey}>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="success"
|
||||
>
|
||||
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
variant="body2"
|
||||
>{`${phases[phaseKey]} ms`}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const IncidentTable = ({
|
||||
isLoading,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
dateRange,
|
||||
updateTrigger,
|
||||
setUpdateTrigger,
|
||||
monitors = [],
|
||||
incidents = [],
|
||||
incidentsCount = 0,
|
||||
isLoading = false,
|
||||
networkError = false,
|
||||
page = 0,
|
||||
rowsPerPage = 10,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
resolveIncident,
|
||||
handleUpdateTrigger,
|
||||
onOpenDetails
|
||||
}) => {
|
||||
//Redux state
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
//Local state
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const selectedMonitorDetails = monitors?.[selectedMonitor];
|
||||
const selectedMonitorType = selectedMonitorDetails?.type;
|
||||
|
||||
//Hooks
|
||||
const [resolveIncident, resolveLoading] = useResolveIncident();
|
||||
|
||||
const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
|
||||
type: selectedMonitorType,
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor !== "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
|
||||
useFetchChecksTeam({
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter === "resolved" ? "all" : filter,
|
||||
ack: filter === "resolved" ? true : false,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor === "0",
|
||||
updateTrigger,
|
||||
});
|
||||
|
||||
const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
|
||||
const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
|
||||
isLoading = isLoadingTeam || isLoadingMonitor;
|
||||
const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
//Handlers
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(event.target.value);
|
||||
};
|
||||
|
||||
const handleResolveIncident = (checkId) => {
|
||||
resolveIncident(checkId, setUpdateTrigger);
|
||||
const handleResolveIncident = async (incidentId, options = {}) => {
|
||||
try {
|
||||
await resolveIncident(incidentId);
|
||||
handleUpdateTrigger();
|
||||
} catch (error) {
|
||||
console.error(t("incidentsPage.errorResolvingIncident"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => monitors[row.monitorId]?.name ?? "N/A",
|
||||
render: (row) => {
|
||||
console.log(monitors, row);
|
||||
const monitor = monitors.find((monitor) => monitor.id === row.monitorId);
|
||||
return monitor ? monitor.name : "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "up" : "down";
|
||||
const status = row.status === true ? "down" : "up";
|
||||
const statusText =
|
||||
row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved");
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
text={statusText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dateTime",
|
||||
content: t("incidentsTableDateTime"),
|
||||
id: "startTime",
|
||||
content: t("incidentsPage.startTime"),
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.createdAt,
|
||||
row.startTime || row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "endTime",
|
||||
content: t("incidentsPage.endTime"),
|
||||
render: (row) => {
|
||||
if (row.endTime) {
|
||||
return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "resolutionType",
|
||||
content: t("incidentsPage.resolutionType"),
|
||||
render: (row) => {
|
||||
if (row.resolutionType) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
color:
|
||||
row.resolutionType === "manual"
|
||||
? theme.palette.accent.main
|
||||
: theme.palette.success.main,
|
||||
}}
|
||||
>
|
||||
{row.resolutionType}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("incidentsTableStatusCode"),
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{ id: "message", content: t("incidentsTableMessage"), render: (row) => row.message },
|
||||
{
|
||||
id: "message",
|
||||
content: t("incidentsTableMessage"),
|
||||
render: (row) => row.message || "-",
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
content: t("actions"),
|
||||
render: (row) => {
|
||||
return row.ack === false ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
handleResolveIncident(row._id);
|
||||
}}
|
||||
>
|
||||
{t("incidentsTableActionResolve")}
|
||||
</Button>
|
||||
) : (
|
||||
<Typography>
|
||||
{t("incidentsTableResolvedAt")}{" "}
|
||||
{formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)}
|
||||
</Typography>
|
||||
return (
|
||||
<IncidentActionsMenu
|
||||
incident={row}
|
||||
onResolve={handleResolveIncident}
|
||||
onOpenDetails={onOpenDetails}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading || resolveLoading) return <TableSkeleton />;
|
||||
if (isLoading) return <TableSkeleton />;
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
@@ -229,20 +148,25 @@ const IncidentTable = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && typeof checksCount === "undefined") {
|
||||
return <GenericFallback>{t("incidentsTableNoIncidents")}</GenericFallback>;
|
||||
if (!isLoading && !networkError && incidents?.length === 0) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
{t("incidentsTableNoIncidents", "No incidents found")}
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
const incidentsData = Array.isArray(incidents) ? incidents : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
config={{ tooltipContent: GetTooltip }}
|
||||
data={incidentsData}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel")}
|
||||
itemCount={checksCount}
|
||||
paginationLabel={t("incidentsTablePaginationLabel", "Incidents")}
|
||||
itemCount={incidentsCount || 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
@@ -253,12 +177,17 @@ const IncidentTable = ({
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.object,
|
||||
selectedMonitor: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
updateTrigger: PropTypes.bool,
|
||||
setUpdateTrigger: PropTypes.func,
|
||||
incidents: PropTypes.array.isRequired, // Array of incident objects
|
||||
incidentsCount: PropTypes.number.isRequired, // Total count for pagination
|
||||
isLoading: PropTypes.bool.isRequired, // Loading state
|
||||
networkError: PropTypes.bool, // Network error object
|
||||
page: PropTypes.number.isRequired, // Current page number
|
||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows per page
|
||||
handleChangePage: PropTypes.func.isRequired, // Handler for page change
|
||||
handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change
|
||||
resolveIncident: PropTypes.func.isRequired,
|
||||
handleUpdateTrigger: PropTypes.func.isRequired,
|
||||
onOpenDetails: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentTable;
|
||||
+1
-1
@@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => {
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
spacing={"16px"}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
File renamed without changes.
@@ -0,0 +1,96 @@
|
||||
import PropTypes from "prop-types";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration";
|
||||
|
||||
const IncidentItem = ({ incident }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isActive = incident.status === true;
|
||||
const duration = useGetIncidentsDuration(incident, isActive);
|
||||
const iconWrapperStyle = {
|
||||
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),
|
||||
width: "100%",
|
||||
py: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<Icon
|
||||
name="Globe"
|
||||
size={18}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
noWrap
|
||||
>
|
||||
{incident.monitorName || t("incidentsPage.unknownMonitor")}
|
||||
</Typography>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentItem.propTypes = {
|
||||
incident: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
monitorId: PropTypes.string,
|
||||
monitorName: PropTypes.string,
|
||||
status: PropTypes.bool,
|
||||
startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
||||
}),
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentItem;
|
||||
+25
-10
@@ -5,6 +5,7 @@ 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
|
||||
@@ -51,21 +52,35 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null
|
||||
return (
|
||||
<SummaryCard title={t("incidentsPage.incidentsLatestPanelTitle")}>
|
||||
{!incidents || incidents.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding={theme.spacing(6)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
<Box sx={{ mb: theme.spacing(1) }}>
|
||||
<Icon
|
||||
name="Check"
|
||||
size={24}
|
||||
color={theme.palette.success.main}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
textAlign="center"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
textAlign: "center",
|
||||
color: theme.palette.success.lowContrast,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.incidentsLatestPanelEmpty")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
{incidents.map((incident, index) => (
|
||||
@@ -5,7 +5,6 @@ import PropTypes from "prop-types";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const OptionsHeader = ({
|
||||
@@ -15,27 +14,27 @@ const OptionsHeader = ({
|
||||
monitors,
|
||||
filter = "all",
|
||||
setFilter,
|
||||
dateRange = "hour",
|
||||
dateRange = "all",
|
||||
setDateRange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
|
||||
|
||||
const filterOptions = [
|
||||
{ _id: "all", name: t("incidentsOptionsHeaderFilterAll") },
|
||||
{ _id: "down", name: t("incidentsOptionsHeaderFilterDown") },
|
||||
{ _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") },
|
||||
{ _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") },
|
||||
{ _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") },
|
||||
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
|
||||
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
|
||||
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
|
||||
];
|
||||
|
||||
// The stacks below which are three in number have the same style so
|
||||
const stackStyles = {
|
||||
direction: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(6),
|
||||
};
|
||||
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
if (!shouldRender) return;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography, Divider } 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 SummaryCard from "../SummaryCard/index.jsx";
|
||||
|
||||
/**
|
||||
* StatisticsPanel Component
|
||||
*
|
||||
* Displays key metrics and statistics about incidents.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.statistics - Statistics data object
|
||||
* @param {number} props.statistics.total - Total incidents count
|
||||
* @param {number} props.statistics.totalTrend - Trend percentage for total (optional)
|
||||
* @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m")
|
||||
* @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional)
|
||||
* @param {Object} props.statistics.topMonitor - Top monitor with most incidents
|
||||
* @param {string} props.statistics.topMonitor.name - Monitor name
|
||||
* @param {number} props.statistics.topMonitor.count - Incident count
|
||||
* @param {number} props.statistics.topMonitor.percentage - Percentage of total
|
||||
* @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m")
|
||||
* @param {number} props.statistics.availability - Availability percentage
|
||||
* @param {boolean} props.isLoading - Loading state
|
||||
* @param {Object} props.error - Error object if any
|
||||
*/
|
||||
const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const totalResolutions =
|
||||
(summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0);
|
||||
|
||||
const iconWrapperStyle = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
if (isLoading) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color="error"
|
||||
align="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsStatisticsPanelError")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<Icon
|
||||
name="Bell"
|
||||
size={18}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
>
|
||||
{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>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
sx={rowStyle}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<Icon
|
||||
name="Wrench"
|
||||
size={18}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("incidentsPage.avgResolutionTime")}:{" "}
|
||||
{summary.total > 0
|
||||
? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}`
|
||||
: "N/A"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsPanel.propTypes = {
|
||||
summary: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatisticsPanel;
|
||||
+10
-14
@@ -8,17 +8,14 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: 3,
|
||||
padding: "16px",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: isHighPriority
|
||||
? `${theme.spacing(1.5)} solid ${theme.palette.error.lowContrast}`
|
||||
: `${theme.spacing(1)} solid ${theme.palette.divider}`,
|
||||
boxShadow: theme.palette.tertiary.cardShadow,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
...sx,
|
||||
@@ -33,13 +30,12 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h2"
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
fontSize: theme.typography.h2.fontSize,
|
||||
|
||||
letterSpacing: theme.spacing(0.5),
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
@@ -49,9 +45,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }
|
||||
</Box>
|
||||
)}
|
||||
<Stack
|
||||
mt={theme.spacing(4)}
|
||||
paddingTop={theme.spacing(5)}
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(2)}
|
||||
paddingTop={theme.spacing(2)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
+1
-2
@@ -45,9 +45,8 @@ const useFetchIncidents = () => {
|
||||
setNetworkError(false);
|
||||
|
||||
const res = await networkService.getIncidentsByTeam(config);
|
||||
|
||||
setIncidents(res.data?.data?.incidents || []);
|
||||
setIncidentsCount(res.data?.data?.incidentsCount || 0);
|
||||
setIncidentsCount(res.data?.data?.count || 0);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
console.error(t("incidentsPage.errorFetchingIncidents"), error);
|
||||
File renamed without changes.
@@ -4,70 +4,102 @@ import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import IncidentTable from "./Components/IncidentTable/index.jsx";
|
||||
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
|
||||
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
|
||||
import { Box, Button } from "@mui/material";
|
||||
|
||||
import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx";
|
||||
import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx";
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js";
|
||||
import {
|
||||
useFetchChecksSummaryByTeamId,
|
||||
useAcknowledgeChecks,
|
||||
} from "@/Hooks/checkHooks.js";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
//Constants
|
||||
const Checks = () => {
|
||||
// Redux state
|
||||
// Hooks
|
||||
import useFetchIncidents from "./hooks/useFetchIncidents.js";
|
||||
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js";
|
||||
|
||||
const Incidents2 = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }];
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
|
||||
];
|
||||
|
||||
// Local state
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [filter, setFilter] = useState(undefined);
|
||||
const [dateRange, setDateRange] = useState(undefined);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [monitorLookup, setMonitorLookup] = useState(undefined);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||||
const [selectedIncidentId, setSelectedIncidentId] = useState(null);
|
||||
const handleUpdateTrigger = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
//Hooks
|
||||
const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks();
|
||||
const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId(
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
incidents,
|
||||
incidentsCount,
|
||||
isLoading: isLoadingIncidents,
|
||||
networkError: incidentsNetworkError,
|
||||
fetchIncidents,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
resolveIncident,
|
||||
} = useFetchIncidents();
|
||||
|
||||
const networkError = monitorsNetworkError || incidentsNetworkError;
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
|
||||
const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({
|
||||
updateTrigger,
|
||||
});
|
||||
const { monitorId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (monitorId) {
|
||||
setSelectedMonitor(monitorId);
|
||||
setPage(0);
|
||||
}, [selectedMonitor, filter, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const config = {
|
||||
monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined,
|
||||
sortOrder: "desc",
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
if (filter === "active") {
|
||||
fetchActiveIncidents(config);
|
||||
} else if (filter === "resolved") {
|
||||
fetchResolvedIncidents(config);
|
||||
} else {
|
||||
fetchIncidents(config);
|
||||
}
|
||||
}, [monitorId]);
|
||||
}, [
|
||||
selectedMonitor,
|
||||
filter,
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
updateTrigger,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
fetchIncidents,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const monitorLookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = {
|
||||
_id: monitor._id,
|
||||
const lookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor.id] = {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitorLookup(monitorLookup);
|
||||
setMonitorLookup(lookup);
|
||||
}, [monitors]);
|
||||
|
||||
const handleAcknowledge = () => {
|
||||
const monitorId = selectedMonitor === "0" ? null : selectedMonitor;
|
||||
acknowledge(setUpdateTrigger, monitorId);
|
||||
};
|
||||
|
||||
if (networkError || networkErrorSummary) {
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
@@ -75,27 +107,33 @@ const Checks = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
const handleOpenDetails = (id) => {
|
||||
if (!id) return;
|
||||
setSelectedIncidentId(id);
|
||||
setIsDetailsOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDetails = () => {
|
||||
setIsDetailsOpen(false);
|
||||
setSelectedIncidentId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Box alignSelf="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleAcknowledge}
|
||||
disabled={isLoadingAcknowledge}
|
||||
>
|
||||
{selectedMonitor === "0"
|
||||
? t("checksPageActionResolveAllMonitor")
|
||||
: t("checksPageActionResolveMonitor")}
|
||||
</Button>
|
||||
</Box>
|
||||
<StatusBoxes
|
||||
isLoading={isLoadingSummary}
|
||||
summary={summary}
|
||||
/>
|
||||
|
||||
<IncidentsSummaryPanel updateTrigger={updateTrigger} />
|
||||
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoading}
|
||||
shouldRender={!isLoadingMonitors}
|
||||
monitors={monitorLookup}
|
||||
selectedMonitor={selectedMonitor}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
@@ -104,17 +142,29 @@ const Checks = () => {
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
|
||||
<IncidentTable
|
||||
isLoading={isLoading}
|
||||
monitors={monitorLookup ? monitorLookup : {}}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
dateRange={dateRange}
|
||||
updateTrigger={updateTrigger}
|
||||
setUpdateTrigger={setUpdateTrigger}
|
||||
monitors={monitors || []}
|
||||
incidents={incidents || []}
|
||||
incidentsCount={incidentsCount || 0}
|
||||
isLoading={isLoadingIncidents}
|
||||
networkError={incidentsNetworkError}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
resolveIncident={resolveIncident}
|
||||
handleUpdateTrigger={handleUpdateTrigger}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
<IncidentDetailsModal
|
||||
open={isDetailsOpen}
|
||||
incidentId={selectedIncidentId}
|
||||
onClose={handleCloseDetails}
|
||||
onResolved={handleUpdateTrigger}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checks;
|
||||
export default Incidents2;
|
||||
@@ -1,188 +0,0 @@
|
||||
//Components
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import IncidentActionsMenu from "./IncidentActionsMenu.jsx";
|
||||
|
||||
//Utils
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
|
||||
const IncidentTable = ({
|
||||
incidents = [],
|
||||
incidentsCount = 0,
|
||||
isLoading = false,
|
||||
networkError = false,
|
||||
page = 0,
|
||||
rowsPerPage = 10,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
resolveIncident,
|
||||
handleUpdateTrigger,
|
||||
onOpenDetails,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleResolveIncident = async (incidentId, options = {}) => {
|
||||
try {
|
||||
await resolveIncident(incidentId, options);
|
||||
handleUpdateTrigger();
|
||||
} catch (error) {
|
||||
console.error(t("incidentsPage.errorResolvingIncident"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => row.monitorName ?? "N/A",
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("incidentsTableStatus"),
|
||||
render: (row) => {
|
||||
const status = row.status === true ? "down" : "up";
|
||||
const statusText =
|
||||
row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved");
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={statusText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "startTime",
|
||||
content: t("incidentsPage.startTime"),
|
||||
render: (row) => {
|
||||
const formattedDate = formatDateWithTz(
|
||||
row.startTime || row.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return formattedDate;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "endTime",
|
||||
content: t("incidentsPage.endTime"),
|
||||
render: (row) => {
|
||||
if (row.endTime) {
|
||||
return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "resolutionType",
|
||||
content: t("incidentsPage.resolutionType"),
|
||||
render: (row) => {
|
||||
if (row.resolutionType) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
color:
|
||||
row.resolutionType === "manual"
|
||||
? theme.palette.accent.main
|
||||
: theme.palette.success.main,
|
||||
}}
|
||||
>
|
||||
{row.resolutionType}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("incidentsTableStatusCode"),
|
||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{
|
||||
id: "message",
|
||||
content: t("incidentsTableMessage"),
|
||||
render: (row) => row.message || "-",
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
content: t("actions"),
|
||||
render: (row) => {
|
||||
return (
|
||||
<IncidentActionsMenu
|
||||
incident={row}
|
||||
onResolve={handleResolveIncident}
|
||||
onOpenDetails={onOpenDetails}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) return <TableSkeleton />;
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && !networkError && incidents?.length === 0) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
{t("incidentsTableNoIncidents", "No incidents found")}
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
const incidentsData = Array.isArray(incidents) ? incidents : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={incidentsData}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel", "Incidents")}
|
||||
itemCount={incidentsCount || 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
incidents: PropTypes.array.isRequired, // Array of incident objects
|
||||
incidentsCount: PropTypes.number.isRequired, // Total count for pagination
|
||||
isLoading: PropTypes.bool.isRequired, // Loading state
|
||||
networkError: PropTypes.bool, // Network error object
|
||||
page: PropTypes.number.isRequired, // Current page number
|
||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows per page
|
||||
handleChangePage: PropTypes.func.isRequired, // Handler for page change
|
||||
handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change
|
||||
resolveIncident: PropTypes.func.isRequired,
|
||||
handleUpdateTrigger: PropTypes.func.isRequired,
|
||||
onOpenDetails: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentTable;
|
||||
@@ -1,129 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import Monitors from "@/assets/icons/monitors.svg?react";
|
||||
import AverageResponseIcon from "@/assets/icons/status-pages.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration";
|
||||
|
||||
const IncidentItem = ({ incident }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isActive = incident.status === true;
|
||||
const duration = useGetIncidentsDuration(incident, isActive);
|
||||
const iconWrapperStyle = {
|
||||
px: theme.spacing(2),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg path": {
|
||||
stroke: "currentColor",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
width: "100%",
|
||||
py: theme.spacing(0.5),
|
||||
"&:hover": { opacity: 0.8 },
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(1.5)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentItem.propTypes = {
|
||||
incident: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
monitorId: PropTypes.string,
|
||||
monitorName: PropTypes.string,
|
||||
status: PropTypes.bool,
|
||||
startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
||||
}),
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default IncidentItem;
|
||||
@@ -1,234 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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";
|
||||
|
||||
/**
|
||||
* StatisticsPanel Component
|
||||
*
|
||||
* Displays key metrics and statistics about incidents.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.statistics - Statistics data object
|
||||
* @param {number} props.statistics.total - Total incidents count
|
||||
* @param {number} props.statistics.totalTrend - Trend percentage for total (optional)
|
||||
* @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m")
|
||||
* @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional)
|
||||
* @param {Object} props.statistics.topMonitor - Top monitor with most incidents
|
||||
* @param {string} props.statistics.topMonitor.name - Monitor name
|
||||
* @param {number} props.statistics.topMonitor.count - Incident count
|
||||
* @param {number} props.statistics.topMonitor.percentage - Percentage of total
|
||||
* @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m")
|
||||
* @param {number} props.statistics.availability - Availability percentage
|
||||
* @param {boolean} props.isLoading - Loading state
|
||||
* @param {Object} props.error - Error object if any
|
||||
*/
|
||||
const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
|
||||
const theme = useTheme();
|
||||
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(3),
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg path": {
|
||||
stroke: "currentColor",
|
||||
},
|
||||
};
|
||||
if (isLoading) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SummaryCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color="error"
|
||||
align="center"
|
||||
>
|
||||
{t("incidentsPage.incidentsStatisticsPanelError")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</SummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
...iconWrapperStyle,
|
||||
}}
|
||||
>
|
||||
<NotificationIcon />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box sx={iconWrapperStyle}>
|
||||
<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>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
paddingTop: theme.spacing(4),
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsPanel.propTypes = {
|
||||
summary: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatisticsPanel;
|
||||
@@ -1,171 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import IncidentTable from "./Components/IncidentTable/index.jsx";
|
||||
import OptionsHeader from "./Components/OptionsHeader/index.jsx";
|
||||
import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx";
|
||||
import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Hooks
|
||||
import useFetchIncidents from "./hooks/useFetchIncidents.js";
|
||||
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js";
|
||||
|
||||
const Incidents2 = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
|
||||
];
|
||||
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [monitorLookup, setMonitorLookup] = useState(undefined);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||||
const [selectedIncidentId, setSelectedIncidentId] = useState(null);
|
||||
const handleUpdateTrigger = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId(
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
incidents,
|
||||
incidentsCount,
|
||||
isLoading: isLoadingIncidents,
|
||||
networkError: incidentsNetworkError,
|
||||
fetchIncidents,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
resolveIncident,
|
||||
} = useFetchIncidents();
|
||||
|
||||
const networkError = monitorsNetworkError || incidentsNetworkError;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [selectedMonitor, filter, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const config = {
|
||||
monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined,
|
||||
sortOrder: "desc",
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
if (filter === "active") {
|
||||
fetchActiveIncidents(config);
|
||||
} else if (filter === "resolved") {
|
||||
fetchResolvedIncidents(config);
|
||||
} else {
|
||||
fetchIncidents(config);
|
||||
}
|
||||
}, [
|
||||
selectedMonitor,
|
||||
filter,
|
||||
dateRange,
|
||||
page,
|
||||
rowsPerPage,
|
||||
updateTrigger,
|
||||
fetchActiveIncidents,
|
||||
fetchResolvedIncidents,
|
||||
fetchIncidents,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const lookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = {
|
||||
_id: monitor._id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitorLookup(lookup);
|
||||
}, [monitors]);
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<NetworkError />
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangePage = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleOpenDetails = (id) => {
|
||||
if (!id) return;
|
||||
setSelectedIncidentId(id);
|
||||
setIsDetailsOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDetails = () => {
|
||||
setIsDetailsOpen(false);
|
||||
setSelectedIncidentId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
<IncidentsSummaryPanel updateTrigger={updateTrigger} />
|
||||
|
||||
<OptionsHeader
|
||||
shouldRender={!isLoadingMonitors}
|
||||
monitors={monitorLookup}
|
||||
selectedMonitor={selectedMonitor}
|
||||
setSelectedMonitor={setSelectedMonitor}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
|
||||
<IncidentTable
|
||||
incidents={incidents || []}
|
||||
incidentsCount={incidentsCount || 0}
|
||||
isLoading={isLoadingIncidents}
|
||||
networkError={incidentsNetworkError}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
resolveIncident={resolveIncident}
|
||||
handleUpdateTrigger={handleUpdateTrigger}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
<IncidentDetailsModal
|
||||
open={isDetailsOpen}
|
||||
incidentId={selectedIncidentId}
|
||||
onClose={handleCloseDetails}
|
||||
onResolved={handleUpdateTrigger}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Incidents2;
|
||||
@@ -2,8 +2,7 @@ import { useState } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -36,12 +35,18 @@ const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) =>
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
<Icon
|
||||
name="PauseCircle"
|
||||
size={20}
|
||||
/>
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
<Icon
|
||||
name="PlayCircle"
|
||||
size={20}
|
||||
/>
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -61,7 +61,7 @@ const useInfrastructureSubmit = () => {
|
||||
};
|
||||
|
||||
const finalForm = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...(isCreate ? {} : { id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
|
||||
@@ -22,9 +22,9 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useDeleteMonitor,
|
||||
useFetchGlobalSettings,
|
||||
useFetchHardwareMonitorById,
|
||||
useFetchMonitorById,
|
||||
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,14 +37,16 @@ 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 [monitor, isLoading] = useFetchHardwareMonitorById({
|
||||
const [isLoading] = useFetchMonitorById({
|
||||
monitorId,
|
||||
updateTrigger,
|
||||
setMonitor,
|
||||
updateTrigger: true,
|
||||
});
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
|
||||
@@ -127,7 +129,9 @@ const CreateInfrastructureMonitor = () => {
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const form = buildForm(infrastructureMonitor, https);
|
||||
const error = validateForm(form);
|
||||
// When editing, exclude URL from validation since it's disabled and can't be changed
|
||||
const formToValidate = isCreate ? form : { ...form, url: monitor.url };
|
||||
const error = validateForm(formToValidate);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
@@ -150,8 +154,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const TEMP_MONITOR = { id: monitor._id };
|
||||
await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/infrastructure" });
|
||||
await deleteMonitor({ monitor, redirect: "/infrastructure" });
|
||||
};
|
||||
|
||||
const isBusy =
|
||||
@@ -204,14 +207,14 @@ const CreateInfrastructureMonitor = () => {
|
||||
<></>
|
||||
)}
|
||||
</Typography>
|
||||
{!isCreate && (
|
||||
{!isCreate && monitor && (
|
||||
<MonitorStatusHeader
|
||||
monitor={monitor}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{!isCreate && (
|
||||
{!isCreate && monitor && (
|
||||
<MonitorActionButtons
|
||||
monitor={monitor}
|
||||
isBusy={isBusy}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const BaseContainer = ({ children, sx = {} }) => {
|
||||
const BaseContainer = ({ children, sx = {}, shouldExpand = false }) => {
|
||||
const theme = useTheme();
|
||||
const { getDimensions } = useHardwareUtils();
|
||||
return (
|
||||
@@ -22,7 +22,7 @@ const BaseContainer = ({ children, sx = {} }) => {
|
||||
sx={{
|
||||
padding: `${theme.spacing(getDimensions().baseBoxPaddingVertical)} ${theme.spacing(getDimensions().baseBoxPaddingHorizontal)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
width: shouldExpand ? "100%" : 225,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
@@ -38,6 +38,7 @@ const BaseContainer = ({ children, sx = {} }) => {
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sx: PropTypes.object,
|
||||
shouldExpand: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
@@ -17,21 +17,26 @@ const Gauge = ({
|
||||
valueThree,
|
||||
metricFour,
|
||||
valueFour,
|
||||
shouldExpand = false,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const valueStyle = {
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
width: "40%",
|
||||
minWidth: "40%",
|
||||
maxWidth: "60%",
|
||||
mb: theme.spacing(2),
|
||||
mt: theme.spacing(2),
|
||||
pr: theme.spacing(2),
|
||||
textAlign: "right",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseContainer>
|
||||
<BaseContainer shouldExpand={shouldExpand}>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(2)}
|
||||
@@ -116,6 +121,7 @@ Gauge.propTypes = {
|
||||
valueThree: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
metricFour: PropTypes.string,
|
||||
valueFour: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
shouldExpand: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Gauge;
|
||||
@@ -1,5 +1,5 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import { Box } from "@mui/material";
|
||||
import Gauge from "./Gauge.jsx";
|
||||
import SkeletonLayout from "./skeleton.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
@@ -65,16 +65,23 @@ const Gauges = ({ isLoading = false, monitor }) => {
|
||||
valueTwo: formatBytes(disk.total_bytes, true),
|
||||
metricThree: t("device"),
|
||||
valueThree: formatDeviceName(disk.device),
|
||||
metricFour: t("mountpoint"),
|
||||
metricFour: t("mountedOn"),
|
||||
valueFour: formatMountpoint(disk.mountpoint),
|
||||
})),
|
||||
];
|
||||
|
||||
// Only expand gauges to fill row when there are 4 or more
|
||||
const shouldExpand = gauges.length >= 4;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(8)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: shouldExpand
|
||||
? "repeat(auto-fill, minmax(200px, 1fr))"
|
||||
: "repeat(auto-fill, 225px)",
|
||||
gap: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{gauges.map((gauge) => {
|
||||
return (
|
||||
@@ -90,10 +97,11 @@ const Gauges = ({ isLoading = false, monitor }) => {
|
||||
valueThree={gauge.valueThree}
|
||||
metricFour={gauge.metricFour}
|
||||
valueFour={gauge.valueFour}
|
||||
shouldExpand={shouldExpand}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -123,14 +123,7 @@ const useHardwareUtils = () => {
|
||||
const formatDeviceName = (device) => {
|
||||
const deviceStr = String(device || "");
|
||||
|
||||
// Extract the last part of the path (after last '/')
|
||||
const parts = deviceStr.split("/");
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
// If there's more than one part, show with "..." prefix
|
||||
const displayText = parts.length > 1 ? `.../${lastPart}` : deviceStr;
|
||||
|
||||
// Always show tooltip with full device path
|
||||
// Show full device path
|
||||
return (
|
||||
<Tooltip
|
||||
title={deviceStr}
|
||||
@@ -149,7 +142,7 @@ const useHardwareUtils = () => {
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
{deviceStr}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -181,14 +174,7 @@ const useHardwareUtils = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Extract the last part of the path (after last '/')
|
||||
const parts = mountpointStr.split("/");
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
// If there's more than one part, show with "..." prefix
|
||||
const displayText = parts.length > 1 ? `.../${lastPart}` : mountpointStr;
|
||||
|
||||
// Always show tooltip with full mountpoint path
|
||||
// Show full mountpoint path
|
||||
return (
|
||||
<Tooltip
|
||||
title={mountpointStr}
|
||||
@@ -207,7 +193,7 @@ const useHardwareUtils = () => {
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
{mountpointStr}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -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 ClearIcon from "@mui/icons-material/Clear";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
@@ -68,7 +68,12 @@ const Filter = ({
|
||||
color={theme.palette.primary.contrastText}
|
||||
onClick={handleReset}
|
||||
variant="contained"
|
||||
endIcon={<ClearIcon />}
|
||||
endIcon={
|
||||
<Icon
|
||||
name="X"
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
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 CPUChipIcon from "@/assets/icons/cpu-chip.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
|
||||
|
||||
// Utils
|
||||
@@ -67,9 +67,9 @@ const MonitorsTable = ({
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
<Icon
|
||||
name="Cpu"
|
||||
size={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 Settings from "@/assets/icons/settings-bold.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "@/Components/v1/Dialog/index.jsx";
|
||||
import { networkService } from "@/Utils/NetworkService.js";
|
||||
@@ -100,7 +100,10 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
}}
|
||||
ref={anchor}
|
||||
>
|
||||
<Settings />
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -71,7 +71,7 @@ const InfrastructureMonitors = () => {
|
||||
|
||||
const field = toFilterStatus !== undefined ? "status" : undefined;
|
||||
|
||||
const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
|
||||
const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
|
||||
types: TYPES,
|
||||
limit: 1,
|
||||
page: page,
|
||||
|
||||
@@ -12,13 +12,16 @@ import { Box } from "@mui/material";
|
||||
const BaseContainer = ({children}) => {
|
||||
const theme = useTheme()
|
||||
return(
|
||||
<Box
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
minWidth: 250,
|
||||
width: "fit-content",
|
||||
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))`,
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
@@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(8)}
|
||||
gap={theme.spacing(8)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<InfrastructureStyleGauge
|
||||
|
||||
@@ -2,14 +2,17 @@ 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 Divider from "@mui/material/Divider";
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
||||
|
||||
import { useFetchLogs } from "../../../Hooks/logHooks.js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formatLog = (theme, log, idx) => {
|
||||
const LevelBadge = ({ level }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const levelColors = {
|
||||
info: theme.palette.success.main,
|
||||
warn: theme.palette.warning.main,
|
||||
@@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => {
|
||||
debug: theme.palette.accent.main,
|
||||
};
|
||||
|
||||
const color = levelColors[log.level] || theme.palette.primary.contrastText;
|
||||
const color = levelColors[level] || theme.palette.primary.contrastText;
|
||||
|
||||
return (
|
||||
<span key={idx}>
|
||||
<span>[{log.timestamp}]</span>{" "}
|
||||
<span style={{ color, fontWeight: "bold" }}>{log.level.toUpperCase()}</span>
|
||||
{": "}
|
||||
{`(${log.service})`}
|
||||
{`(${log.method})`}
|
||||
{": "}
|
||||
{log.message}
|
||||
<br />
|
||||
</span>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: color,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{level}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Logs = () => {
|
||||
// Local state
|
||||
const [logLevel, setLogLevel] = useState("all");
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return "-";
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const Logs = () => {
|
||||
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") },
|
||||
@@ -49,52 +59,116 @@ 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={(e) => {
|
||||
setLogLevel(e.target.value);
|
||||
}}
|
||||
onChange={handleLogLevelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
color: theme.palette.primary.contrastText,
|
||||
padding: 2,
|
||||
borderRadius: 1,
|
||||
overflowX: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
<DataTable
|
||||
shouldRender={!isLoading}
|
||||
headers={headers}
|
||||
data={paginatedLogs || []}
|
||||
config={{
|
||||
emptyView: t("logsPage.noLogs"),
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{logs
|
||||
?.filter((log) => {
|
||||
if (logLevel === "all") return true;
|
||||
return log.level === logLevel;
|
||||
})
|
||||
.reverse()
|
||||
.map((log, idx) => formatLog(theme, log, idx))}
|
||||
</code>
|
||||
</Box>
|
||||
/>
|
||||
|
||||
{filteredLogs?.length > 0 && (
|
||||
<Pagination
|
||||
paginationLabel={t("logsPage.table.logs")}
|
||||
itemCount={filteredLogs?.length || 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -14,8 +14,10 @@ const MonitorListItem = ({ monitor, onDelete }) => {
|
||||
width="100%"
|
||||
>
|
||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
||||
<DeleteIcon
|
||||
sx={{ cursor: "pointer" }}
|
||||
<Icon
|
||||
name="Trash2"
|
||||
size={20}
|
||||
style={{ 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 CalendarIcon from "@/assets/icons/calendar.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import "./index.css";
|
||||
import Search from "@/Components/v1/Inputs/Search/index.jsx";
|
||||
import { logger } from "@/Utils/Logger.js";
|
||||
@@ -217,7 +217,14 @@ const CreateMaintenance = () => {
|
||||
disablePast
|
||||
disableHighlightToday
|
||||
value={form.startDate}
|
||||
slots={{ openPickerIcon: () => <CalendarIcon /> }}
|
||||
slots={{
|
||||
openPickerIcon: () => (
|
||||
<Icon
|
||||
name="Calendar"
|
||||
size={20}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
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 Settings from "@/assets/icons/settings-bold.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { createToast } from "@/Utils/toastUtils.jsx";
|
||||
@@ -98,7 +98,10 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -2,8 +2,7 @@ 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 ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import ActionsMenu from "./ActionsMenu/index.jsx";
|
||||
import { memo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -68,9 +67,15 @@ const MaintenanceTable = ({
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
<Icon
|
||||
name="ArrowUp"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
<Icon
|
||||
name="ArrowDown"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
@@ -89,9 +94,15 @@ const MaintenanceTable = ({
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
<Icon
|
||||
name="ArrowUp"
|
||||
size={18}
|
||||
/>
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
<Icon
|
||||
name="ArrowDown"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Components
|
||||
import Menu from "@mui/material/Menu";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
|
||||
// Utils
|
||||
@@ -49,7 +49,10 @@ const ActionMenu = ({ notification, onDelete }) => {
|
||||
onClick={handleClick}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SettingsOutlinedIcon />
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
|
||||
@@ -3,10 +3,9 @@ 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 PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
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";
|
||||
@@ -178,8 +177,7 @@ const PageSpeedSetup = () => {
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const TEMP_MONITOR = { id: monitor._id };
|
||||
await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/pagespeed" });
|
||||
await deleteMonitor({ monitor: { id: monitorId }, redirect: "/pagespeed" });
|
||||
};
|
||||
|
||||
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
|
||||
@@ -314,12 +312,18 @@ const PageSpeedSetup = () => {
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
<Icon
|
||||
name="PauseCircle"
|
||||
size={20}
|
||||
/>
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
<Icon
|
||||
name="PlayCircle"
|
||||
size={20}
|
||||
/>
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Typography, Divider } from "@mui/material";
|
||||
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
|
||||
import MetricsIcon from "@/assets/icons/ruler-icon.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -10,7 +10,12 @@ const AreaChartLegend = ({ metrics, handleMetrics }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<LegendBox
|
||||
icon={<MetricsIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="Ruler"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
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 SpeedometerIcon from "@/assets/icons/speedometer-icon.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const PieChartLegend = ({ audits }) => {
|
||||
@@ -9,7 +9,12 @@ const PieChartLegend = ({ audits }) => {
|
||||
|
||||
return (
|
||||
<LegendBox
|
||||
icon={<SpeedometerIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="Gauge"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
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 ScoreIcon from "@/assets/icons/monitor-graph-line.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -24,7 +24,12 @@ const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) =
|
||||
>
|
||||
<ChartBox
|
||||
justifyContent="flex-start"
|
||||
icon={<ScoreIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="TrendingUp"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Score history"
|
||||
height="100%"
|
||||
borderRadiusRight={16}
|
||||
|
||||
@@ -4,9 +4,21 @@ import { Typography } from "@mui/material";
|
||||
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
|
||||
const uptimeDuration = getHumanReadableDuration(monitor?.uptimeDuration);
|
||||
const time = getHumanReadableDuration(monitor?.lastChecked);
|
||||
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 { t } = useTranslation();
|
||||
|
||||
@@ -16,7 +28,7 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
|
||||
heading="checks since"
|
||||
subHeading={
|
||||
<>
|
||||
{uptimeDuration}
|
||||
{streakTime}
|
||||
<Typography component="span">{t("ago")}</Typography>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
|
||||
import PerformanceIcon from "@/assets/icons/performance-report.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import PieChart from "../Charts/PieChart.jsx";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -17,7 +17,12 @@ const PerformanceReport = ({ shouldRender, audits }) => {
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={<PerformanceIcon />}
|
||||
icon={
|
||||
<Icon
|
||||
name="Layers"
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
header="Performance report"
|
||||
Legend={<PieChartLegend audits={audits} />}
|
||||
borderRadiusRight={16}
|
||||
|
||||
@@ -35,7 +35,7 @@ const PageSpeedDetails = () => {
|
||||
});
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
// Network
|
||||
const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({
|
||||
const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({
|
||||
monitorId,
|
||||
dateRange: "day",
|
||||
updateTrigger: trigger,
|
||||
@@ -95,7 +95,7 @@ const PageSpeedDetails = () => {
|
||||
/>
|
||||
<PageSpeedStatusBoxes
|
||||
shouldRender={!isLoading}
|
||||
monitor={monitor}
|
||||
monitorStats={monitorStats}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
shouldRender={!isLoading}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react";
|
||||
import PageSpeedIcon from "@/assets/icons/page-speed.svg?react";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -241,7 +240,10 @@ const Card = ({ monitor }) => {
|
||||
}}
|
||||
>
|
||||
<IconBox>
|
||||
<PageSpeedIcon />
|
||||
<Icon
|
||||
name="Gauge"
|
||||
size={20}
|
||||
/>
|
||||
</IconBox>
|
||||
<Typography
|
||||
component="h2"
|
||||
|
||||
Loaded 100 of 311 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user