Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79aca01bbf | ||
|
|
a87ca648dc |
No files matched your search
@@ -1,145 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
### Client (React/Vite)
|
|
||||||
```bash
|
|
||||||
cd client
|
|
||||||
npm install
|
|
||||||
npm run dev # Start dev server at http://localhost:5173
|
|
||||||
npm run build # TypeScript check + production build
|
|
||||||
npm run lint # ESLint (strict, max-warnings 0)
|
|
||||||
npm run format # Prettier formatting
|
|
||||||
npm run format-check # Check formatting
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server (Node.js/Express)
|
|
||||||
```bash
|
|
||||||
cd server
|
|
||||||
npm install
|
|
||||||
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
|
|
||||||
npm run build # TypeScript compile + path alias resolution
|
|
||||||
npm run test # Run Mocha tests with c8 coverage
|
|
||||||
npm run lint # ESLint v9
|
|
||||||
npm run lint-fix # Auto-fix lint issues
|
|
||||||
npm run format # Prettier formatting
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Development
|
|
||||||
```bash
|
|
||||||
cd docker/dev
|
|
||||||
./build_images.sh
|
|
||||||
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Setup
|
|
||||||
|
|
||||||
### Server `.env` (minimum required)
|
|
||||||
```env
|
|
||||||
CLIENT_HOST="http://localhost:5173"
|
|
||||||
JWT_SECRET="my_secret_key_change_this"
|
|
||||||
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
|
|
||||||
TOKEN_TTL="99d"
|
|
||||||
ORIGIN="localhost"
|
|
||||||
LOG_LEVEL="debug"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Client `.env`
|
|
||||||
```env
|
|
||||||
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
|
|
||||||
VITE_APP_LOG_LEVEL="debug"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Monorepo Structure
|
|
||||||
- `/client` - React 18 + TypeScript + Vite + MUI frontend
|
|
||||||
- `/server` - Node.js 20+ + Express + TypeScript backend
|
|
||||||
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
|
|
||||||
|
|
||||||
### Backend Layers
|
|
||||||
```
|
|
||||||
server/src/
|
|
||||||
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
|
||||||
├── service/ # Business logic
|
|
||||||
│ ├── business/ # Core monitoring logic
|
|
||||||
│ ├── infrastructure/ # Server/system utilities
|
|
||||||
│ └── system/ # App-level settings
|
|
||||||
├── db/
|
|
||||||
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
|
||||||
│ ├── migration/ # Database migrations (run on startup)
|
|
||||||
│ └── modules/ # Database-specific modules
|
|
||||||
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
|
||||||
├── routes/v1/ # API route definitions
|
|
||||||
├── validation/ # Joi input validation schemas
|
|
||||||
└── repositories/ # Data access layer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Structure
|
|
||||||
```
|
|
||||||
client/src/
|
|
||||||
├── Components/ # Reusable UI components
|
|
||||||
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
|
|
||||||
├── Features/ # Redux slices (Auth, UI)
|
|
||||||
├── Hooks/ # Custom React hooks
|
|
||||||
├── Utils/ # Utilities (NetworkService.js is main API client)
|
|
||||||
├── Validation/ # Input validation
|
|
||||||
└── locales/ # i18n translations
|
|
||||||
```
|
|
||||||
|
|
||||||
### API
|
|
||||||
- Base URL: `/api/v1`
|
|
||||||
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
|
|
||||||
- OpenAPI spec: `/server/openapi.json`
|
|
||||||
|
|
||||||
### Key Technologies
|
|
||||||
- **State Management**: Redux Toolkit + Redux-Persist
|
|
||||||
- **Data Fetching**: SWR + Axios
|
|
||||||
- **Database**: MongoDB with Mongoose ODM
|
|
||||||
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
|
|
||||||
- **i18n**: i18next + react-i18next (translations via PoEditor)
|
|
||||||
|
|
||||||
## Code Conventions
|
|
||||||
|
|
||||||
### Internationalization
|
|
||||||
All user-facing strings must use the translation function:
|
|
||||||
```javascript
|
|
||||||
t('your.key') // Never hardcode UI strings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Branching
|
|
||||||
- Always branch from `develop` (not master)
|
|
||||||
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
|
|
||||||
- PRs target `develop` branch
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
|
|
||||||
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
|
|
||||||
- Both use ESLint with strict settings
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
Server tests use Mocha + Chai + Sinon:
|
|
||||||
```bash
|
|
||||||
npm test # Run all tests with coverage
|
|
||||||
npm test -- --grep "pattern" # Run specific tests
|
|
||||||
```
|
|
||||||
Test files: `server/tests/**/*.test.js`
|
|
||||||
|
|
||||||
## Database Models
|
|
||||||
|
|
||||||
Key Mongoose models in `/server/src/db/models/`:
|
|
||||||
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
|
|
||||||
- **Check** - Individual monitoring check results
|
|
||||||
- **Incident** - Downtime incidents
|
|
||||||
- **User** - User accounts
|
|
||||||
- **Team** - Team/workspace management
|
|
||||||
- **StatusPage** - Public status pages
|
|
||||||
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
|
|
||||||
- **MaintenanceWindow** - Scheduled maintenance periods
|
|
||||||
- **AppSettings** - Global application settings
|
|
||||||
Generated
+44
-27
@@ -12,6 +12,7 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@hello-pangea/dnd": "^18.0.0",
|
"@hello-pangea/dnd": "^18.0.0",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
|
"@mui/icons-material": "6.4.11",
|
||||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||||
"@mui/material": "6.4.11",
|
"@mui/material": "6.4.11",
|
||||||
"@mui/x-charts": "^7.5.1",
|
"@mui/x-charts": "^7.5.1",
|
||||||
@@ -24,9 +25,8 @@
|
|||||||
"human-interval": "2.0.1",
|
"human-interval": "2.0.1",
|
||||||
"i18next": "25.4.2",
|
"i18next": "25.4.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"mui-color-input": "^6.0.0",
|
"mui-color-input": "^6.0.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "9.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.63.0",
|
"react-hook-form": "7.63.0",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"recharts": "2.15.2",
|
"recharts": "2.15.2",
|
||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "4.1.11"
|
"zod": "4.1.11"
|
||||||
},
|
},
|
||||||
@@ -1189,6 +1189,32 @@
|
|||||||
"url": "https://opencollective.com/mui-org"
|
"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": {
|
"node_modules/@mui/lab": {
|
||||||
"version": "6.0.0-dev.240424162023-9968b4889d",
|
"version": "6.0.0-dev.240424162023-9968b4889d",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
|
||||||
@@ -2024,9 +2050,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -5271,9 +5297,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -5438,15 +5464,6 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -6112,12 +6129,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2"
|
"@remix-run/router": "1.23.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -6127,13 +6144,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.30.3",
|
"version": "6.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.2",
|
"@remix-run/router": "1.23.0",
|
||||||
"react-router": "6.30.3"
|
"react-router": "6.30.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
+3
-3
@@ -17,6 +17,7 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@hello-pangea/dnd": "^18.0.0",
|
"@hello-pangea/dnd": "^18.0.0",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
|
"@mui/icons-material": "6.4.11",
|
||||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||||
"@mui/material": "6.4.11",
|
"@mui/material": "6.4.11",
|
||||||
"@mui/x-charts": "^7.5.1",
|
"@mui/x-charts": "^7.5.1",
|
||||||
@@ -29,9 +30,8 @@
|
|||||||
"human-interval": "2.0.1",
|
"human-interval": "2.0.1",
|
||||||
"i18next": "25.4.2",
|
"i18next": "25.4.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"mui-color-input": "^6.0.0",
|
"mui-color-input": "^6.0.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "9.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.63.0",
|
"react-hook-form": "7.63.0",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"recharts": "2.15.2",
|
"recharts": "2.15.2",
|
||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "4.1.11"
|
"zod": "4.1.11"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-18
@@ -22,29 +22,13 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const theme = mode === "light" ? lightTheme : darkTheme;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<GlobalStyles
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
backgroundColor: theme.palette.background.main,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Routes />
|
<Routes />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
<ToastContainer
|
<ToastContainer />
|
||||||
newestOnTop={true}
|
|
||||||
theme={mode}
|
|
||||||
style={{
|
|
||||||
"--toastify-color-progress-light": "#7C8BA1",
|
|
||||||
"--toastify-color-progress-dark": "#7C8BA1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Icon from "../Icon";
|
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||||
import Dialog from "../Dialog/index.jsx";
|
import Dialog from "../Dialog/index.jsx";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -32,6 +32,7 @@ const ActionsMenu = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let monitor = { id: actions.id };
|
let monitor = { id: actions.id };
|
||||||
|
console.log(monitor);
|
||||||
await deleteMonitor({ monitor });
|
await deleteMonitor({ monitor });
|
||||||
updateRowCallback();
|
updateRowCallback();
|
||||||
};
|
};
|
||||||
@@ -83,10 +84,7 @@ const ActionsMenu = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Settings />
|
||||||
name="Settings"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||||
import Icon from "../Icon";
|
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||||
|
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||||
|
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,24 +13,9 @@ import "./index.css";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
info: (
|
info: <InfoOutlinedIcon />,
|
||||||
<Icon
|
error: <ErrorOutlineOutlinedIcon />,
|
||||||
name="Info"
|
warning: <WarningAmberOutlinedIcon />,
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
error: (
|
|
||||||
<Icon
|
|
||||||
name="AlertCircle"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
warning: (
|
|
||||||
<Icon
|
|
||||||
name="AlertTriangle"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,9 +97,10 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<CloseIcon
|
||||||
name="X"
|
sx={{
|
||||||
size={20}
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import Icon from "../Icon";
|
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
|
||||||
|
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
|
||||||
|
import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||||
if (type === "double") {
|
if (type === "double") {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<LeftArrowDouble
|
||||||
name="ChevronsLeft"
|
style={{ color }}
|
||||||
color={color}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (type === "long") {
|
} else if (type === "long") {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<LeftArrowLong
|
||||||
name="ArrowLeft"
|
style={{ color }}
|
||||||
color={color}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<LeftArrow
|
||||||
name="ChevronLeft"
|
style={{ color }}
|
||||||
color={color}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import Icon from "../Icon";
|
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
|
||||||
|
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
||||||
if (type === "double") {
|
if (type === "double") {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<RightArrowDouble
|
||||||
name="ChevronsRight"
|
style={{ color }}
|
||||||
color={color}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<RightArrow
|
||||||
name="ChevronRight"
|
style={{ color }}
|
||||||
color={color}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
const PlaceholderCheck = ({ daysToShow }) => {
|
const PlaceholderCheck = ({ daysToShow }) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||||
import {
|
import {
|
||||||
tooltipDateFormatLookup,
|
tooltipDateFormatLookup,
|
||||||
tickDateFormatLookup,
|
tickDateFormatLookup,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||||
import { useEffect, useState, forwardRef } from "react";
|
import { useEffect, useState, forwardRef } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -148,7 +148,7 @@ const StatusPageBarChart = ({ checks = [] }) => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
key={`check-${check?.id}`}
|
key={`check-${check?._id}`}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
className: "bar-tooltip",
|
className: "bar-tooltip",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { Text } from "recharts";
|
import { Text } from "recharts";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
|
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "./check.css";
|
import "./check.css";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Icon from "../Icon";
|
import CheckGrey from "../../../assets/icons/check.svg?react";
|
||||||
|
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
|
|
||||||
@@ -34,16 +35,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
{outlined ? (
|
{outlined ? (
|
||||||
<Icon
|
<CheckOutlined alt="check" />
|
||||||
name="CheckCircle"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Box
|
||||||
name="Check"
|
lineHeight={0}
|
||||||
size={16}
|
sx={{
|
||||||
color={colors[variant]}
|
"& svg > path": { fill: colors[variant] },
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<CheckGrey alt="form checks" />
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
@@ -56,14 +57,7 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
|||||||
fontWeight: 450,
|
fontWeight: 450,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{noHighlightText && (
|
{noHighlightText && <Typography component="span">{noHighlightText}</Typography>}{" "}
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
sx={{ color: "inherit" }}
|
|
||||||
>
|
|
||||||
{noHighlightText}
|
|
||||||
</Typography>
|
|
||||||
)}{" "}
|
|
||||||
{text}
|
{text}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import Icon from "../Icon";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
|
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
|
||||||
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -152,11 +153,7 @@ const AppAppBar = () => {
|
|||||||
aria-label="Menu button"
|
aria-label="Menu button"
|
||||||
onClick={toggleDrawer(true)}
|
onClick={toggleDrawer(true)}
|
||||||
>
|
>
|
||||||
<Icon
|
<MenuIcon sx={{ color: theme.palette.text.primary }} />
|
||||||
name="Menu"
|
|
||||||
size={24}
|
|
||||||
color={theme.palette.text.primary}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor="top"
|
anchor="top"
|
||||||
@@ -179,11 +176,7 @@ const AppAppBar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton onClick={toggleDrawer(false)}>
|
<IconButton onClick={toggleDrawer(false)}>
|
||||||
<Icon
|
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
|
||||||
name="X"
|
|
||||||
size={24}
|
|
||||||
color={theme.palette.text.primary}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
|
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Icon from "../Icon";
|
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable filter header component that displays a dropdown menu with selectable options.
|
* A reusable filter header component that displays a dropdown menu with selectable options.
|
||||||
@@ -29,10 +29,9 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
|
|||||||
<Select
|
<Select
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
IconComponent={(props) => (
|
IconComponent={(props) => (
|
||||||
<Icon
|
<AddCircleOutlineIcon
|
||||||
{...props}
|
{...props}
|
||||||
name="PlusCircle"
|
sx={{ fontSize: "medium" }}
|
||||||
size={18}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
|||||||
direction="row"
|
direction="row"
|
||||||
position="relative"
|
position="relative"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(5)}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
|||||||
<>
|
<>
|
||||||
<Dot />
|
<Dot />
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
variant="h6"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
color: percentageColor,
|
color: percentageColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* Maps legacy icon names to Lucide icon names
|
|
||||||
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
|
|
||||||
*/
|
|
||||||
export const iconMap = {
|
|
||||||
// Navigation/Sidebar icons
|
|
||||||
monitors: "Globe",
|
|
||||||
incidents: "AlertTriangle",
|
|
||||||
integrations: "Link",
|
|
||||||
"page-speed": "Gauge",
|
|
||||||
settings: "Settings",
|
|
||||||
notifications: "Bell",
|
|
||||||
maintenance: "Wrench",
|
|
||||||
"status-pages": "Wifi",
|
|
||||||
docs: "FileText",
|
|
||||||
discussions: "MessageCircle",
|
|
||||||
support: "HelpCircle",
|
|
||||||
changeLog: "Code",
|
|
||||||
logs: "Database",
|
|
||||||
|
|
||||||
// User icons
|
|
||||||
user: "User",
|
|
||||||
"user-two": "Users",
|
|
||||||
"user-edit": "UserPen",
|
|
||||||
groups: "Users",
|
|
||||||
lock: "Lock",
|
|
||||||
key: "Key",
|
|
||||||
logout: "LogOut",
|
|
||||||
|
|
||||||
// Action icons
|
|
||||||
edit: "Pencil",
|
|
||||||
"trash-bin": "Trash2",
|
|
||||||
search: "Search",
|
|
||||||
"close-icon": "X",
|
|
||||||
check: "Check",
|
|
||||||
"check-icon": "CheckCircle",
|
|
||||||
"check-outlined": "CheckCircle",
|
|
||||||
|
|
||||||
// Arrow icons
|
|
||||||
"left-arrow": "ChevronLeft",
|
|
||||||
"right-arrow": "ChevronRight",
|
|
||||||
"up-arrow": "ChevronUp",
|
|
||||||
"down-arrow": "ChevronDown",
|
|
||||||
"left-arrow-double": "ChevronsLeft",
|
|
||||||
"right-arrow-double": "ChevronsRight",
|
|
||||||
"left-arrow-long": "ArrowLeft",
|
|
||||||
"top-right-arrow": "ArrowUpRight",
|
|
||||||
"open-in-new-page": "ExternalLink",
|
|
||||||
"selector-vertical": "ChevronsUpDown",
|
|
||||||
|
|
||||||
// Status/Alert icons
|
|
||||||
"alert-icon": "Bell",
|
|
||||||
"warning-icon": "AlertCircle",
|
|
||||||
"history-icon": "History",
|
|
||||||
|
|
||||||
// Data/Chart icons
|
|
||||||
dashboard: "LayoutGrid",
|
|
||||||
"monitor-graph-line": "TrendingUp",
|
|
||||||
"response-time-icon": "TrendingUp",
|
|
||||||
"uptime-icon": "Activity",
|
|
||||||
"speedometer-icon": "Gauge",
|
|
||||||
"spedometer-icon": "Gauge",
|
|
||||||
"ruler-icon": "Ruler",
|
|
||||||
"performance-report": "Layers",
|
|
||||||
"average-response-icon": "BarChart3",
|
|
||||||
"distributed-uptime": "Network",
|
|
||||||
|
|
||||||
// Misc icons
|
|
||||||
calendar: "Calendar",
|
|
||||||
"calendar-check": "CalendarCheck",
|
|
||||||
"clock-snooze": "Clock",
|
|
||||||
mail: "Mail",
|
|
||||||
email: "Mail",
|
|
||||||
folder: "Folder",
|
|
||||||
"dots-vertical": "MoreVertical",
|
|
||||||
"pause-icon": "PauseCircle",
|
|
||||||
"resume-icon": "PlayCircle",
|
|
||||||
"checkbox-filled": "CheckSquare",
|
|
||||||
"checkbox-outline": "Square",
|
|
||||||
"radio-checked": "Circle",
|
|
||||||
"cpu-chip": "Cpu",
|
|
||||||
certificate: "Award",
|
|
||||||
"interval-check": "RefreshCw",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MUI icon to Lucide mapping
|
|
||||||
*/
|
|
||||||
export const muiToLucideMap = {
|
|
||||||
AddCircleOutline: "PlusCircle",
|
|
||||||
ArrowDownwardRounded: "ArrowDown",
|
|
||||||
ArrowUpwardRounded: "ArrowUp",
|
|
||||||
ArrowDropDown: "ChevronDown",
|
|
||||||
ArrowOutward: "ExternalLink",
|
|
||||||
CheckCircle: "CheckCircle2",
|
|
||||||
Clear: "X",
|
|
||||||
Close: "X",
|
|
||||||
CloseRounded: "X",
|
|
||||||
CloudUpload: "Upload",
|
|
||||||
Delete: "Trash2",
|
|
||||||
DeleteOutlineRounded: "Trash2",
|
|
||||||
Email: "Mail",
|
|
||||||
ErrorOutline: "AlertTriangle",
|
|
||||||
ErrorOutlineOutlined: "AlertTriangle",
|
|
||||||
Image: "ImageIcon",
|
|
||||||
InfoOutlined: "Info",
|
|
||||||
KeyboardArrowDown: "ChevronDown",
|
|
||||||
Menu: "Menu",
|
|
||||||
PauseCircleOutline: "PauseCircle",
|
|
||||||
PauseOutlined: "Pause",
|
|
||||||
PlayArrowOutlined: "Play",
|
|
||||||
PlayCircleOutlineRounded: "PlayCircle",
|
|
||||||
RadioButtonChecked: "Circle",
|
|
||||||
ReorderRounded: "GripVertical",
|
|
||||||
SettingsOutlined: "Settings",
|
|
||||||
Share: "Share2",
|
|
||||||
Visibility: "Eye",
|
|
||||||
VisibilityOff: "EyeOff",
|
|
||||||
WarningAmberOutlined: "AlertTriangle",
|
|
||||||
};
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { useTheme } from "@mui/material";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import {
|
|
||||||
Activity,
|
|
||||||
AlertCircle,
|
|
||||||
AlertTriangle,
|
|
||||||
ArrowDown,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowUpRight,
|
|
||||||
Bell,
|
|
||||||
Calendar,
|
|
||||||
Check,
|
|
||||||
CheckCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
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,
|
|
||||||
CheckCircle2,
|
|
||||||
CheckSquare,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
ChevronsLeft,
|
|
||||||
ChevronsRight,
|
|
||||||
ChevronsUpDown,
|
|
||||||
Circle,
|
|
||||||
CircleDot,
|
|
||||||
Clock,
|
|
||||||
Code,
|
|
||||||
Cpu,
|
|
||||||
Database,
|
|
||||||
ExternalLink,
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
FileText,
|
|
||||||
Gauge,
|
|
||||||
Globe,
|
|
||||||
GripVertical,
|
|
||||||
HelpCircle,
|
|
||||||
History,
|
|
||||||
Image: ImageIcon,
|
|
||||||
Info,
|
|
||||||
Key,
|
|
||||||
Layers,
|
|
||||||
Link,
|
|
||||||
Lock,
|
|
||||||
LogOut,
|
|
||||||
Mail,
|
|
||||||
Menu,
|
|
||||||
MessageCircle,
|
|
||||||
MoreVertical,
|
|
||||||
Pause,
|
|
||||||
PauseCircle,
|
|
||||||
Play,
|
|
||||||
PlayCircle,
|
|
||||||
PlusCircle,
|
|
||||||
RefreshCw,
|
|
||||||
Ruler,
|
|
||||||
Search,
|
|
||||||
Settings,
|
|
||||||
Square,
|
|
||||||
Trash2,
|
|
||||||
TrendingUp,
|
|
||||||
Upload,
|
|
||||||
User,
|
|
||||||
Users,
|
|
||||||
Wifi,
|
|
||||||
Wrench,
|
|
||||||
X,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme-aware icon component wrapping Lucide icons
|
|
||||||
*
|
|
||||||
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
|
|
||||||
* @param {number} size - Icon size in pixels (default: 20)
|
|
||||||
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
|
|
||||||
* @param {number} strokeWidth - Stroke width (default: 1.5)
|
|
||||||
* @param {string} fill - Fill color (default: "none")
|
|
||||||
*/
|
|
||||||
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
// Resolve color from theme path or use direct value
|
|
||||||
const resolveColor = (colorValue) => {
|
|
||||||
if (!colorValue) {
|
|
||||||
return theme.palette.primary.contrastTextTertiary; // Default icon color
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a theme path like "primary.contrastText"
|
|
||||||
if (typeof colorValue === "string" && colorValue.includes(".")) {
|
|
||||||
const parts = colorValue.split(".");
|
|
||||||
let resolved = theme.palette;
|
|
||||||
for (const part of parts) {
|
|
||||||
resolved = resolved?.[part];
|
|
||||||
}
|
|
||||||
return resolved || colorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct color value
|
|
||||||
return colorValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LucideIcon = iconComponents[name];
|
|
||||||
|
|
||||||
if (!LucideIcon) {
|
|
||||||
console.warn(`Icon "${name}" not found in Icon component`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LucideIcon
|
|
||||||
size={size}
|
|
||||||
color={resolveColor(color)}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill={fill}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Icon.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number,
|
|
||||||
color: PropTypes.string,
|
|
||||||
strokeWidth: PropTypes.number,
|
|
||||||
fill: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Icon;
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import Icon from "../../Icon";
|
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
|
||||||
|
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkbox Component
|
* Checkbox Component
|
||||||
@@ -64,8 +65,8 @@ const Checkbox = ({
|
|||||||
name={name}
|
name={name}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
icon={<Icon name="Square" />}
|
icon={<CheckboxOutline />}
|
||||||
checkedIcon={<Icon name="CheckSquare" />}
|
checkedIcon={<CheckboxFilled />}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
"aria-label": "controlled checkbox",
|
"aria-label": "controlled checkbox",
|
||||||
id: id,
|
id: id,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// Components
|
// Components
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import Icon from "../../Icon";
|
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||||
import Image from "../../Image/index.jsx";
|
import Image from "../../Image/index.jsx";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ProgressUpload from "../../ProgressBars/index.jsx";
|
import ProgressUpload from "../../ProgressBars/index.jsx";
|
||||||
|
import ImageIcon from "@mui/icons-material/Image";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -175,10 +176,7 @@ const ImageUpload = ({
|
|||||||
boxShadow: theme.shape.boxShadow,
|
boxShadow: theme.shape.boxShadow,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<CloudUploadIcon />
|
||||||
name="Upload"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography
|
<Typography
|
||||||
component="h2"
|
component="h2"
|
||||||
@@ -205,12 +203,7 @@ const ImageUpload = ({
|
|||||||
</Box>
|
</Box>
|
||||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||||
<ProgressUpload
|
<ProgressUpload
|
||||||
icon={
|
icon={<ImageIcon />}
|
||||||
<Icon
|
|
||||||
name="Image"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={file?.name || "Upload failed"}
|
label={file?.name || "Upload failed"}
|
||||||
size={file?.size}
|
size={file?.size}
|
||||||
progress={progress.value}
|
progress={progress.value}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
|
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import Icon from "../../Icon";
|
import RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,12 +47,7 @@ const Radio = ({
|
|||||||
<MUIRadio
|
<MUIRadio
|
||||||
id={id}
|
id={id}
|
||||||
size={size}
|
size={size}
|
||||||
checkedIcon={
|
checkedIcon={<RadioChecked />}
|
||||||
<Icon
|
|
||||||
name="CircleDot"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: "transparent",
|
color: "transparent",
|
||||||
width: 16,
|
width: 16,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import Icon from "../../Icon";
|
import SearchIcon from "../../../../assets/icons/search.svg?react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||||
@@ -31,13 +31,20 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
|
|||||||
const SearchAdornment = () => {
|
const SearchAdornment = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Box height={16}>
|
<Box
|
||||||
<Icon
|
height={16}
|
||||||
name="Search"
|
sx={{
|
||||||
size={16}
|
"& svg": {
|
||||||
color="primary.contrastTextTertiary"
|
width: 16,
|
||||||
strokeWidth={1.2}
|
height: 16,
|
||||||
/>
|
"& path": {
|
||||||
|
stroke: theme.palette.primary.contrastTextTertiary,
|
||||||
|
strokeWidth: 1.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||||
import Icon from "../../Icon";
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -98,13 +98,7 @@ const Select = ({
|
|||||||
error={error}
|
error={error}
|
||||||
name={name}
|
name={name}
|
||||||
inputProps={{ id: id }}
|
inputProps={{ id: id }}
|
||||||
IconComponent={(props) => (
|
IconComponent={KeyboardArrowDownIcon}
|
||||||
<Icon
|
|
||||||
name="ChevronDown"
|
|
||||||
size={20}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
MenuProps={{ disableScrollLock: true }}
|
MenuProps={{ disableScrollLock: true }}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Icon from "../../../Icon";
|
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||||
|
import Visibility from "@mui/icons-material/Visibility";
|
||||||
|
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
|
||||||
|
import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react";
|
||||||
|
|
||||||
export const HttpAdornment = ({ https }) => {
|
export const HttpAdornment = ({ https }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -53,17 +56,7 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldType === "password" ? (
|
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
||||||
<Icon
|
|
||||||
name="EyeOff"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon
|
|
||||||
name="Eye"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
||||||
@@ -14,7 +12,15 @@
|
|||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
|
||||||
.home-layout > .home-content-wrapper {
|
/* .home-layout aside {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
max-width: var(--env-var-side-bar-width);
|
||||||
|
} */
|
||||||
|
|
||||||
|
.home-layout > div {
|
||||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -24,6 +30,4 @@
|
|||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,17 @@
|
|||||||
import Sidebar from "../../Sidebar/index.jsx";
|
import Sidebar from "../../Sidebar/index.jsx";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
import { Box, Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const HomeLayout = () => {
|
const HomeLayout = () => {
|
||||||
const { width, transition } = useSidebar();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="home-layout"
|
className="home-layout"
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
|
gap={14}
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
{/* Spacer for fixed sidebar */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width,
|
|
||||||
flexShrink: 0,
|
|
||||||
transition,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack className="home-content-wrapper">
|
<Stack className="home-content-wrapper">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||||
import Icon from "../Icon";
|
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||||
import Grow from "@mui/material/Grow";
|
import Grow from "@mui/material/Grow";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
@@ -70,10 +70,7 @@ const MonitorActions = ({ isLoading }) => {
|
|||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
<Icon
|
<ArrowDropDownIcon />
|
||||||
name="ChevronDown"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Popper
|
<Popper
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import Status from "./status.jsx";
|
|||||||
import Skeleton from "./skeleton.jsx";
|
import Skeleton from "./skeleton.jsx";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
import Icon from "../Icon";
|
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||||
|
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||||
|
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||||
|
import EmailIcon from "@mui/icons-material/Email";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -72,12 +75,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
loading={isSending}
|
loading={isSending}
|
||||||
startIcon={
|
startIcon={<EmailIcon />}
|
||||||
<Icon
|
|
||||||
name="Mail"
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
disabled={isTestNotificationsDisabled}
|
disabled={isTestNotificationsDisabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
testAllNotifications({ monitorId: monitor?.id });
|
testAllNotifications({ monitorId: monitor?.id });
|
||||||
@@ -105,17 +103,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
loading={isPausing}
|
loading={isPausing}
|
||||||
startIcon={
|
startIcon={
|
||||||
monitor?.isActive ? (
|
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||||
<Icon
|
|
||||||
name="Pause"
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon
|
|
||||||
name="Play"
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pauseMonitor({
|
pauseMonitor({
|
||||||
@@ -124,19 +112,14 @@ const MonitorDetailsControlHeader = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{monitor?.isActive ? t("pause") : t("resume")}
|
{monitor?.isActive ? "Pause" : "Resume"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={
|
startIcon={<SettingsOutlinedIcon />}
|
||||||
<Icon
|
|
||||||
name="Settings"
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||||
>
|
>
|
||||||
Configure
|
Configure
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PulseDot from "../Animated/PulseDot.jsx";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Dot from "../Dot/index.jsx";
|
import Dot from "../Dot/index.jsx";
|
||||||
// Utils
|
// Utils
|
||||||
import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js";
|
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "../Icon";
|
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||||
import Search from "../Inputs/Search/index.jsx";
|
import Search from "../Inputs/Search/index.jsx";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -31,7 +31,7 @@ const NotificationConfig = ({
|
|||||||
setMonitor((prev) => {
|
setMonitor((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
notifications: value.map((notification) => notification.id),
|
notifications: value.map((notification) => notification._id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -39,14 +39,14 @@ const NotificationConfig = ({
|
|||||||
// Handlers
|
// Handlers
|
||||||
const handleDelete = (id) => {
|
const handleDelete = (id) => {
|
||||||
const updatedNotifications = selectedNotifications.filter(
|
const updatedNotifications = selectedNotifications.filter(
|
||||||
(notification) => notification.id !== id
|
(notification) => notification._id !== id
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedNotifications(updatedNotifications);
|
setSelectedNotifications(updatedNotifications);
|
||||||
setMonitor((prev) => {
|
setMonitor((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
notifications: updatedNotifications.map((notification) => notification.id),
|
notifications: updatedNotifications.map((notification) => notification._id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -57,7 +57,7 @@ const NotificationConfig = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setNotifications) {
|
if (setNotifications) {
|
||||||
const toSet = setNotifications.map((notification) => {
|
const toSet = setNotifications.map((notification) => {
|
||||||
return notifications.find((n) => n.id === notification);
|
return notifications.find((n) => n._id === notification);
|
||||||
});
|
});
|
||||||
setSelectedNotifications(toSet);
|
setSelectedNotifications(toSet);
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ const NotificationConfig = ({
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
key={notification.id}
|
key={notification._id}
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -102,13 +102,11 @@ const NotificationConfig = ({
|
|||||||
>
|
>
|
||||||
{notification.notificationName}
|
{notification.notificationName}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Icon
|
<DeleteOutlineRoundedIcon
|
||||||
name="Trash2"
|
|
||||||
size={20}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDelete(notification.id);
|
handleDelete(notification._id);
|
||||||
}}
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
{index < selectedNotifications.length - 1 && <Divider />}
|
{index < selectedNotifications.length - 1 && <Divider />}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
||||||
import Icon from "../Icon";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,10 +51,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
|||||||
alignItems={error ? "center" : "flex-start"}
|
alignItems={error ? "center" : "flex-start"}
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Icon
|
<ErrorOutlineOutlinedIcon />
|
||||||
name="AlertTriangle"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
) : icon ? (
|
) : icon ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -129,9 +127,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<CloseIcon
|
||||||
name="X"
|
sx={{
|
||||||
size={20}
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
||||||
import Icon from "../Icon";
|
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
|
||||||
|
import CheckCircle from "@mui/icons-material/CheckCircle";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const CustomStepIcon = (props) => {
|
const CustomStepIcon = (props) => {
|
||||||
const { completed, active } = props;
|
const { completed, active } = props;
|
||||||
return completed ? (
|
return completed ? (
|
||||||
<Icon
|
<CheckCircle color="accent" />
|
||||||
name="CheckCircle2"
|
|
||||||
size={24}
|
|
||||||
color="accent.main"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
|
||||||
name="Circle"
|
|
||||||
size={24}
|
|
||||||
color={active ? "accent.main" : "text.disabled"}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html2canvas from "html2canvas";
|
import html2canvas from "html2canvas";
|
||||||
import Icon from "../Icon";
|
import ShareIcon from "@mui/icons-material/Share";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
|
|
||||||
@@ -55,13 +55,7 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={
|
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
|
||||||
<Icon
|
|
||||||
name="Share2"
|
|
||||||
size={18}
|
|
||||||
color="success.main"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
color="success"
|
color="success"
|
||||||
onClick={captureAndShare}
|
onClick={captureAndShare}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import Avatar from "../../Avatar/index.jsx";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "../../Icon";
|
import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
|
||||||
|
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
|
||||||
|
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -80,10 +81,6 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(4),
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
|
||||||
"& svg": {
|
|
||||||
stroke: theme.palette.primary.contrastTextTertiary,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
@@ -163,14 +160,19 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
ml: "50px",
|
ml: "50px",
|
||||||
"&:focus": { outline: "none" },
|
"&:focus": { outline: "none" },
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
|
|
||||||
|
"& svg": {
|
||||||
|
width: "22px",
|
||||||
|
height: "22px",
|
||||||
|
},
|
||||||
|
"& svg path": {
|
||||||
|
/* Vertical three dots */
|
||||||
|
stroke: theme.palette.primary.contrastTextTertiary,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
onClick={(event) => openPopup(event)}
|
onClick={(event) => openPopup(event)}
|
||||||
>
|
>
|
||||||
<Icon
|
<DotsVertical />
|
||||||
name="MoreVertical"
|
|
||||||
size={22}
|
|
||||||
color="primary.contrastTextTertiary"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -254,13 +256,12 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
gap: theme.spacing(4),
|
gap: theme.spacing(4),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(4),
|
||||||
|
"& svg path": {
|
||||||
|
stroke: theme.palette.primary.contrastTextTertiary,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<LogoutSvg />
|
||||||
name="LogOut"
|
|
||||||
size={20}
|
|
||||||
color="primary.contrastTextTertiary"
|
|
||||||
/>
|
|
||||||
{t("menu.logOut", "Log out")}
|
{t("menu.logOut", "Log out")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -52,11 +52,12 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
|||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
color: iconStroke,
|
|
||||||
"& svg": {
|
"& svg": {
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
opacity: 0.81,
|
opacity: 0.81,
|
||||||
|
},
|
||||||
|
"& svg path": {
|
||||||
stroke: iconStroke,
|
stroke: iconStroke,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,13 +8,28 @@ import NavItem from "./components/navItem.jsx";
|
|||||||
import AuthFooter from "./components/authFooter.jsx";
|
import AuthFooter from "./components/authFooter.jsx";
|
||||||
|
|
||||||
import StarPrompt from "../StarPrompt/index.jsx";
|
import StarPrompt from "../StarPrompt/index.jsx";
|
||||||
import Icon from "../Icon";
|
import LockSvg from "../../../assets/icons/lock.svg?react";
|
||||||
|
import UserSvg from "../../../assets/icons/user.svg?react";
|
||||||
|
import TeamSvg from "../../../assets/icons/user-two.svg?react";
|
||||||
|
import Support from "../../../assets/icons/support.svg?react";
|
||||||
|
import Maintenance from "../../../assets/icons/maintenance.svg?react";
|
||||||
|
import Monitors from "../../../assets/icons/monitors.svg?react";
|
||||||
|
import Incidents from "../../../assets/icons/incidents.svg?react";
|
||||||
|
import Integrations from "../../../assets/icons/integrations.svg?react";
|
||||||
|
import PageSpeed from "../../../assets/icons/page-speed.svg?react";
|
||||||
|
import Settings from "../../../assets/icons/settings.svg?react";
|
||||||
|
import ChangeLog from "../../../assets/icons/changeLog.svg?react";
|
||||||
|
import Docs from "../../../assets/icons/docs.svg?react";
|
||||||
|
import StatusPages from "../../../assets/icons/status-pages.svg?react";
|
||||||
|
import Discussions from "../../../assets/icons/discussions.svg?react";
|
||||||
|
import Notifications from "../../../assets/icons/notifications.svg?react";
|
||||||
|
import Logs from "../../../assets/icons/logs.svg?react";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
|
||||||
|
|
||||||
const URL_MAP = {
|
const URL_MAP = {
|
||||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||||
@@ -24,48 +39,52 @@ const URL_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMenu = (t) => [
|
const getMenu = (t) => [
|
||||||
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
|
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
|
||||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
|
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
||||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
|
|
||||||
|
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
||||||
{
|
{
|
||||||
name: t("menu.notifications"),
|
name: t("menu.notifications"),
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
icon: <Icon name="Bell" />,
|
icon: <Notifications />,
|
||||||
},
|
},
|
||||||
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
|
{ name: t("menu.checks"), path: "checks", icon: <Docs /> },
|
||||||
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
|
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||||
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
|
|
||||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
|
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||||
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
|
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
||||||
|
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
||||||
|
|
||||||
{
|
{
|
||||||
name: t("menu.settings"),
|
name: t("menu.settings"),
|
||||||
icon: <Icon name="Settings" />,
|
icon: <Settings />,
|
||||||
path: "settings",
|
path: "settings",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getOtherMenuItems = (t) => [
|
const getOtherMenuItems = (t) => [
|
||||||
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
|
{ name: t("menu.support"), path: "support", icon: <Support /> },
|
||||||
{
|
{
|
||||||
name: t("menu.discussions"),
|
name: t("menu.discussions"),
|
||||||
path: "discussions",
|
path: "discussions",
|
||||||
icon: <Icon name="MessageCircle" />,
|
icon: <Discussions />,
|
||||||
},
|
},
|
||||||
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
|
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
|
||||||
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
|
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getAccountMenuItems = (t) => [
|
const getAccountMenuItems = (t) => [
|
||||||
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
|
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
|
||||||
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
|
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
|
||||||
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
|
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { collapsed, width, transition } = useSidebar();
|
// Redux state
|
||||||
|
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
|
||||||
|
|
||||||
const menu = getMenu(t);
|
const menu = getMenu(t);
|
||||||
const otherMenuItems = getOtherMenuItems(t);
|
const otherMenuItems = getOtherMenuItems(t);
|
||||||
@@ -74,19 +93,20 @@ const Sidebar = () => {
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
height="100vh"
|
height="100vh"
|
||||||
width={width}
|
width={
|
||||||
|
collapsed
|
||||||
|
? "var(--env-var-side-bar-collapsed-width)"
|
||||||
|
: "var(--env-var-side-bar-width)"
|
||||||
|
}
|
||||||
component="aside"
|
component="aside"
|
||||||
position="fixed"
|
position="sticky"
|
||||||
top={0}
|
top={0}
|
||||||
left={0}
|
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||||
paddingTop={theme.spacing(6)}
|
paddingTop={theme.spacing(6)}
|
||||||
paddingBottom={theme.spacing(6)}
|
paddingBottom={theme.spacing(6)}
|
||||||
gap={theme.spacing(6)}
|
gap={theme.spacing(6)}
|
||||||
sx={{
|
sx={{
|
||||||
transition,
|
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||||
backgroundColor: theme.palette.background.main,
|
|
||||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CollapseButton collapsed={collapsed} />
|
<CollapseButton collapsed={collapsed} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||||
import Icon from "../Icon";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -67,10 +67,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<CloseIcon sx={{ fontSize: "1.25rem" }} />
|
||||||
name="X"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Stack, TablePagination, Typography } from "@mui/material";
|
import { Stack, TablePagination, Typography } from "@mui/material";
|
||||||
import { TablePaginationActions } from "./Actions/index.jsx";
|
import { TablePaginationActions } from "./Actions/index.jsx";
|
||||||
import Icon from "../../Icon";
|
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
||||||
|
|
||||||
Pagination.propTypes = {
|
Pagination.propTypes = {
|
||||||
paginationLabel: PropTypes.string, // Label for the pagination.
|
paginationLabel: PropTypes.string, // Label for the pagination.
|
||||||
@@ -100,12 +100,7 @@ function Pagination({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputProps: { id: "pagination-dropdown" },
|
inputProps: { id: "pagination-dropdown" },
|
||||||
IconComponent: () => (
|
IconComponent: SelectorVertical,
|
||||||
<Icon
|
|
||||||
name="ChevronsUpDown"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
sx: {
|
sx: {
|
||||||
ml: theme.spacing(4),
|
ml: theme.spacing(4),
|
||||||
mr: theme.spacing(12),
|
mr: theme.spacing(12),
|
||||||
|
|||||||
@@ -3,31 +3,19 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import ToastBody from "./body.jsx";
|
import ToastBody from "./body.jsx";
|
||||||
import Icon from "../Icon";
|
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||||
|
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||||
|
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
info: (
|
info: <InfoOutlinedIcon />,
|
||||||
<Icon
|
error: <ErrorOutlineOutlinedIcon />,
|
||||||
name="Info"
|
warning: <WarningAmberOutlinedIcon />,
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
error: (
|
|
||||||
<Icon
|
|
||||||
name="AlertCircle"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
warning: (
|
|
||||||
<Icon
|
|
||||||
name="AlertTriangle"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||||
@@ -62,10 +50,7 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
|||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<IconButton onClick={onClick}>
|
<IconButton onClick={onClick}>
|
||||||
<Icon
|
<CloseIcon />
|
||||||
name="X"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -78,10 +63,7 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
|||||||
<ToastBody body={body} />
|
<ToastBody body={body} />
|
||||||
{!title && (
|
{!title && (
|
||||||
<IconButton onClick={onClick}>
|
<IconButton onClick={onClick}>
|
||||||
<Icon
|
<CloseIcon />
|
||||||
name="X"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import Menu from "@mui/material/Menu";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import { Settings } from "lucide-react";
|
|
||||||
|
|
||||||
export type ActionMenuItem = {
|
|
||||||
id: number | string;
|
|
||||||
label: React.ReactNode;
|
|
||||||
action: Function;
|
|
||||||
closeMenu?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | any>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<any>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setAnchorEl(e.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (e: React.MouseEvent<any>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<IconButton onClick={handleClick}>
|
|
||||||
<Settings
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
{items.map((item) => (
|
|
||||||
<MenuItem
|
|
||||||
key={item.id}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (item.closeMenu) handleClose(e);
|
|
||||||
item.action();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import { getResponseColor } from "@/Utils/DataUtils";
|
|
||||||
import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip";
|
|
||||||
import type { SxProps } from "@mui/material/styles";
|
|
||||||
import type { ResponsiveStyleValue } from "@mui/system";
|
|
||||||
|
|
||||||
interface PlaceholderCheck {
|
|
||||||
status: "placeholder";
|
|
||||||
responseTime: 0;
|
|
||||||
createdAt: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeatmapResponseTimeProps {
|
|
||||||
checks: CheckSnapshot[];
|
|
||||||
gap?: ResponsiveStyleValue<number | string>;
|
|
||||||
availabilityCellSx?: SxProps;
|
|
||||||
responseCellSx?: SxProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeatmapResponseTime = ({
|
|
||||||
checks,
|
|
||||||
gap,
|
|
||||||
availabilityCellSx,
|
|
||||||
responseCellSx,
|
|
||||||
}: HeatmapResponseTimeProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
if (!gap) {
|
|
||||||
gap = theme.spacing(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checks || checks.length === 0) return null;
|
|
||||||
|
|
||||||
const latestChecks = checks.slice(-25).reverse();
|
|
||||||
|
|
||||||
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
|
||||||
if (latestChecks.length !== 25) {
|
|
||||||
const placeholders = Array(25 - latestChecks.length).fill({
|
|
||||||
status: "placeholder" as const,
|
|
||||||
});
|
|
||||||
data = [...latestChecks, ...placeholders];
|
|
||||||
} else {
|
|
||||||
data = latestChecks;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(25, 1fr)",
|
|
||||||
gap: gap,
|
|
||||||
alignItems: "stretch",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.map((check, index) => {
|
|
||||||
const statusBg =
|
|
||||||
check.status === "placeholder"
|
|
||||||
? theme.palette.action.hover
|
|
||||||
: check.status === true
|
|
||||||
? theme.palette.success.light
|
|
||||||
: theme.palette.error.light;
|
|
||||||
const respBg =
|
|
||||||
check.status === "placeholder"
|
|
||||||
? theme.palette.action.hover
|
|
||||||
: getResponseColor(check.responseTime || 0, {
|
|
||||||
start: theme.palette.success.main,
|
|
||||||
mid: theme.palette.warning.main,
|
|
||||||
end: theme.palette.error.main,
|
|
||||||
});
|
|
||||||
const statusBorder =
|
|
||||||
check.status === "placeholder"
|
|
||||||
? theme.palette.divider
|
|
||||||
: check.status === true
|
|
||||||
? theme.palette.success.main
|
|
||||||
: theme.palette.error.main;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HeatmapResponseTimeTooltip
|
|
||||||
key={`${check}-${index}`}
|
|
||||||
check={check}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateRows: "auto auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
aspectRatio: "10",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
bgcolor: statusBg,
|
|
||||||
borderRadius: theme.spacing(0.5),
|
|
||||||
...availabilityCellSx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
pointerEvents: "none",
|
|
||||||
borderRadius: theme.spacing(0.5),
|
|
||||||
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
aspectRatio: "1 / 1",
|
|
||||||
bgcolor: respBg,
|
|
||||||
borderRadius: theme.spacing(0.5),
|
|
||||||
...responseCellSx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</HeatmapResponseTimeTooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
type HeatmapCheck =
|
|
||||||
| CheckSnapshot
|
|
||||||
| { status: "placeholder"; responseTime: 0; createdAt: "" };
|
|
||||||
|
|
||||||
export const HeatmapResponseTimeTooltip = ({
|
|
||||||
children,
|
|
||||||
check,
|
|
||||||
}: {
|
|
||||||
children: React.ReactElement;
|
|
||||||
check: HeatmapCheck;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const getColor = (status: boolean) => {
|
|
||||||
if (status === true) return theme.palette.success.light;
|
|
||||||
if (status === false) return theme.palette.error.light;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (check.status === "placeholder") {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
slotProps={{
|
|
||||||
tooltip: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
color: "inherit",
|
|
||||||
boxShadow: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
border: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
p: theme.spacing(4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography>
|
|
||||||
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
|
||||||
</Typography>
|
|
||||||
{check?.responseTime && (
|
|
||||||
<Typography>
|
|
||||||
{t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Typography textTransform={"capitalize"}>
|
|
||||||
Status:{" "}
|
|
||||||
<span style={{ color: getColor(check?.status) }}>
|
|
||||||
{check?.status === true
|
|
||||||
? t("pages.common.monitors.status.up")
|
|
||||||
: t("pages.common.monitors.status.down")}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { ResponsiveStyleValue } from "@mui/system";
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
|
||||||
|
|
||||||
interface HistogramResponseTimeProps {
|
|
||||||
checks: CheckSnapshot[];
|
|
||||||
height?: ResponsiveStyleValue<number | string>;
|
|
||||||
gap?: ResponsiveStyleValue<number | string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_HEIGHT = 50;
|
|
||||||
|
|
||||||
export const HistogramResponseTime = ({
|
|
||||||
checks,
|
|
||||||
height = DEFAULT_HEIGHT,
|
|
||||||
gap,
|
|
||||||
}: HistogramResponseTimeProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (!Array.isArray(checks) || checks.length === 0) return null;
|
|
||||||
|
|
||||||
let data = Array<any>();
|
|
||||||
|
|
||||||
if (!checks || checks.length === 0) {
|
|
||||||
data = [];
|
|
||||||
}
|
|
||||||
if (checks.length !== 25) {
|
|
||||||
const placeholders = Array(25 - checks.length).fill({
|
|
||||||
status: "placeholder",
|
|
||||||
});
|
|
||||||
data = [...checks, ...placeholders];
|
|
||||||
} else {
|
|
||||||
data = checks;
|
|
||||||
}
|
|
||||||
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
|
||||||
const gridGap = gap ?? theme.spacing(0.5);
|
|
||||||
return (
|
|
||||||
<Box sx={{ width: "100%" }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(25, 1fr)",
|
|
||||||
gap: gridGap,
|
|
||||||
alignItems: "end",
|
|
||||||
height: chartHeight,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.map((check, index) => {
|
|
||||||
const isPlaceholder = (check as any).status === "placeholder";
|
|
||||||
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
|
|
||||||
const barColor =
|
|
||||||
check.status === true
|
|
||||||
? theme.palette.success.light
|
|
||||||
: theme.palette.error.light;
|
|
||||||
const bar = (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: theme.spacing(1),
|
|
||||||
bgcolor: theme.palette.action.hover,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: isPlaceholder ? 0 : heightPct,
|
|
||||||
bgcolor: barColor,
|
|
||||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
return isPlaceholder ? (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
sx={{ height: "100%" }}
|
|
||||||
>
|
|
||||||
{bar}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<HeatmapResponseTimeTooltip
|
|
||||||
key={index}
|
|
||||||
check={check}
|
|
||||||
>
|
|
||||||
{bar}
|
|
||||||
</HeatmapResponseTimeTooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Stack } from "@mui/material";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { ButtonInput } from "../../inputs/Button";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface HeaderCreateProps {
|
|
||||||
label?: string;
|
|
||||||
path: string;
|
|
||||||
isLoading?: boolean;
|
|
||||||
isAdmin?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeaderCreate = ({
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
isLoading = false,
|
|
||||||
isAdmin = false,
|
|
||||||
}: HeaderCreateProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
navigate(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="flex-end"
|
|
||||||
alignItems="center"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<ButtonInput
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={handleClick}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{label || t("common.buttons.create")}
|
|
||||||
</ButtonInput>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { Icon, MonitorStatus } from "@/Components/v2/design-elements";
|
|
||||||
import { Button } from "@/Components/v2/inputs";
|
|
||||||
import { Settings, Pause, Play, Mail, Bug } from "lucide-react";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { usePost } from "@/Hooks/UseApi";
|
|
||||||
|
|
||||||
import type { Monitor } from "@/Types/Monitor.js";
|
|
||||||
|
|
||||||
interface HeaderMonitorControlsProps {
|
|
||||||
path: string;
|
|
||||||
monitor?: Monitor;
|
|
||||||
isAdmin: boolean;
|
|
||||||
refetch: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeaderMonitorControls = ({
|
|
||||||
path,
|
|
||||||
monitor,
|
|
||||||
isAdmin,
|
|
||||||
refetch,
|
|
||||||
}: HeaderMonitorControlsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
|
||||||
const {
|
|
||||||
post,
|
|
||||||
loading: isPosting,
|
|
||||||
// error: postError,
|
|
||||||
} = usePost<any, Monitor>();
|
|
||||||
|
|
||||||
if (!monitor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
spacing={{ xs: theme.spacing(8), md: 0 }}
|
|
||||||
direction={{ xs: "column", md: "row" }}
|
|
||||||
alignItems={"center"}
|
|
||||||
justifyContent={"space-between"}
|
|
||||||
>
|
|
||||||
<MonitorStatus monitor={monitor} />
|
|
||||||
<Stack
|
|
||||||
width={{ xs: "100%", md: "auto" }}
|
|
||||||
direction={{ xs: "column", md: "row" }}
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
loading={isPosting}
|
|
||||||
startIcon={<Icon icon={Mail} />}
|
|
||||||
onClick={async () => {
|
|
||||||
await post(`/notifications/test/all`, { monitorId: monitor.id });
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.buttons.testNotifications")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<Icon icon={Bug} />}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/incidents/${monitor?.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common.buttons.incidents")}
|
|
||||||
</Button>
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
loading={isPosting}
|
|
||||||
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
|
|
||||||
onClick={async () => {
|
|
||||||
await post(`/monitors/pause/${monitor.id}`, {});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{monitor?.isActive ? t("pause") : t("resume")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<Icon icon={Settings} />}
|
|
||||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
interface MonitorTimeFrameHeaderProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
hasDateRange?: boolean;
|
|
||||||
dateRange: string;
|
|
||||||
setDateRange: (dateRange: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeaderTimeRange = ({
|
|
||||||
isLoading = false,
|
|
||||||
hasDateRange = true,
|
|
||||||
dateRange,
|
|
||||||
setDateRange,
|
|
||||||
}: MonitorTimeFrameHeaderProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
_event: React.MouseEvent<HTMLElement>,
|
|
||||||
newValue: string | null
|
|
||||||
) => {
|
|
||||||
if (newValue !== null) {
|
|
||||||
setDateRange(newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let timeFramePicker = null;
|
|
||||||
|
|
||||||
if (hasDateRange) {
|
|
||||||
timeFramePicker = (
|
|
||||||
<ToggleButtonGroup
|
|
||||||
value={dateRange}
|
|
||||||
exclusive
|
|
||||||
onChange={handleChange}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<ToggleButton
|
|
||||||
disabled={isLoading}
|
|
||||||
value="recent"
|
|
||||||
>
|
|
||||||
Recent
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton
|
|
||||||
disabled={isLoading}
|
|
||||||
value="day"
|
|
||||||
>
|
|
||||||
Day
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton
|
|
||||||
disabled={isLoading}
|
|
||||||
value="week"
|
|
||||||
>
|
|
||||||
Week
|
|
||||||
</ToggleButton>
|
|
||||||
<ToggleButton
|
|
||||||
disabled={isLoading}
|
|
||||||
value="month"
|
|
||||||
>
|
|
||||||
Month
|
|
||||||
</ToggleButton>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="flex-end"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Showing statistics for past{" "}
|
|
||||||
{dateRange === "recent"
|
|
||||||
? "2 hours"
|
|
||||||
: dateRange === "day"
|
|
||||||
? "24 hours"
|
|
||||||
: dateRange === "week"
|
|
||||||
? "7 days"
|
|
||||||
: "30 days"}
|
|
||||||
.
|
|
||||||
</Typography>
|
|
||||||
{timeFramePicker}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * from "../common/charts/HistogramResponseTime";
|
|
||||||
export * from "../common/charts/HeatmapResponseTime";
|
|
||||||
export * from "../common/charts/HeatmapResponseTimeTooltip";
|
|
||||||
export * from "../common/controls/HeaderCreate";
|
|
||||||
export * from "../common/controls/HeaderMonitorControls";
|
|
||||||
export * from "../common/controls/HeaderTimeRange";
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import Box, { type BoxProps } from "@mui/material/Box";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const BaseBox = ({ sx, ...rest }: BoxProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
{...rest}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
border: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { BaseBox } from "../design-elements";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import type { ResponsiveStyleValue } from "@mui/system";
|
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
type BaseChartProps = React.PropsWithChildren<{
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
width?: number | string;
|
|
||||||
maxWidth?: number | string;
|
|
||||||
padding?: number | string | ResponsiveStyleValue<number | string>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const BaseChart = ({
|
|
||||||
children,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
width = "100%",
|
|
||||||
maxWidth = "100%",
|
|
||||||
padding,
|
|
||||||
}: BaseChartProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
padding: padding ?? theme.spacing(8),
|
|
||||||
display: "flex",
|
|
||||||
flex: 1,
|
|
||||||
width: width,
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
gap={theme.spacing(8)}
|
|
||||||
flex={1}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems={"center"}
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
{icon && (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: 34,
|
|
||||||
height: 34,
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
"& svg": {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
"& path": {
|
|
||||||
stroke: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</BaseBox>
|
|
||||||
)}
|
|
||||||
<Typography variant="h2">{title}</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Box flex={1}>{children}</Box>
|
|
||||||
</Stack>
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Link from "@mui/material/Link";
|
|
||||||
import {
|
|
||||||
ErrorFallback,
|
|
||||||
EmptyFallback,
|
|
||||||
EmptyMonitorFallback,
|
|
||||||
BaseFallback,
|
|
||||||
} from "@/Components/v2/design-elements/Fallback";
|
|
||||||
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
|
|
||||||
import type { StackProps } from "@mui/material/Stack";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
|
|
||||||
export const PageSpeedKeyPriorityFallback = () => {
|
|
||||||
return (
|
|
||||||
<BaseFallback>
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography>
|
|
||||||
<Trans
|
|
||||||
i18nKey="common.alerts.pageSpeedApiKey.content"
|
|
||||||
components={{
|
|
||||||
settingsLink: (
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to="/settings"
|
|
||||||
color="inherit"
|
|
||||||
fontWeight="inherit"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
</BaseFallback>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BasePageProps extends StackProps {
|
|
||||||
loading?: boolean;
|
|
||||||
error?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
breadcrumbOverride?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BasePage = ({
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
children,
|
|
||||||
breadcrumbOverride,
|
|
||||||
...props
|
|
||||||
}: BasePageProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
sx={{ height: "100%" }}
|
|
||||||
>
|
|
||||||
<CircularProgress
|
|
||||||
color="primary"
|
|
||||||
size={28}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorFallback
|
|
||||||
title="Something went wrong..."
|
|
||||||
subtitle="Please try again later"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
spacing={theme.spacing(10)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Breadcrumb />
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BasePageWithStatesProps extends StackProps {
|
|
||||||
loading: boolean;
|
|
||||||
error: any;
|
|
||||||
items: any[];
|
|
||||||
bullets: string[] | unknown;
|
|
||||||
page: string;
|
|
||||||
actionButtonText: string;
|
|
||||||
actionLink: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BasePageWithStates = ({
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
page,
|
|
||||||
bullets,
|
|
||||||
actionButtonText,
|
|
||||||
actionLink,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: BasePageWithStatesProps) => {
|
|
||||||
const showLoading = loading && (!items || items.length === 0);
|
|
||||||
|
|
||||||
if (isEmpty(items)) {
|
|
||||||
return (
|
|
||||||
<EmptyFallback
|
|
||||||
bullets={bullets}
|
|
||||||
title={page}
|
|
||||||
actionButtonText={actionButtonText}
|
|
||||||
actionLink={actionLink}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BasePage
|
|
||||||
loading={showLoading}
|
|
||||||
error={error}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</BasePage>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MonitorBasePageWithStatesProps extends StackProps {
|
|
||||||
loading: boolean;
|
|
||||||
error: any;
|
|
||||||
items: any[];
|
|
||||||
page: string;
|
|
||||||
actionLink?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
priorityFallback?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmpty = (items: any[]) => {
|
|
||||||
if (!items) return true;
|
|
||||||
if (Array.isArray(items) && items.length === 0) return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MonitorBasePageWithStates = ({
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
page,
|
|
||||||
actionLink,
|
|
||||||
children,
|
|
||||||
priorityFallback,
|
|
||||||
...props
|
|
||||||
}: MonitorBasePageWithStatesProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const showLoading = loading && (!items || items.length === 0);
|
|
||||||
|
|
||||||
if (priorityFallback) {
|
|
||||||
return (
|
|
||||||
<BasePage
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{priorityFallback}
|
|
||||||
</BasePage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loading && isEmpty(items)) {
|
|
||||||
return (
|
|
||||||
<EmptyMonitorFallback
|
|
||||||
page={page}
|
|
||||||
title={t(`pages.${page}.fallback.title`)}
|
|
||||||
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
|
|
||||||
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
|
|
||||||
actionLink={actionLink || ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BasePage
|
|
||||||
loading={showLoading}
|
|
||||||
error={error}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</BasePage>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { Link, useLocation } from "react-router-dom";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
|
|
||||||
const isId = (segment: string): boolean => {
|
|
||||||
return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Breadcrumb = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const segments = location.pathname.split("/").filter((x) => x);
|
|
||||||
|
|
||||||
// Build simplified breadcrumb: "uptime" or "uptime / details"
|
|
||||||
const basePage = segments[0] || "home";
|
|
||||||
const hasDetailsPage = segments.length > 1 && isId(segments[1]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MuiBreadcrumbs
|
|
||||||
separator={
|
|
||||||
<ChevronRight
|
|
||||||
size={16}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
fontSize: "14px",
|
|
||||||
marginBottom: theme.spacing(6),
|
|
||||||
"& .MuiBreadcrumbs-separator": {
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hasDetailsPage ? (
|
|
||||||
<Link
|
|
||||||
to={`/${basePage}`}
|
|
||||||
style={{ textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: "14px",
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
"&:hover": {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
|
||||||
</Typography>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{hasDetailsPage && (
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</MuiBreadcrumbs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const BulletPointCheck = ({
|
|
||||||
text,
|
|
||||||
variant = "info",
|
|
||||||
}: {
|
|
||||||
text: string;
|
|
||||||
noHighlightText?: string;
|
|
||||||
variant?: "success" | "error" | "info";
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const colors: Record<string, string | undefined> = {
|
|
||||||
success: theme.palette.success.main,
|
|
||||||
error: theme.palette.error.main,
|
|
||||||
info: theme.palette.text.secondary,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
gap={theme.spacing(6)}
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
size={16}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
style={{
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
|
|
||||||
fontWeight={450}
|
|
||||||
sx={{
|
|
||||||
opacity: 0.9,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type { CSSProperties } from "react";
|
|
||||||
|
|
||||||
interface DotProps {
|
|
||||||
color?: string;
|
|
||||||
size?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
content: '""',
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: color,
|
|
||||||
opacity: 0.8,
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements";
|
|
||||||
import { Button } from "@/Components/v2/inputs";
|
|
||||||
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { StackProps } from "@mui/material/Stack";
|
|
||||||
|
|
||||||
interface BaseFallbackProps extends StackProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
alignItems={"center"}
|
|
||||||
margin={isSmall ? "inherit" : "auto"}
|
|
||||||
marginTop={isSmall ? "33%" : "10%"}
|
|
||||||
width={{
|
|
||||||
sm: "90%",
|
|
||||||
md: "70%",
|
|
||||||
lg: "50%",
|
|
||||||
xl: "40%",
|
|
||||||
}}
|
|
||||||
padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
|
|
||||||
bgcolor={theme.palette.background.paper}
|
|
||||||
border={1}
|
|
||||||
borderColor={theme.palette.divider}
|
|
||||||
borderRadius={theme.shape.borderRadius}
|
|
||||||
sx={{
|
|
||||||
borderStyle: "dashed",
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SkeletonCard showHalo={true} />
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ErrorFallback = ({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<BaseFallback>
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
marginY={theme.spacing(4)}
|
|
||||||
color={theme.palette.text.secondary}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{subtitle}</Typography>
|
|
||||||
</BaseFallback>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmptyFallback = ({
|
|
||||||
title,
|
|
||||||
bullets,
|
|
||||||
actionButtonText,
|
|
||||||
actionLink,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
bullets: any;
|
|
||||||
actionButtonText: string;
|
|
||||||
actionLink: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<BaseFallback>
|
|
||||||
<Stack
|
|
||||||
gap={theme.spacing(10)}
|
|
||||||
zIndex={1}
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
component="h1"
|
|
||||||
color={theme.palette.primary.contrastText}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
maxWidth: "1100px",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.isArray(bullets) &&
|
|
||||||
bullets?.map((bullet: string) => (
|
|
||||||
<BulletPointCheck
|
|
||||||
text={bullet}
|
|
||||||
key={`${bullet}-${Math.random()}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
<Stack>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => navigate(actionLink)}
|
|
||||||
>
|
|
||||||
{actionButtonText}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</BaseFallback>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmptyMonitorFallback = ({
|
|
||||||
page,
|
|
||||||
title,
|
|
||||||
bullets,
|
|
||||||
actionButtonText,
|
|
||||||
actionLink,
|
|
||||||
}: {
|
|
||||||
page: string;
|
|
||||||
title: string;
|
|
||||||
bullets: any;
|
|
||||||
actionButtonText: string;
|
|
||||||
actionLink: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<BaseFallback>
|
|
||||||
<Stack
|
|
||||||
gap={theme.spacing(10)}
|
|
||||||
zIndex={1}
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
component="h1"
|
|
||||||
color={theme.palette.primary.contrastText}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
maxWidth: "1100px",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.isArray(bullets) &&
|
|
||||||
bullets?.map((bullet: string, index: number) => (
|
|
||||||
<BulletPointCheck
|
|
||||||
text={bullet}
|
|
||||||
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
<Stack>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => navigate(actionLink)}
|
|
||||||
>
|
|
||||||
{actionButtonText}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</BaseFallback>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
|
|
||||||
interface IconProps {
|
|
||||||
icon: LucideIcon;
|
|
||||||
size?: number;
|
|
||||||
strokeWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
size={size}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Icon;
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { PulseDot, Dot } from "@/Components/v2/design-elements";
|
|
||||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import prettyMilliseconds from "pretty-ms";
|
|
||||||
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
||||||
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
||||||
|
|
||||||
if (!monitor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Typography
|
|
||||||
fontSize={typographyLevels.xl}
|
|
||||||
fontWeight={500}
|
|
||||||
color={theme.palette.primary.contrastText}
|
|
||||||
overflow={"hidden"}
|
|
||||||
textOverflow={"ellipsis"}
|
|
||||||
whiteSpace={"nowrap"}
|
|
||||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
|
||||||
>
|
|
||||||
{monitor.name}
|
|
||||||
</Typography>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems={"center"}
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
|
||||||
<Typography
|
|
||||||
fontSize={typographyLevels.l}
|
|
||||||
fontWeight={"bolder"}
|
|
||||||
fontFamily={"monospace"}
|
|
||||||
overflow={"hidden"}
|
|
||||||
textOverflow={"ellipsis"}
|
|
||||||
whiteSpace={"nowrap"}
|
|
||||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
|
||||||
>
|
|
||||||
{formatUrl(monitor?.url)}
|
|
||||||
</Typography>
|
|
||||||
{!isSmall && (
|
|
||||||
<>
|
|
||||||
<Dot />
|
|
||||||
<Typography>
|
|
||||||
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Box, Stack, useTheme } from "@mui/material";
|
|
||||||
|
|
||||||
interface PulseDotProps {
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PulseDot = ({ color }: PulseDotProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
width="26px"
|
|
||||||
height="24px"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
minWidth="18px"
|
|
||||||
minHeight="18px"
|
|
||||||
sx={{
|
|
||||||
position: "relative",
|
|
||||||
backgroundColor: color,
|
|
||||||
borderRadius: "50%",
|
|
||||||
"&::before": {
|
|
||||||
content: `""`,
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "inherit",
|
|
||||||
borderRadius: "50%",
|
|
||||||
animation: "ripple 1.8s ease-out infinite",
|
|
||||||
},
|
|
||||||
"&::after": {
|
|
||||||
content: `""`,
|
|
||||||
position: "absolute",
|
|
||||||
width: "7px",
|
|
||||||
height: "7px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { Box, Stack, useTheme } from "@mui/material";
|
|
||||||
|
|
||||||
interface SkeletonCardProps {
|
|
||||||
width?: number | string | { xs?: number | string; md?: number | string };
|
|
||||||
showHalo?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SkeletonCard = ({
|
|
||||||
width = { xs: 130, md: 216 },
|
|
||||||
showHalo = true,
|
|
||||||
}: SkeletonCardProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const pulseAnimation = {
|
|
||||||
"@keyframes pulse": {
|
|
||||||
"0%, 100%": { opacity: 1 },
|
|
||||||
"50%": { opacity: 0.72 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const floatAnimation = {
|
|
||||||
"@keyframes float": {
|
|
||||||
"0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" },
|
|
||||||
"50%": { transform: "rotate(5deg) translate(12px, -24px)" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
width: width,
|
|
||||||
height: 57,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
background:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? theme.palette.secondary.dark
|
|
||||||
: theme.palette.background.paper,
|
|
||||||
boxShadow:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset"
|
|
||||||
: "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset",
|
|
||||||
display: "flex",
|
|
||||||
gap: theme.spacing(5),
|
|
||||||
alignItems: "center",
|
|
||||||
padding: theme.spacing(5),
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockStyle = {
|
|
||||||
width: 55,
|
|
||||||
height: 34,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
background: theme.palette.secondary.main,
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
animation: "pulse 1.6s ease-in-out infinite",
|
|
||||||
...pulseAnimation,
|
|
||||||
};
|
|
||||||
|
|
||||||
const lineStyle = {
|
|
||||||
height: 7,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
background: theme.palette.secondary.main,
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
animation: "pulse 1.6s ease-in-out infinite",
|
|
||||||
...pulseAnimation,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
position={"relative"}
|
|
||||||
mt={20}
|
|
||||||
mb={20}
|
|
||||||
>
|
|
||||||
{showHalo && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
display: "grid",
|
|
||||||
placeItems: "center",
|
|
||||||
pointerEvents: "none",
|
|
||||||
"&::before": {
|
|
||||||
content: '""',
|
|
||||||
width: "260px",
|
|
||||||
height: "260px",
|
|
||||||
borderRadius: "999px",
|
|
||||||
background:
|
|
||||||
theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "#fff",
|
|
||||||
filter: "blur(60px)",
|
|
||||||
opacity: 0.9,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Card stack */}
|
|
||||||
<Box sx={{ position: "relative", zIndex: 1 }}>
|
|
||||||
{/* Front card (now at bottom) */}
|
|
||||||
<Box sx={cardStyle}>
|
|
||||||
<Box sx={blockStyle} />
|
|
||||||
<Stack sx={{ flex: 1, gap: "12px" }}>
|
|
||||||
{/* Empty space to maintain card height */}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Back card A */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
...cardStyle,
|
|
||||||
transform: "rotate(-4deg) translate(-12px, -12px)",
|
|
||||||
filter: "blur(.2px)",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Box sx={blockStyle} />
|
|
||||||
<Stack sx={{ flex: 1, gap: "12px" }}>
|
|
||||||
<Box sx={{ ...lineStyle, width: "60%" }} />
|
|
||||||
<Box sx={{ ...lineStyle, width: "80%" }} />
|
|
||||||
<Box sx={{ ...lineStyle, width: "40%" }} />
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Back card B */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
...cardStyle,
|
|
||||||
transform: "rotate(5deg) translate(12px, -20px)",
|
|
||||||
filter: "blur(.3px)",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
animation: "float 3s ease-in-out 5",
|
|
||||||
...floatAnimation,
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Box sx={blockStyle} />
|
|
||||||
<Stack sx={{ flex: 1, gap: "12px" }}>
|
|
||||||
<Box sx={{ ...lineStyle, width: "60%" }} />
|
|
||||||
<Box sx={{ ...lineStyle, width: "80%" }} />
|
|
||||||
<Box sx={{ ...lineStyle, width: "40%" }} />
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { lighten } from "@mui/material/styles";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
||||||
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
|
||||||
import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements";
|
|
||||||
import type { SxProps } from "@mui/material";
|
|
||||||
|
|
||||||
type GradientBox = React.PropsWithChildren<{
|
|
||||||
palette?: PaletteKey;
|
|
||||||
sx?: SxProps;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const GradientBox = ({ children, palette, sx }: GradientBox) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
||||||
const isLight = theme.palette.mode === "light";
|
|
||||||
const paper = theme.palette.background.paper;
|
|
||||||
const paperStart = lighten(paper, isLight ? 0.06 : 0.08);
|
|
||||||
const paperEnd = paper;
|
|
||||||
const bg = palette
|
|
||||||
? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)`
|
|
||||||
: `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
|
||||||
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
|
||||||
background: bg,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type StatBoxProps = React.PropsWithChildren<{
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
palette?: PaletteKey;
|
|
||||||
sx?: SxProps;
|
|
||||||
tooltip?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const StatBox = ({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
palette,
|
|
||||||
children,
|
|
||||||
sx,
|
|
||||||
tooltip,
|
|
||||||
onClick,
|
|
||||||
}: StatBoxProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GradientBox
|
|
||||||
palette={palette}
|
|
||||||
sx={{
|
|
||||||
...(sx as object),
|
|
||||||
...(onClick ? { cursor: "pointer", "&:hover": { opacity: 0.95 } } : {}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack onClick={onClick}>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
|
|
||||||
<Typography color={textColor}>{title}</Typography>
|
|
||||||
{tooltip && (
|
|
||||||
<TooltipWithInfo
|
|
||||||
title={tooltip}
|
|
||||||
iconColor={textColor as string}
|
|
||||||
iconSize={14}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Typography color={textColor}>{subtitle}</Typography>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
</GradientBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { BaseBox } from "@/Components/v2/design-elements";
|
|
||||||
import Background from "@/assets/Images/background-grid.svg?react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
type StatusBoxProps = React.PropsWithChildren<{ children: React.ReactNode }>;
|
|
||||||
|
|
||||||
export const BGBox = ({ children }: StatusBoxProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
position: "relative",
|
|
||||||
flex: 1,
|
|
||||||
padding: theme.spacing(4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="-10%"
|
|
||||||
left="5%"
|
|
||||||
>
|
|
||||||
<Background />
|
|
||||||
</Box>
|
|
||||||
{children}
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusBox = ({
|
|
||||||
label,
|
|
||||||
n,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
n: number;
|
|
||||||
color: string | undefined;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<BGBox>
|
|
||||||
<Stack spacing={theme.spacing(4)}>
|
|
||||||
<Typography
|
|
||||||
variant={"h2"}
|
|
||||||
textTransform="uppercase"
|
|
||||||
color={theme.palette.text.secondary}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
color={color}
|
|
||||||
>
|
|
||||||
{n}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</BGBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpStatusBox = ({ n }: { n: number }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<StatusBox
|
|
||||||
label={t("pages.common.monitors.status.up")}
|
|
||||||
n={n}
|
|
||||||
color={theme.palette.success.light}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DownStatusBox = ({ n }: { n: number }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<StatusBox
|
|
||||||
label={t("pages.common.monitors.status.down")}
|
|
||||||
n={n}
|
|
||||||
color={theme.palette.error.light}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PausedStatusBox = ({ n }: { n: number }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<StatusBox
|
|
||||||
label={t("pages.common.monitors.status.paused")}
|
|
||||||
n={n}
|
|
||||||
color={theme.palette.warning.light}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<StatusBox
|
|
||||||
label={t("pages.common.monitors.status.initializing")}
|
|
||||||
n={n}
|
|
||||||
color={theme.palette.warning.light}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import { BaseBox } from "@/Components/v2/design-elements";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
|
|
||||||
import type { MonitorStatus } from "@/Types/Monitor";
|
|
||||||
import type { SxProps } from "@mui/material/styles";
|
|
||||||
import { getStatusPalette, getValuePalette } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const ValueTypes = ["positive", "negative", "neutral"] as const;
|
|
||||||
export type ValueType = (typeof ValueTypes)[number];
|
|
||||||
|
|
||||||
export const StatusLabel = ({
|
|
||||||
status,
|
|
||||||
isActive,
|
|
||||||
sx,
|
|
||||||
}: {
|
|
||||||
status: MonitorStatus;
|
|
||||||
isActive?: boolean;
|
|
||||||
sx?: SxProps;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const palette = getStatusPalette(status);
|
|
||||||
|
|
||||||
const determineStatus = (
|
|
||||||
isActive: boolean | undefined,
|
|
||||||
status: MonitorStatus
|
|
||||||
): string => {
|
|
||||||
if (isActive === false) {
|
|
||||||
return t("pages.common.monitors.status.paused");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === true) {
|
|
||||||
return t("pages.common.monitors.status.up");
|
|
||||||
}
|
|
||||||
if (status === false) {
|
|
||||||
return t("pages.common.monitors.status.down");
|
|
||||||
}
|
|
||||||
return t("pages.common.monitors.status.initializing");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
display: "inline-flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: theme.spacing(3, 5),
|
|
||||||
color: theme.palette[palette].main,
|
|
||||||
borderColor:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "rgba(255, 255, 255, 0.08)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
width={7}
|
|
||||||
height={7}
|
|
||||||
bgcolor={theme.palette[palette].light}
|
|
||||||
borderRadius="50%"
|
|
||||||
marginRight="5px"
|
|
||||||
/>
|
|
||||||
<Typography textTransform={"capitalize"}>
|
|
||||||
{determineStatus(isActive, status)}
|
|
||||||
</Typography>
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const palette = getValuePalette(value);
|
|
||||||
const transformedText = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseBox
|
|
||||||
sx={{
|
|
||||||
display: "inline-flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: theme.spacing(3, 5),
|
|
||||||
color: theme.palette[palette].main,
|
|
||||||
borderColor:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "rgba(255, 255, 255, 0.08)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
width={7}
|
|
||||||
height={7}
|
|
||||||
bgcolor={theme.palette[palette].light}
|
|
||||||
borderRadius="50%"
|
|
||||||
marginRight="5px"
|
|
||||||
/>
|
|
||||||
{transformedText}
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,577 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Grid2 from "@mui/material/Grid2";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import Table from "@mui/material/Table";
|
|
||||||
import TableBody from "@mui/material/TableBody";
|
|
||||||
import TableCell from "@mui/material/TableCell";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
|
||||||
import TableHead from "@mui/material/TableHead";
|
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import Collapse from "@mui/material/Collapse";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import {
|
|
||||||
ChevronsLeft,
|
|
||||||
ChevronsRight,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Coffee,
|
|
||||||
Ellipsis,
|
|
||||||
PartyPopper,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import TablePagination from "@mui/material/TablePagination";
|
|
||||||
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useState, Fragment } from "react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
||||||
|
|
||||||
export type Header<T> = {
|
|
||||||
id: number | string;
|
|
||||||
content: React.ReactNode;
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
|
||||||
render: (row: T) => React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
|
||||||
headers: Header<T>[];
|
|
||||||
data: T[];
|
|
||||||
onRowClick?: (row: T) => void;
|
|
||||||
cardsOnSmallScreens?: boolean;
|
|
||||||
expandableRows?: boolean;
|
|
||||||
renderExpandedContent?: (row: T) => React.ReactNode;
|
|
||||||
emptyViewText?: string;
|
|
||||||
emptyViewPositive?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DataTable<
|
|
||||||
T extends {
|
|
||||||
id?: string | number;
|
|
||||||
_id?: string | number;
|
|
||||||
onRowClick?: (row: T) => void;
|
|
||||||
},
|
|
||||||
>({
|
|
||||||
headers,
|
|
||||||
data,
|
|
||||||
onRowClick,
|
|
||||||
cardsOnSmallScreens = true,
|
|
||||||
expandableRows = false,
|
|
||||||
renderExpandedContent,
|
|
||||||
emptyViewText,
|
|
||||||
emptyViewPositive,
|
|
||||||
}: DataTableProps<T>) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [expanded, setExpanded] = useState<(string | number) | null>(null);
|
|
||||||
const handleExpand = (row: T) => {
|
|
||||||
const key = row.id || row._id || null;
|
|
||||||
setExpanded(expanded === key ? null : key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
||||||
|
|
||||||
if (data.length === 0 || headers.length === 0) {
|
|
||||||
return (
|
|
||||||
<EmptyView
|
|
||||||
text={emptyViewText}
|
|
||||||
positive={emptyViewPositive}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = [];
|
|
||||||
// Return stack of cards for small screens
|
|
||||||
if (isSmall && cardsOnSmallScreens) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={theme.spacing(4)}>
|
|
||||||
{data.map((row) => {
|
|
||||||
const key = row.id || row._id || Math.random();
|
|
||||||
keys.push(key);
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
|
||||||
spacing={theme.spacing(4)}
|
|
||||||
sx={{
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
padding: theme.spacing(4),
|
|
||||||
cursor: onRowClick ? "pointer" : "default",
|
|
||||||
}}
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
{headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<Grid2
|
|
||||||
container
|
|
||||||
key={header.id}
|
|
||||||
>
|
|
||||||
<Grid2
|
|
||||||
size={5}
|
|
||||||
display={"flex"}
|
|
||||||
alignItems={"center"}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
component="div"
|
|
||||||
color={theme.palette.text.primary}
|
|
||||||
>
|
|
||||||
{header.content}
|
|
||||||
</Typography>
|
|
||||||
</Grid2>
|
|
||||||
<Grid2
|
|
||||||
size={7}
|
|
||||||
display="flex"
|
|
||||||
alignItems={"center"}
|
|
||||||
>
|
|
||||||
{header.render(row)}{" "}
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{expandableRows && renderExpandedContent && renderExpandedContent(row) && (
|
|
||||||
<Grid2 size={12}>{renderExpandedContent(row)}</Grid2>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer
|
|
||||||
component={Paper}
|
|
||||||
elevation={0}
|
|
||||||
sx={{ boxShadow: "none", overflowX: "hidden" }}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
sx={{
|
|
||||||
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
|
||||||
paddingLeft: theme.spacing(8),
|
|
||||||
},
|
|
||||||
"& .MuiTableCell-root": {
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
},
|
|
||||||
"& .MuiTableHead-root .MuiTableRow-root": {
|
|
||||||
height: "28px",
|
|
||||||
},
|
|
||||||
"& :is(th)": {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
padding: `${theme.spacing(2)} ${theme.spacing(8)}`,
|
|
||||||
fontSize: theme.typography.fontSize,
|
|
||||||
},
|
|
||||||
"& :is(td)": {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
|
|
||||||
fontSize: theme.typography.fontSize,
|
|
||||||
overflowX: "hidden",
|
|
||||||
},
|
|
||||||
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
|
||||||
borderBottom: "none",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
{headers.map((header, idx) => {
|
|
||||||
return (
|
|
||||||
<TableCell
|
|
||||||
align={idx === 0 ? "left" : "center"}
|
|
||||||
key={header.id}
|
|
||||||
>
|
|
||||||
{header.content}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((row) => {
|
|
||||||
const key = row.id || row._id || Math.random();
|
|
||||||
const isExpanded = expanded === key;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={key}>
|
|
||||||
<TableRow
|
|
||||||
sx={{
|
|
||||||
cursor: onRowClick ? "pointer" : "default",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (expandableRows) handleExpand(row);
|
|
||||||
else if (onRowClick) onRowClick(row);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{headers.map((header, index) => {
|
|
||||||
return (
|
|
||||||
<TableCell
|
|
||||||
align={index === 0 ? "left" : "center"}
|
|
||||||
key={header.id}
|
|
||||||
onClick={
|
|
||||||
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{header.render(row)}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
{expandableRows &&
|
|
||||||
renderExpandedContent &&
|
|
||||||
renderExpandedContent(row) && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={headers.length}
|
|
||||||
style={{
|
|
||||||
borderBottom: isExpanded ? undefined : "none",
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Collapse
|
|
||||||
in={isExpanded}
|
|
||||||
timeout="auto"
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<Box sx={{ pt: 4, pb: 4 }}>{renderExpandedContent(row)}</Box>
|
|
||||||
</Collapse>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TablePaginationActionsProps {
|
|
||||||
count: number;
|
|
||||||
page: number;
|
|
||||||
rowsPerPage: number;
|
|
||||||
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TablePaginationActions(props: TablePaginationActionsProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { count, page, rowsPerPage, onPageChange } = props;
|
|
||||||
|
|
||||||
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, page - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, page + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
|
||||||
className="table-pagination-actions"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleFirstPageButtonClick}
|
|
||||||
disabled={page === 0}
|
|
||||||
aria-label="first page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronsRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronsLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleBackButtonClick}
|
|
||||||
disabled={page === 0}
|
|
||||||
aria-label="previous page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleNextButtonClick}
|
|
||||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
|
||||||
aria-label="next page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleLastPageButtonClick}
|
|
||||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
|
||||||
aria-label="last page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronsLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronsRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HasMoreTablePaginationActionsProps {
|
|
||||||
hasMore?: boolean;
|
|
||||||
page: number;
|
|
||||||
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
|
||||||
}
|
|
||||||
function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { hasMore, page, onPageChange } = props;
|
|
||||||
|
|
||||||
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, page - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
onPageChange(event, page + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
|
||||||
className="table-pagination-actions"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleFirstPageButtonClick}
|
|
||||||
disabled={page === 0}
|
|
||||||
aria-label="first page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronsRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronsLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleBackButtonClick}
|
|
||||||
disabled={page === 0}
|
|
||||||
aria-label="previous page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleNextButtonClick}
|
|
||||||
disabled={hasMore === false}
|
|
||||||
aria-label="next page"
|
|
||||||
>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronLeft
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronRight
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<Tooltip title={hasMore === true ? "More pages available" : "No more pages"}>
|
|
||||||
<IconButton
|
|
||||||
disabled={hasMore === false}
|
|
||||||
aria-label="next page"
|
|
||||||
>
|
|
||||||
<Ellipsis
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaginationProps extends TablePaginationOwnProps {
|
|
||||||
component?: React.ElementType;
|
|
||||||
hasMore?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Pagination = ({ ...props }: PaginationProps) => {
|
|
||||||
const { hasMore, ...rest } = props;
|
|
||||||
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
|
||||||
const theme = useTheme();
|
|
||||||
const labelDisplayedRows = ({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
count,
|
|
||||||
}: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
count: number;
|
|
||||||
}) => {
|
|
||||||
if (hasMore) {
|
|
||||||
return to === 0 ? "" : `${from}–${to}`;
|
|
||||||
}
|
|
||||||
return `${from}–${to} of ${count}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TablePagination
|
|
||||||
ActionsComponent={(props) =>
|
|
||||||
typeof hasMore === "boolean" ? (
|
|
||||||
<HasMoreTablePaginationActions
|
|
||||||
{...props}
|
|
||||||
hasMore={hasMore}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TablePaginationActions {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
rowsPerPageOptions={[5, 10, 25]}
|
|
||||||
labelDisplayedRows={labelDisplayedRows}
|
|
||||||
{...rest}
|
|
||||||
sx={{
|
|
||||||
"& .MuiTablePagination-toolbar": isSmall
|
|
||||||
? {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gridAutoRows: "auto",
|
|
||||||
rowGap: theme.spacing(1),
|
|
||||||
alignItems: "center",
|
|
||||||
justifyItems: "center",
|
|
||||||
paddingLeft: 0,
|
|
||||||
paddingRight: 0,
|
|
||||||
}
|
|
||||||
: { display: "flex", alignItems: "center" },
|
|
||||||
"& .MuiTablePagination-selectLabel": {
|
|
||||||
gridColumn: "1",
|
|
||||||
gridRow: "1",
|
|
||||||
justifySelf: "center",
|
|
||||||
},
|
|
||||||
"& .MuiTablePagination-select": {
|
|
||||||
gridColumn: "2",
|
|
||||||
gridRow: "1",
|
|
||||||
justifySelf: "center",
|
|
||||||
},
|
|
||||||
"& .MuiTablePagination-displayedRows": isSmall
|
|
||||||
? { gridColumn: "1 / span 2", gridRow: "2", justifySelf: "center" }
|
|
||||||
: {},
|
|
||||||
"& .table-pagination-actions": isSmall
|
|
||||||
? { gridColumn: "1 / span 2", gridRow: "3", justifySelf: "center" }
|
|
||||||
: {},
|
|
||||||
"& .MuiSelect-select": {
|
|
||||||
border: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const Icon = positive ? PartyPopper : Coffee;
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
alignItems={"center"}
|
|
||||||
justifyContent={"center"}
|
|
||||||
sx={{
|
|
||||||
py: theme.spacing(12),
|
|
||||||
px: theme.spacing(4),
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
mb: theme.spacing(2),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
size={28}
|
|
||||||
strokeWidth={1}
|
|
||||||
color={theme.palette.text.secondary}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
color={theme.palette.text.primary}
|
|
||||||
sx={{ fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
{text ?? t("common.table.empty")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import MuiTooltip from "@mui/material/Tooltip";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { Info } from "lucide-react";
|
|
||||||
|
|
||||||
import type { TooltipProps } from "@mui/material/Tooltip";
|
|
||||||
|
|
||||||
type StyledTooltipProps = TooltipProps;
|
|
||||||
|
|
||||||
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MuiTooltip
|
|
||||||
title={props.title}
|
|
||||||
placement={placement}
|
|
||||||
slotProps={{
|
|
||||||
tooltip: {
|
|
||||||
sx: {
|
|
||||||
background:
|
|
||||||
theme.palette.mode === "dark"
|
|
||||||
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
|
|
||||||
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
color: "#ffffff",
|
|
||||||
fontSize: "13px",
|
|
||||||
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
|
|
||||||
borderRadius: `${theme.shape.borderRadius}px`,
|
|
||||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</MuiTooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface TooltipWithInfoProps extends Omit<StyledTooltipProps, "children"> {
|
|
||||||
iconSize?: number;
|
|
||||||
iconColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TooltipWithInfo = ({
|
|
||||||
iconSize = 14,
|
|
||||||
iconColor,
|
|
||||||
...props
|
|
||||||
}: TooltipWithInfoProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const defaultColor = theme.palette.text.secondary;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip {...props}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
cursor: "help",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Info
|
|
||||||
size={iconSize}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
style={{
|
|
||||||
opacity: 0.7,
|
|
||||||
color: iconColor || defaultColor,
|
|
||||||
transition: "opacity 0.2s ease",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.opacity = "1";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.opacity = "0.7";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export * from "./Breadcrumb";
|
|
||||||
export * from "./BasePage";
|
|
||||||
export * from "./Fallback";
|
|
||||||
export * from "./SkeletonCard";
|
|
||||||
export * from "./BulletPointCheck";
|
|
||||||
export * from "./StatusBox";
|
|
||||||
export * from "./BaseBox";
|
|
||||||
export * from "./Table";
|
|
||||||
export { DataTable as Table } from "./Table";
|
|
||||||
export * from "./StatusLabel";
|
|
||||||
export * from "./PulseDot";
|
|
||||||
export * from "./Dot";
|
|
||||||
export * from "./MonitorStatus";
|
|
||||||
export { default as Icon } from "./Icon";
|
|
||||||
export * from "./Tooltip";
|
|
||||||
export * from "./StatBox";
|
|
||||||
export * from "./BaseChart";
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import Autocomplete from "@mui/material/Autocomplete";
|
|
||||||
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
|
||||||
import { TextField, Checkbox } from "@/Components/v2/inputs";
|
|
||||||
import ListItem from "@mui/material/ListItem";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { FieldLabel } from "./FieldLabel";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
|
|
||||||
type AutoCompleteInputProps = Omit<
|
|
||||||
AutocompleteProps<any, boolean, boolean, boolean>,
|
|
||||||
"renderInput"
|
|
||||||
> & {
|
|
||||||
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
|
||||||
fieldLabel?: string;
|
|
||||||
required?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AutoCompleteInput = ({
|
|
||||||
fieldLabel,
|
|
||||||
required,
|
|
||||||
renderInput,
|
|
||||||
...props
|
|
||||||
}: AutoCompleteInputProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const multiple = props.multiple;
|
|
||||||
|
|
||||||
const defaultRenderInput = (params: any) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder="Type to search"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const autocomplete = (
|
|
||||||
<Autocomplete
|
|
||||||
{...props}
|
|
||||||
disableCloseOnSelect={!!multiple}
|
|
||||||
popupIcon={
|
|
||||||
<ChevronDown
|
|
||||||
size={18}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
style={{ marginRight: theme.spacing(3) }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
renderInput={renderInput || defaultRenderInput}
|
|
||||||
getOptionKey={(option) => option.id}
|
|
||||||
renderTags={() => null}
|
|
||||||
renderOption={(props, option, { selected }) => {
|
|
||||||
const { key, ...optionProps } = props;
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={key}
|
|
||||||
{...optionProps}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction={"row"}
|
|
||||||
alignItems={"center"}
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
{multiple && <Checkbox checked={selected} />}
|
|
||||||
{option.name}
|
|
||||||
</Stack>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
|
||||||
padding: `0 ${theme.spacing(5)}`,
|
|
||||||
},
|
|
||||||
"& .MuiInputBase-root .MuiAutocomplete-endAdornment": {
|
|
||||||
right: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldLabel) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={theme.spacing(2)}>
|
|
||||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
|
||||||
{autocomplete}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return autocomplete;
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import Button from "@mui/material/Button";
|
|
||||||
import type { ButtonProps } from "@mui/material/Button";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const outlinedSx =
|
|
||||||
props.variant === "outlined"
|
|
||||||
? {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
"&:hover": {
|
|
||||||
borderColor: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
height: 34,
|
|
||||||
fontWeight: 400,
|
|
||||||
borderRadius: 2,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "none",
|
|
||||||
"&:hover": {
|
|
||||||
boxShadow: "none",
|
|
||||||
},
|
|
||||||
...outlinedSx,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { forwardRef } from "react";
|
|
||||||
import Checkbox from "@mui/material/Checkbox";
|
|
||||||
import type { CheckboxProps } from "@mui/material/Checkbox";
|
|
||||||
import { Square, SquareCheck } from "lucide-react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
type CheckboxInputProps = CheckboxProps & {
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
export const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(
|
|
||||||
function CheckboxInput(
|
|
||||||
{ sx, ...props }: CheckboxInputProps,
|
|
||||||
ref: React.Ref<HTMLInputElement>
|
|
||||||
) {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
{...props}
|
|
||||||
aria-label={props.label}
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
ref: ref,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Square
|
|
||||||
size={16}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
checkedIcon={
|
|
||||||
<SquareCheck
|
|
||||||
size={16}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
"&.Mui-checked": {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
"&:hover": { backgroundColor: "transparent" },
|
|
||||||
"& svg": {
|
|
||||||
stroke: "currentColor",
|
|
||||||
},
|
|
||||||
"& svg path, & svg line, & svg polyline, & svg rect": {
|
|
||||||
stroke: "currentColor",
|
|
||||||
fill: "none",
|
|
||||||
},
|
|
||||||
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import { Button } from "@/Components/v2/inputs";
|
|
||||||
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export const DialogInput = ({
|
|
||||||
open,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
confirmColor = "primary",
|
|
||||||
cancelColor = "error",
|
|
||||||
loading = false,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
title?: string;
|
|
||||||
content?: string;
|
|
||||||
onConfirm?(item: any): any;
|
|
||||||
onCancel?(item: any): any;
|
|
||||||
confirmColor?: "error" | "primary";
|
|
||||||
cancelColor?: "error" | "primary";
|
|
||||||
loading?: boolean;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<Dialog open={open}>
|
|
||||||
<DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>{content}</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
loading={loading}
|
|
||||||
variant="contained"
|
|
||||||
color={cancelColor}
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
{t("common.buttons.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
loading={loading}
|
|
||||||
variant="contained"
|
|
||||||
color={confirmColor}
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
{t("common.buttons.confirm")}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
interface FieldLabelProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
required?: boolean;
|
|
||||||
htmlFor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
component="label"
|
|
||||||
htmlFor={htmlFor}
|
|
||||||
sx={{
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
display: "block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{required && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
marginLeft: theme.spacing(2),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import Select from "@mui/material/Select";
|
|
||||||
import React from "react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { SelectProps } from "@mui/material/Select";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { FieldLabel } from "./FieldLabel";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
|
|
||||||
interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
|
|
||||||
fieldLabel?: string;
|
|
||||||
required?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
placeholderColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SelectInput = <T,>({
|
|
||||||
fieldLabel,
|
|
||||||
required,
|
|
||||||
placeholder,
|
|
||||||
placeholderColor,
|
|
||||||
...props
|
|
||||||
}: SelectInputProps<T>) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
|
|
||||||
|
|
||||||
const renderValue = (selected: any) => {
|
|
||||||
const isMultiple = Boolean((props as any).multiple);
|
|
||||||
const isEmpty = isMultiple
|
|
||||||
? !Array.isArray(selected) || selected.length === 0
|
|
||||||
: selected === undefined || selected === null || selected === "";
|
|
||||||
|
|
||||||
if (isEmpty && placeholder) {
|
|
||||||
return <Typography sx={{ color: emptyPlaceholderColor }}>{placeholder}</Typography>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMultiple) {
|
|
||||||
const items: string[] = Array.isArray(selected) ? selected : [];
|
|
||||||
const capitalized = items.map(
|
|
||||||
(item) => item.charAt(0).toUpperCase() + item.slice(1)
|
|
||||||
);
|
|
||||||
return (<Typography>{capitalized.join(" | ")}</Typography>) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = React.Children.toArray(props.children as React.ReactNode);
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!React.isValidElement(node)) continue;
|
|
||||||
const el = node as React.ReactElement<{
|
|
||||||
value?: unknown;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}>;
|
|
||||||
if (el.props?.value === selected) {
|
|
||||||
return (el.props.children ?? selected) as React.ReactNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selected as React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const select = (
|
|
||||||
<Select<T>
|
|
||||||
{...props}
|
|
||||||
displayEmpty
|
|
||||||
renderValue={renderValue}
|
|
||||||
inputProps={{
|
|
||||||
...(props.inputProps || {}),
|
|
||||||
"aria-placeholder": placeholder,
|
|
||||||
}}
|
|
||||||
IconComponent={() => (
|
|
||||||
<ChevronDown
|
|
||||||
size={18}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
style={{ marginRight: theme.spacing(3) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
sx={{
|
|
||||||
height: "34px",
|
|
||||||
"& .MuiSelect-icon": {
|
|
||||||
right: theme.spacing(3),
|
|
||||||
},
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": {
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
...props.sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldLabel) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={theme.spacing(2)}>
|
|
||||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
|
||||||
{select}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return select;
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { forwardRef } from "react";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import type { TextFieldProps } from "@mui/material";
|
|
||||||
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { FieldLabel } from "./FieldLabel";
|
|
||||||
|
|
||||||
interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
|
||||||
fieldLabel?: string;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
|
||||||
{ fieldLabel, required, ...props },
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const input = (
|
|
||||||
<TextField
|
|
||||||
{...props}
|
|
||||||
inputRef={ref}
|
|
||||||
sx={{
|
|
||||||
"& .MuiOutlinedInput-root": {
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
height: 34,
|
|
||||||
fontSize: typographyLevels.base,
|
|
||||||
},
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
"& .MuiFormHelperText-root": {
|
|
||||||
marginLeft: 0,
|
|
||||||
marginRight: 0,
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldLabel) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={theme.spacing(2)}>
|
|
||||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
|
||||||
{input}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return input;
|
|
||||||
});
|
|
||||||
TextInput.displayName = "TextInput";
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import ToggleButton from "@mui/material/ToggleButton";
|
|
||||||
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
|
||||||
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
|
|
||||||
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
|
||||||
return (
|
|
||||||
<ToggleButton
|
|
||||||
{...props}
|
|
||||||
sx={{
|
|
||||||
px: 8,
|
|
||||||
textTransform: "none",
|
|
||||||
height: 34,
|
|
||||||
fontWeight: 400,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "none",
|
|
||||||
"&:hover": {
|
|
||||||
boxShadow: "none",
|
|
||||||
},
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<ToggleButtonGroup
|
|
||||||
{...props}
|
|
||||||
sx={{
|
|
||||||
"& .MuiToggleButtonGroup-grouped": {
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
|
|
||||||
borderRadius: 0,
|
|
||||||
"&:first-of-type": {
|
|
||||||
borderTopLeftRadius: 2,
|
|
||||||
borderBottomLeftRadius: 2,
|
|
||||||
},
|
|
||||||
"&:last-of-type": {
|
|
||||||
borderTopRightRadius: 2,
|
|
||||||
borderBottomRightRadius: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export { ButtonInput as Button } from "./Button";
|
|
||||||
export * from "./FieldLabel";
|
|
||||||
export { SelectInput as Select } from "./Select";
|
|
||||||
export { CheckboxInput as Checkbox } from "./Checkbox";
|
|
||||||
export { AutoCompleteInput as Autocomplete } from "./AutoComplete";
|
|
||||||
export { TextInput as TextField } from "./TextInput";
|
|
||||||
export {
|
|
||||||
ToggleButtonInput as ToggleButton,
|
|
||||||
ToggleButtonGroupInput as ToggleButtonGroup,
|
|
||||||
} from "./ToggleButton";
|
|
||||||
export { DialogInput as Dialog } from "./Dialog";
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { Select, Button } from "@/Components/v2/inputs";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
||||||
|
|
||||||
import type { MonitorType } from "@/Types/Monitor";
|
|
||||||
import { Typography, useTheme } from "@mui/material";
|
|
||||||
|
|
||||||
const types = ["http", "ping", "port", "docker"];
|
|
||||||
const statuses = ["up", "down"];
|
|
||||||
const states = ["active", "paused"];
|
|
||||||
|
|
||||||
export const ControlsFilter = ({
|
|
||||||
selectedTypes,
|
|
||||||
setSelectedTypes,
|
|
||||||
selectedStatus,
|
|
||||||
setSelectedStatus,
|
|
||||||
selectedState,
|
|
||||||
setSelectedState,
|
|
||||||
onClearFilters,
|
|
||||||
}: {
|
|
||||||
selectedTypes: MonitorType[];
|
|
||||||
setSelectedTypes: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
|
||||||
selectedStatus: string;
|
|
||||||
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
selectedState: string;
|
|
||||||
setSelectedState: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
onClearFilters: () => void;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
|
||||||
const isFilterActive =
|
|
||||||
selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== "";
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction={isSmall ? "column" : "row"}
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
multiple
|
|
||||||
placeholder="Type"
|
|
||||||
value={selectedTypes}
|
|
||||||
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
|
||||||
>
|
|
||||||
{types.map((type) => (
|
|
||||||
<MenuItem
|
|
||||||
key={type}
|
|
||||||
value={type}
|
|
||||||
>
|
|
||||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
placeholder="Status"
|
|
||||||
value={selectedStatus}
|
|
||||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
|
||||||
>
|
|
||||||
{statuses.map((status) => (
|
|
||||||
<MenuItem
|
|
||||||
key={status}
|
|
||||||
value={status}
|
|
||||||
>
|
|
||||||
<Typography textTransform={"capitalize"}>{status}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
placeholder="State"
|
|
||||||
value={selectedState}
|
|
||||||
onChange={(e) => setSelectedState(e.target.value)}
|
|
||||||
>
|
|
||||||
{states.map((state) => (
|
|
||||||
<MenuItem
|
|
||||||
key={state}
|
|
||||||
value={state}
|
|
||||||
>
|
|
||||||
<Typography textTransform={"capitalize"}>{state}</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{isFilterActive && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={onClearFilters}
|
|
||||||
>
|
|
||||||
Clear Filters
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { StatBox } from "@/Components/v2/design-elements";
|
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { MonitorStats, Monitor } from "@/Types/Monitor";
|
|
||||||
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
|
|
||||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface MonitorStatBoxesProps {
|
|
||||||
monitor?: Monitor;
|
|
||||||
monitorStats: MonitorStats | null;
|
|
||||||
certificateExpiry?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MonitorStatBoxes = ({
|
|
||||||
monitor,
|
|
||||||
monitorStats,
|
|
||||||
certificateExpiry,
|
|
||||||
}: MonitorStatBoxesProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
if (!monitorStats || !monitor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
|
|
||||||
const timeSinceLastFailure =
|
|
||||||
timeOfLastFailure > 0
|
|
||||||
? Date.now() - timeOfLastFailure
|
|
||||||
: Date.now() - new Date(monitorStats?.createdAt || 0).getTime();
|
|
||||||
|
|
||||||
// Determine time since last check
|
|
||||||
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
|
|
||||||
const timeSinceLastCheck = Date.now() - timeOfLastCheck;
|
|
||||||
|
|
||||||
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
|
|
||||||
|
|
||||||
const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck);
|
|
||||||
const palette = getStatusPalette(monitor?.status);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction={{ xs: "column", md: "row" }}
|
|
||||||
gap={theme.spacing(8)}
|
|
||||||
>
|
|
||||||
<StatBox
|
|
||||||
palette={palette}
|
|
||||||
title={t("pages.common.monitors.statBoxes.activeFor")}
|
|
||||||
subtitle={streakTime}
|
|
||||||
/>
|
|
||||||
<StatBox
|
|
||||||
title={t("pages.common.monitors.statBoxes.lastCheck")}
|
|
||||||
subtitle={lastCheckTime}
|
|
||||||
/>
|
|
||||||
<StatBox
|
|
||||||
title={t("pages.common.monitors.statBoxes.lastResponseTime")}
|
|
||||||
subtitle={monitorStats?.lastResponseTime + " ms"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{monitor?.type === "http" && (
|
|
||||||
<StatBox
|
|
||||||
title={t("pages.common.monitors.statBoxes.certificateExpiry")}
|
|
||||||
subtitle={certificateExpiry || "N/A"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { BaseChart, BaseBox } from "@/Components/v2/design-elements";
|
|
||||||
import { Clock } from "lucide-react";
|
|
||||||
import {
|
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
XAxis,
|
|
||||||
Tooltip,
|
|
||||||
CartesianGrid,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Text,
|
|
||||||
} from "recharts";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import {
|
|
||||||
formatDateWithTz,
|
|
||||||
tickDateFormatLookup,
|
|
||||||
tooltipDateFormatLookup,
|
|
||||||
} from "@/Utils/TimeUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { GroupedCheck } from "@/Types/Check";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
|
|
||||||
type XTickProps = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
payload: { value: any };
|
|
||||||
range: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const XTick = ({ x, y, payload, range }: XTickProps) => {
|
|
||||||
const format = tickDateFormatLookup(range);
|
|
||||||
const theme = useTheme();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
x={x}
|
|
||||||
y={y + 10}
|
|
||||||
textAnchor="middle"
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
fontSize={11}
|
|
||||||
fontWeight={400}
|
|
||||||
>
|
|
||||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResponseTimeToolTipProps = {
|
|
||||||
active?: boolean | undefined;
|
|
||||||
payload?: any[];
|
|
||||||
label?: string | number;
|
|
||||||
range: string;
|
|
||||||
theme: any;
|
|
||||||
uiTimezone: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResponseTimeToolTip = ({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
label,
|
|
||||||
range,
|
|
||||||
theme,
|
|
||||||
uiTimezone,
|
|
||||||
}: ResponseTimeToolTipProps) => {
|
|
||||||
if (!label) return null;
|
|
||||||
if (!payload) return null;
|
|
||||||
if (!active) return null;
|
|
||||||
|
|
||||||
const format = tooltipDateFormatLookup(range);
|
|
||||||
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
|
|
||||||
return (
|
|
||||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
|
||||||
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
|
|
||||||
<Typography>Response time: {responseTime} ms</Typography>
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HistogramDetails = ({
|
|
||||||
checks,
|
|
||||||
range,
|
|
||||||
}: {
|
|
||||||
checks: GroupedCheck[];
|
|
||||||
range: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={
|
|
||||||
<Clock
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Response times"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
height={300}
|
|
||||||
>
|
|
||||||
<AreaChart data={checks?.slice()}>
|
|
||||||
<CartesianGrid
|
|
||||||
stroke={theme.palette.divider}
|
|
||||||
strokeWidth={1}
|
|
||||||
strokeOpacity={1}
|
|
||||||
fill="transparent"
|
|
||||||
vertical={false}
|
|
||||||
/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="colorUv"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
stopColor={theme.palette.primary.main}
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
stopColor={theme.palette.primary.light}
|
|
||||||
stopOpacity={0}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<XAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
dataKey="bucketDate"
|
|
||||||
tick={(props) => (
|
|
||||||
<XTick
|
|
||||||
{...props}
|
|
||||||
range={range}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
content={(props) => (
|
|
||||||
<ResponseTimeToolTip
|
|
||||||
{...props}
|
|
||||||
range={range}
|
|
||||||
theme={theme}
|
|
||||||
uiTimezone={uiTimezone}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="avgResponseTime"
|
|
||||||
stroke={theme.palette.primary.main}
|
|
||||||
fill="url(#colorUv)"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
|
|
||||||
import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import type { MonitorStatus } from "@/Types/Monitor";
|
|
||||||
|
|
||||||
import { getStatusColor } from "@/Utils/MonitorUtils";
|
|
||||||
|
|
||||||
const processData = (checks: CheckSnapshot[]) => {
|
|
||||||
if (checks.length === 0) return [];
|
|
||||||
const formattedData: { score: number; date: string }[] = [];
|
|
||||||
|
|
||||||
const calculateScore = (entry: CheckSnapshot) => {
|
|
||||||
const accessibility = entry?.accessibility || 0;
|
|
||||||
const bestPractices = entry?.bestPractices || 0;
|
|
||||||
const performance = entry?.performance || 0;
|
|
||||||
const seo = entry?.seo || 0;
|
|
||||||
return Math.floor((accessibility + bestPractices + performance + seo) / 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
checks.forEach((entry) => {
|
|
||||||
const score = calculateScore(entry);
|
|
||||||
const date = entry.createdAt;
|
|
||||||
formattedData.push({ score, date });
|
|
||||||
});
|
|
||||||
|
|
||||||
return formattedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HistogramPageSpeed = ({
|
|
||||||
checks,
|
|
||||||
status,
|
|
||||||
}: {
|
|
||||||
checks: CheckSnapshot[];
|
|
||||||
status: MonitorStatus;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const scores = processData(checks ?? []);
|
|
||||||
const color = getStatusColor(status, theme);
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
flexWrap="nowrap"
|
|
||||||
gap={theme.spacing(1.5)}
|
|
||||||
height="100px"
|
|
||||||
maxWidth="400px"
|
|
||||||
width="100%"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
overflow={"hidden"}
|
|
||||||
sx={{
|
|
||||||
cursor: "default",
|
|
||||||
position: "relative",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
height={"100%"}
|
|
||||||
>
|
|
||||||
<AreaChart
|
|
||||||
data={scores}
|
|
||||||
margin={{ top: 10, bottom: -5 }}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<CartesianGrid
|
|
||||||
stroke={theme.palette.divider}
|
|
||||||
strokeWidth={1}
|
|
||||||
strokeOpacity={1}
|
|
||||||
fill="transparent"
|
|
||||||
vertical={false}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ stroke: theme.palette.divider }}
|
|
||||||
content={<HistogramPageSpeedTooltip />}
|
|
||||||
/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id={`pagespeed-chart-${status}`}
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
stopColor={color}
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
stopColor={color}
|
|
||||||
stopOpacity={0}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<Area
|
|
||||||
type="monotone"
|
|
||||||
dataKey="score"
|
|
||||||
stroke={color}
|
|
||||||
fill={`url(#pagespeed-chart-${status})`}
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import { BaseChart } from "@/Components/v2/design-elements";
|
|
||||||
import { TrendingUp } from "lucide-react";
|
|
||||||
import { XTick } from "@/Components/v2/monitors/";
|
|
||||||
import {
|
|
||||||
XAxis,
|
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
Tooltip,
|
|
||||||
CartesianGrid,
|
|
||||||
ResponsiveContainer,
|
|
||||||
} from "recharts";
|
|
||||||
import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors";
|
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import type { Palette } from "@mui/material/styles";
|
|
||||||
type PaletteColorKey = Extract<
|
|
||||||
keyof Palette,
|
|
||||||
"primary" | "error" | "success" | "warning"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ConfigItem {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
palette: PaletteColorKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: Record<string, ConfigItem> = {
|
|
||||||
seo: {
|
|
||||||
id: "seo",
|
|
||||||
text: "SEO",
|
|
||||||
palette: "primary",
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
id: "performance",
|
|
||||||
text: "performance",
|
|
||||||
palette: "success",
|
|
||||||
},
|
|
||||||
bestPractices: {
|
|
||||||
id: "bestPractices",
|
|
||||||
text: "best practices",
|
|
||||||
palette: "warning",
|
|
||||||
},
|
|
||||||
accessibility: {
|
|
||||||
id: "accessibility",
|
|
||||||
text: "accessibility",
|
|
||||||
palette: "error",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HistogramPageSpeedDetails = ({
|
|
||||||
checks,
|
|
||||||
range,
|
|
||||||
}: {
|
|
||||||
checks: CheckSnapshot[];
|
|
||||||
range: string;
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={
|
|
||||||
<TrendingUp
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Score history"
|
|
||||||
>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
minWidth={25}
|
|
||||||
height={215}
|
|
||||||
>
|
|
||||||
<AreaChart data={checks}>
|
|
||||||
<XAxis
|
|
||||||
dataKey={"createdAt"}
|
|
||||||
tick={(props) => (
|
|
||||||
<XTick
|
|
||||||
{...props}
|
|
||||||
range={range}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<CartesianGrid
|
|
||||||
stroke={theme.palette.divider}
|
|
||||||
strokeWidth={1}
|
|
||||||
strokeOpacity={1}
|
|
||||||
fill="transparent"
|
|
||||||
vertical={false}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ stroke: theme.palette.divider }}
|
|
||||||
content={<HistogramPageSpeedScoresTooltip config={config} />}
|
|
||||||
/>
|
|
||||||
<defs>
|
|
||||||
{Object.values(config).map(({ id, palette }) => {
|
|
||||||
const startColor = theme.palette[palette].main;
|
|
||||||
const endColor = theme.palette[palette].light;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<linearGradient
|
|
||||||
id={id}
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
stopColor={startColor}
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
stopColor={endColor}
|
|
||||||
stopOpacity={0}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</defs>
|
|
||||||
{Object.keys(config).map((key) => {
|
|
||||||
const { palette } = config[key];
|
|
||||||
const strokeColor = theme.palette[palette].main;
|
|
||||||
const bgColor = theme.palette.primary.main;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Area
|
|
||||||
connectNulls
|
|
||||||
key={key}
|
|
||||||
dataKey={key}
|
|
||||||
stackId={1}
|
|
||||||
stroke={strokeColor}
|
|
||||||
fill={`url(#${config[key].id})`}
|
|
||||||
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
|
||||||
import type { ConfigItem } from "@/Components/v2/monitors";
|
|
||||||
import type { TooltipProps } from "recharts";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
|
|
||||||
interface HistogramPageSpeedScoresTooltipProps
|
|
||||||
extends Partial<TooltipProps<number, string>> {
|
|
||||||
config: Record<string, ConfigItem>;
|
|
||||||
}
|
|
||||||
export const HistogramPageSpeedScoresTooltip = ({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
label,
|
|
||||||
config,
|
|
||||||
}: HistogramPageSpeedScoresTooltipProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
border: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
py: theme.spacing(2),
|
|
||||||
px: theme.spacing(4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
|
||||||
</Typography>
|
|
||||||
{Object.keys(config)
|
|
||||||
.reverse()
|
|
||||||
.map((key) => {
|
|
||||||
const { palette } = config[key];
|
|
||||||
const dotColor = theme.palette[palette].main;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
key={`${key}-tooltip`}
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(3)}
|
|
||||||
mt={theme.spacing(1)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
width={theme.spacing(4)}
|
|
||||||
height={theme.spacing(4)}
|
|
||||||
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
textTransform="capitalize"
|
|
||||||
sx={{ opacity: 0.8 }}
|
|
||||||
>
|
|
||||||
{config[key].text}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{Math.floor(payload[0].payload[key])}</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
|
||||||
import type { TooltipProps } from "recharts";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
|
|
||||||
export const HistogramPageSpeedTooltip = ({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
}: TooltipProps<number, string>) => {
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (!active || !payload || payload.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = payload[0]?.payload;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
alignItems="flex-start"
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
border: 1,
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
p: theme.spacing(4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography>
|
|
||||||
{formatDateWithTz(data?.date, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
|
||||||
</Typography>
|
|
||||||
<Typography>Score: {data?.score}</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { BaseChart } from "@/Components/v2/design-elements";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts";
|
|
||||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
|
||||||
import type { GroupedCheck } from "@/Types/Check";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
|
|
||||||
interface XLabelProps {
|
|
||||||
p1: GroupedCheck;
|
|
||||||
p2: GroupedCheck;
|
|
||||||
range: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const XLabel = ({ p1, p2, range }: XLabelProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<text
|
|
||||||
x={0}
|
|
||||||
y="100%"
|
|
||||||
dy={-3}
|
|
||||||
textAnchor="start"
|
|
||||||
fontSize={11}
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
>
|
|
||||||
{formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)}
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
x="100%"
|
|
||||||
y="100%"
|
|
||||||
dy={-3}
|
|
||||||
textAnchor="end"
|
|
||||||
fontSize={11}
|
|
||||||
fill={theme.palette.text.secondary}
|
|
||||||
>
|
|
||||||
{formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)}
|
|
||||||
</text>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HistogramStatusProps {
|
|
||||||
title: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
checks?: GroupedCheck[];
|
|
||||||
range: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HistogramStatus = ({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
checks = [],
|
|
||||||
range,
|
|
||||||
}: HistogramStatusProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const CustomTooltip = ({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
}: {
|
|
||||||
active?: boolean;
|
|
||||||
payload?: Array<{ payload: GroupedCheck }>;
|
|
||||||
}) => {
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
if (!active || !payload?.length) return null;
|
|
||||||
const d = payload[0]?.payload;
|
|
||||||
const avg = d?.avgResponseTime ?? 0;
|
|
||||||
const titleText = t("common.charts.labels.averageResponseTime");
|
|
||||||
const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A";
|
|
||||||
let dateLabel = "";
|
|
||||||
if (d?.bucketDate) {
|
|
||||||
const base = new Date(d.bucketDate);
|
|
||||||
const midBucket =
|
|
||||||
range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base;
|
|
||||||
dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
p: 1,
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
border: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dateLabel ? (
|
|
||||||
<Typography
|
|
||||||
sx={{ color: "text.secondary" }}
|
|
||||||
variant="caption"
|
|
||||||
>
|
|
||||||
{dateLabel}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<Typography variant="caption">{titleText}</Typography>
|
|
||||||
<Typography variant="body2">{Math.floor(avg)} ms</Typography>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={icon}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<Stack gap={theme.spacing(8)}>
|
|
||||||
<Stack
|
|
||||||
position="relative"
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Typography>Total checks: {totalChecks}</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
height={155}
|
|
||||||
>
|
|
||||||
<BarChart data={checks}>
|
|
||||||
<XAxis
|
|
||||||
stroke={theme.palette.divider}
|
|
||||||
height={15}
|
|
||||||
tick={false}
|
|
||||||
label={
|
|
||||||
<XLabel
|
|
||||||
p1={checks[0]}
|
|
||||||
p2={checks[checks.length - 1]}
|
|
||||||
range={range}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={false}
|
|
||||||
content={<CustomTooltip />}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="avgResponseTime"
|
|
||||||
maxBarSize={7}
|
|
||||||
background={{ fill: "transparent" }}
|
|
||||||
>
|
|
||||||
{checks?.map((groupedCheck, index) => {
|
|
||||||
const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
|
|
||||||
return (
|
|
||||||
<Cell
|
|
||||||
key={groupedCheck.bucketDate || `check-${index}`}
|
|
||||||
fill={theme.palette[fillColor].main}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Stack>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { BaseChart } from "@/Components/v2/design-elements";
|
|
||||||
import { FileText } from "lucide-react";
|
|
||||||
import type { CheckAudits, CheckSnapshot } from "@/Types/Check";
|
|
||||||
import { Pie, PieChart, ResponsiveContainer, Label } from "recharts";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { alpha } from "@mui/material/styles";
|
|
||||||
import { useState } from "react";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const CenterLabel = ({ viewBox, value }: any) => {
|
|
||||||
const { cx, cy } = viewBox;
|
|
||||||
return (
|
|
||||||
<foreignObject
|
|
||||||
x={cx - 25}
|
|
||||||
y={cy - 10}
|
|
||||||
width={50}
|
|
||||||
height={50}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</Typography>
|
|
||||||
</foreignObject>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PiePageSpeed = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [hoverTitle, setHoverTitle] = useState<string | null>(null);
|
|
||||||
|
|
||||||
if (!latestCheck) return null;
|
|
||||||
const LABELS: Record<string, string> = {
|
|
||||||
FCP: t("pages.pageSpeed.charts.common.fcp"),
|
|
||||||
SI: t("pages.pageSpeed.charts.common.si"),
|
|
||||||
LCP: t("pages.pageSpeed.charts.common.lcp"),
|
|
||||||
TBT: t("pages.pageSpeed.charts.common.tbt"),
|
|
||||||
CLS: t("pages.pageSpeed.charts.common.cls"),
|
|
||||||
};
|
|
||||||
const metrics: { key: keyof CheckAudits; color: string; weight: number }[] = [
|
|
||||||
{ key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 },
|
|
||||||
{ key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 },
|
|
||||||
{ key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 },
|
|
||||||
{ key: "tbt", color: alpha("#00AFFE", 0.6), weight: 0.3 },
|
|
||||||
{ key: "cls", color: alpha("#FF4181", 0.6), weight: 0.25 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const scores = metrics.flatMap(({ key, color, weight }) => {
|
|
||||||
const audit = latestCheck?.audits?.[key];
|
|
||||||
const val = Math.floor((audit?.score ?? 0) * 100);
|
|
||||||
const inverse = 100 - val;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: `${key.toUpperCase()} Inverse`,
|
|
||||||
value: inverse * weight,
|
|
||||||
fill: "transparent",
|
|
||||||
stroke: color,
|
|
||||||
weight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: key.toUpperCase(),
|
|
||||||
value: val * weight,
|
|
||||||
fill: color,
|
|
||||||
stroke: color,
|
|
||||||
weight,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalScore =
|
|
||||||
(latestCheck.audits?.fcp?.score || 0) * 0.1 +
|
|
||||||
(latestCheck.audits?.si?.score || 0) * 0.1 +
|
|
||||||
(latestCheck.audits?.lcp?.score || 0) * 0.25 +
|
|
||||||
(latestCheck.audits?.tbt?.score || 0) * 0.3 +
|
|
||||||
(latestCheck.audits?.cls?.score || 0) * 0.25;
|
|
||||||
const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100));
|
|
||||||
|
|
||||||
const score = [
|
|
||||||
{
|
|
||||||
value: 100,
|
|
||||||
fill: alpha(theme.palette[pageSpeedPalette].light || "#ffffff", 0.6),
|
|
||||||
stroke: "none",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={
|
|
||||||
<FileText
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={t("pages.pageSpeed.charts.pie.title")}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
open={Boolean(hoverTitle)}
|
|
||||||
title={hoverTitle || ""}
|
|
||||||
arrow
|
|
||||||
followCursor
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
"& .recharts-wrapper:focus, & .recharts-surface:focus": {
|
|
||||||
outline: "none",
|
|
||||||
},
|
|
||||||
"& .recharts-wrapper *:focus": { outline: "none" },
|
|
||||||
"& svg:focus, & g:focus, & path:focus": { outline: "none" },
|
|
||||||
position: "relative",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
height={250}
|
|
||||||
>
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={score}
|
|
||||||
dataKey="value"
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
outerRadius="65%"
|
|
||||||
>
|
|
||||||
<Label
|
|
||||||
position={"center"}
|
|
||||||
content={<CenterLabel value={Math.floor(totalScore * 100)} />}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
<Pie
|
|
||||||
data={scores}
|
|
||||||
innerRadius="70%"
|
|
||||||
outerRadius="80%"
|
|
||||||
label={({ name, value }) => `${name}: ${Math.round(value)}`}
|
|
||||||
dataKey="value"
|
|
||||||
stroke="none"
|
|
||||||
onMouseEnter={(_, index: number) => {
|
|
||||||
const d = scores[index];
|
|
||||||
if (!d) return;
|
|
||||||
const isInverse = String(d.name).includes("Inverse");
|
|
||||||
const pair = isInverse && scores[index + 1] ? scores[index + 1] : d;
|
|
||||||
const base = String(pair.name).replace(" Inverse", "");
|
|
||||||
const full = LABELS[base] ?? base;
|
|
||||||
const displayVal = Math.round(Number(pair.value) || 0);
|
|
||||||
setHoverTitle(`${full}: ${displayVal}`);
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => setHoverTitle(null)}
|
|
||||||
/>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { BarChart3 } from "lucide-react";
|
|
||||||
import { BaseChart } from "@/Components/v2/design-elements";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
|
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { getPageSpeedPalette } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
const MetricBox = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
weight,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
weight: number;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const palette = getPageSpeedPalette(value);
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction={"row"}
|
|
||||||
sx={{
|
|
||||||
border: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
flex={1}
|
|
||||||
p={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
<Typography textTransform={"uppercase"}>{label}</Typography>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent={"space-between"}
|
|
||||||
>
|
|
||||||
<Typography>{`${value}%`}</Typography>
|
|
||||||
<Typography>{`${t("pages.pageSpeed.charts.legend.weight")}: ${weight}%`}</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<Box
|
|
||||||
width={4}
|
|
||||||
bgcolor={theme.palette[palette].light}
|
|
||||||
sx={{
|
|
||||||
borderTopRightRadius: theme.shape.borderRadius,
|
|
||||||
borderBottomRightRadius: theme.shape.borderRadius,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: CheckSnapshot }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!latestCheck) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={
|
|
||||||
<BarChart3
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={t("pages.pageSpeed.charts.legend.title")}
|
|
||||||
>
|
|
||||||
<Stack gap={theme.spacing(4)}>
|
|
||||||
<MetricBox
|
|
||||||
label={t("pages.pageSpeed.charts.common.si")}
|
|
||||||
value={Math.floor((latestCheck.audits?.si?.score || 0) * 100)}
|
|
||||||
weight={10}
|
|
||||||
/>
|
|
||||||
<MetricBox
|
|
||||||
label={t("pages.pageSpeed.charts.common.fcp")}
|
|
||||||
value={Math.floor((latestCheck.audits?.fcp?.score || 0) * 100)}
|
|
||||||
weight={10}
|
|
||||||
/>
|
|
||||||
<MetricBox
|
|
||||||
label={t("pages.pageSpeed.charts.common.cls")}
|
|
||||||
value={Math.floor((latestCheck.audits?.cls?.score || 0) * 100)}
|
|
||||||
weight={25}
|
|
||||||
/>
|
|
||||||
<MetricBox
|
|
||||||
label={t("pages.pageSpeed.charts.common.tbt")}
|
|
||||||
value={Math.floor((latestCheck.audits?.tbt?.score || 0) * 100)}
|
|
||||||
weight={30}
|
|
||||||
/>
|
|
||||||
<MetricBox
|
|
||||||
label={t("pages.pageSpeed.charts.common.lcp")}
|
|
||||||
value={Math.floor((latestCheck.audits?.lcp?.score || 0) * 100)}
|
|
||||||
weight={25}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { BaseChart } from "@/Components/v2/design-elements";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { Gauge } from "lucide-react";
|
|
||||||
import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
|
|
||||||
export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const chartData = [
|
|
||||||
{ name: "max", value: max - avg, color: "transparent" },
|
|
||||||
{ name: "avg", value: avg, color: "red" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const palette = getResponseTimeColor(avg);
|
|
||||||
const msg: Record<string, string> = {
|
|
||||||
success: "Excellent",
|
|
||||||
warning: "Average",
|
|
||||||
danger: "Poor",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseChart
|
|
||||||
icon={
|
|
||||||
<Gauge
|
|
||||||
size={20}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={t("common.charts.labels.averageResponseTime")}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
height="100%"
|
|
||||||
position={"relative"}
|
|
||||||
justifyContent={"flex-end"}
|
|
||||||
>
|
|
||||||
<ResponsiveContainer
|
|
||||||
width="100%"
|
|
||||||
height={155}
|
|
||||||
>
|
|
||||||
<RadialBarChart
|
|
||||||
cy="100%"
|
|
||||||
data={chartData}
|
|
||||||
startAngle={180}
|
|
||||||
endAngle={0}
|
|
||||||
innerRadius={"120%"}
|
|
||||||
outerRadius={"200%"}
|
|
||||||
>
|
|
||||||
<RadialBar
|
|
||||||
dataKey="value"
|
|
||||||
background={{ fill: theme.palette[palette].light }}
|
|
||||||
>
|
|
||||||
<Cell visibility={"hidden"} />
|
|
||||||
<Cell fill={theme.palette[palette].main} />
|
|
||||||
</RadialBar>
|
|
||||||
</RadialBarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
<Stack
|
|
||||||
direction={"row"}
|
|
||||||
justifyContent={"space-between"}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">{t("common.charts.labels.low")}</Typography>
|
|
||||||
<Typography variant="body2">{t("common.charts.labels.high")}</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Stack
|
|
||||||
position="absolute"
|
|
||||||
top={"50%"}
|
|
||||||
right={"50%"}
|
|
||||||
sx={{
|
|
||||||
transform: "translate(50%, 0%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
textAlign={"center"}
|
|
||||||
>
|
|
||||||
{msg[palette]}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
textAlign={"center"}
|
|
||||||
>{`${avg?.toFixed()}ms`}</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</BaseChart>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export * from "./ControlsFilter";
|
|
||||||
export * from "./MonitorStatBoxes";
|
|
||||||
export * from "./charts/HistogramStatus";
|
|
||||||
export * from "./charts/RadialAvgResponse";
|
|
||||||
export * from "./charts/HistogramDetails";
|
|
||||||
export * from "./charts/HistogramPageSpeed";
|
|
||||||
export * from "./charts/HistogramPageSpeedTooltip";
|
|
||||||
export * from "./charts/PiePageSpeed";
|
|
||||||
export * from "./charts/PiePageSpeedLegend";
|
|
||||||
export * from "./charts/HistogramPageSpeedDetails";
|
|
||||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
|
||||||
@@ -5,29 +5,27 @@ import axios from "axios";
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
authToken: "",
|
authToken: "",
|
||||||
|
refreshToken: "",
|
||||||
user: "",
|
user: "",
|
||||||
success: null,
|
success: null,
|
||||||
msg: null,
|
msg: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const register = createAsyncThunk(
|
export const register = createAsyncThunk("auth/register", async (form, thunkApi) => {
|
||||||
"auth/register",
|
try {
|
||||||
async ({ user, token }, thunkApi) => {
|
const res = await networkService.registerUser(form);
|
||||||
try {
|
return res.data;
|
||||||
const res = await networkService.registerUser(user, token);
|
} catch (error) {
|
||||||
return res.data;
|
if (error.response.data) {
|
||||||
} catch (error) {
|
return thunkApi.rejectWithValue(error.response.data);
|
||||||
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) => {
|
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||||
try {
|
try {
|
||||||
@@ -141,6 +139,7 @@ const handleAuthFulfilled = (state, action) => {
|
|||||||
state.success = action.payload.success;
|
state.success = action.payload.success;
|
||||||
state.msg = action.payload.msg;
|
state.msg = action.payload.msg;
|
||||||
state.authToken = action.payload.data.token;
|
state.authToken = action.payload.data.token;
|
||||||
|
state.refreshToken = action.payload.data.refreshToken || "";
|
||||||
state.user = action.payload.data.user;
|
state.user = action.payload.data.user;
|
||||||
};
|
};
|
||||||
const handleAuthRejected = (state, action) => {
|
const handleAuthRejected = (state, action) => {
|
||||||
@@ -191,11 +190,15 @@ const authSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
clearAuthState: (state) => {
|
clearAuthState: (state) => {
|
||||||
state.authToken = "";
|
state.authToken = "";
|
||||||
|
state.refreshToken = "";
|
||||||
state.user = "";
|
state.user = "";
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.success = true;
|
state.success = true;
|
||||||
state.msg = "Logged out successfully";
|
state.msg = "Logged out successfully";
|
||||||
},
|
},
|
||||||
|
setAuthToken: (state, action) => {
|
||||||
|
state.authToken = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// Register thunk
|
// Register thunk
|
||||||
@@ -249,4 +252,4 @@ const authSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default authSlice.reducer;
|
export default authSlice.reducer;
|
||||||
export const { clearAuthState } = authSlice.actions;
|
export const { clearAuthState, setAuthToken } = authSlice.actions;
|
||||||
@@ -29,7 +29,6 @@ const initialState = {
|
|||||||
distributedUptimeEnabled: false,
|
distributedUptimeEnabled: false,
|
||||||
language: "en",
|
language: "en",
|
||||||
starPromptOpen: true,
|
starPromptOpen: true,
|
||||||
chartType: "histogram",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const uiSlice = createSlice({
|
const uiSlice = createSlice({
|
||||||
@@ -74,9 +73,6 @@ const uiSlice = createSlice({
|
|||||||
setStarPromptOpen: (state, action) => {
|
setStarPromptOpen: (state, action) => {
|
||||||
state.starPromptOpen = action.payload;
|
state.starPromptOpen = action.payload;
|
||||||
},
|
},
|
||||||
setChartType: (state, action) => {
|
|
||||||
state.chartType = action.payload;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,5 +88,4 @@ export const {
|
|||||||
setDistributedUptimeEnabled,
|
setDistributedUptimeEnabled,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
setStarPromptOpen,
|
setStarPromptOpen,
|
||||||
setChartType,
|
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import type { SWRConfiguration } from "swr";
|
|
||||||
import type { AxiosRequestConfig } from "axios";
|
|
||||||
import { get, patch, post, deleteOp } from "@/Utils/ApiClient";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useToast } from "@/Hooks/UseToast";
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
|
||||||
success: boolean;
|
|
||||||
msg: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetcher = async <T>(url: string, config?: AxiosRequestConfig) => {
|
|
||||||
const res = await get<ApiResponse<T>>(url, config);
|
|
||||||
return res.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGet = <T>(
|
|
||||||
url: string | null,
|
|
||||||
axiosConfig?: AxiosRequestConfig,
|
|
||||||
swrConfig?: SWRConfiguration
|
|
||||||
) => {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR<ApiResponse<T>>(
|
|
||||||
url,
|
|
||||||
(url: string) => fetcher<T>(url, axiosConfig),
|
|
||||||
swrConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: data?.data ?? null,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
refetch: mutate,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePost = <B = any, R = any>() => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { toastError, toastSuccess } = useToast();
|
|
||||||
|
|
||||||
const postFn = async (
|
|
||||||
endpoint: string,
|
|
||||||
body: B,
|
|
||||||
config?: AxiosRequestConfig
|
|
||||||
): Promise<ApiResponse<R> | null> => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await post<ApiResponse<R>>(endpoint, body, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toastSuccess(res.data?.msg || "Operation successful");
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
|
||||||
console.error(err);
|
|
||||||
toastError(errMsg);
|
|
||||||
setError(errMsg);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { post: postFn, loading, error };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePatch = <B = any, R = any>() => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { toastError, toastSuccess } = useToast();
|
|
||||||
|
|
||||||
const patchFn = async (
|
|
||||||
endpoint: string,
|
|
||||||
body?: B,
|
|
||||||
config?: AxiosRequestConfig
|
|
||||||
): Promise<ApiResponse<R> | null> => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await patch<ApiResponse<R>>(endpoint, body, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toastSuccess(res.data?.msg || "Operation successful");
|
|
||||||
return res.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
|
||||||
toastError(errMsg);
|
|
||||||
setError(errMsg);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { patch: patchFn, loading, error };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDelete = <R = any>() => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { toastError, toastSuccess } = useToast();
|
|
||||||
|
|
||||||
const deleteFn = async (
|
|
||||||
endpoint: string,
|
|
||||||
config?: AxiosRequestConfig
|
|
||||||
): Promise<ApiResponse<R> | null> => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await deleteOp<ApiResponse<R>>(endpoint, {
|
|
||||||
...config,
|
|
||||||
headers: {
|
|
||||||
...config?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toastSuccess(res.data?.msg || "Operation successful");
|
|
||||||
return res.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
|
|
||||||
toastError(errMsg);
|
|
||||||
setError(errMsg);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { deleteFn, loading, error };
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { toast, type ToastOptions } from "react-toastify";
|
|
||||||
|
|
||||||
export const useToast = () => {
|
|
||||||
const showToast = (message: string, options?: ToastOptions) => {
|
|
||||||
toast.dismiss();
|
|
||||||
const baseStyle: React.CSSProperties = {
|
|
||||||
whiteSpace: "pre-line",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
};
|
|
||||||
toast(message, {
|
|
||||||
...options,
|
|
||||||
style: { ...baseStyle, ...(options?.style || {}) },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toastSuccess = (msg: string, opts?: ToastOptions) =>
|
|
||||||
showToast(msg, { ...opts, type: "success" });
|
|
||||||
const toastError = (msg: string, opts?: ToastOptions) =>
|
|
||||||
showToast(msg, { ...opts, type: "error" });
|
|
||||||
const toastInfo = (msg: string, opts?: ToastOptions) =>
|
|
||||||
showToast(msg, { ...opts, type: "info" });
|
|
||||||
|
|
||||||
return { showToast, toastSuccess, toastError, toastInfo };
|
|
||||||
};
|
|
||||||
@@ -137,7 +137,7 @@ const useFetchChecksByMonitor = ({
|
|||||||
return [checks, checksCount, isLoading, networkError];
|
return [checks, checksCount, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => {
|
const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
|
||||||
const [summary, setSummary] = useState(undefined);
|
const [summary, setSummary] = useState(undefined);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
@@ -147,7 +147,7 @@ const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const res = await networkService.getChecksAndSummaryByTeamId({ dateRange });
|
const res = await networkService.getChecksAndSummaryByTeamId();
|
||||||
setSummary(res.data.data);
|
setSummary(res.data.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNetworkError(true);
|
setNetworkError(true);
|
||||||
@@ -158,7 +158,7 @@ const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchSummary();
|
fetchSummary();
|
||||||
}, [dateRange, updateTrigger]);
|
}, [updateTrigger]);
|
||||||
|
|
||||||
return [summary, isLoading, networkError];
|
return [summary, isLoading, networkError];
|
||||||
};
|
};
|
||||||
@@ -185,9 +185,36 @@ const useResolveIncident = () => {
|
|||||||
return [resolveIncident, isLoading];
|
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 {
|
export {
|
||||||
useFetchChecksByMonitor,
|
useFetchChecksByMonitor,
|
||||||
useFetchChecksTeam,
|
useFetchChecksTeam,
|
||||||
useFetchChecksSummaryByTeamId,
|
useFetchChecksSummaryByTeamId,
|
||||||
useResolveIncident,
|
useResolveIncident,
|
||||||
|
useAcknowledgeChecks,
|
||||||
};
|
};
|
||||||
@@ -243,7 +243,6 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
const [monitor, setMonitor] = useState(undefined);
|
const [monitor, setMonitor] = useState(undefined);
|
||||||
const [monitorStats, setMonitorStats] = useState(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMonitor = async () => {
|
const fetchMonitor = async () => {
|
||||||
@@ -255,8 +254,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
|
|||||||
monitorId: monitorId,
|
monitorId: monitorId,
|
||||||
dateRange: dateRange,
|
dateRange: dateRange,
|
||||||
});
|
});
|
||||||
setMonitor(response.data.data.monitor);
|
setMonitor(response.data.data);
|
||||||
setMonitorStats(response.data.data.monitorStats);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNetworkError(true);
|
setNetworkError(true);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -265,7 +263,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg
|
|||||||
};
|
};
|
||||||
fetchMonitor();
|
fetchMonitor();
|
||||||
}, [monitorId, dateRange, updateTrigger]);
|
}, [monitorId, dateRange, updateTrigger]);
|
||||||
return [monitor, monitorStats, isLoading, networkError];
|
return [monitor, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||||
@@ -344,6 +342,7 @@ export const useDeleteMonitor = () => {
|
|||||||
const deleteMonitor = async ({ monitor, redirect }) => {
|
const deleteMonitor = async ({ monitor, redirect }) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
console.log(monitor);
|
||||||
await networkService.deleteMonitorById({ monitorId: monitor.id });
|
await networkService.deleteMonitorById({ monitorId: monitor.id });
|
||||||
createToast({ body: "Monitor deleted successfully!" });
|
createToast({ body: "Monitor deleted successfully!" });
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const useGetNotificationById = (id, setNotification) => {
|
|||||||
const notificationData = {
|
const notificationData = {
|
||||||
address: notification?.address,
|
address: notification?.address,
|
||||||
notificationName: notification?.notificationName,
|
notificationName: notification?.notificationName,
|
||||||
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id,
|
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
|
||||||
};
|
};
|
||||||
|
|
||||||
setNotification(notificationData);
|
setNotification(notificationData);
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
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 Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import Icon from "@/Components/v1/Icon";
|
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Proptypes from "prop-types";
|
import Proptypes from "prop-types";
|
||||||
|
|
||||||
@@ -24,13 +24,7 @@ const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="accent"
|
color="accent"
|
||||||
endIcon={
|
endIcon={<ArrowDropDownIcon sx={{ color: theme.palette.secondary.light }} />}
|
||||||
<Icon
|
|
||||||
name="ChevronDown"
|
|
||||||
size={20}
|
|
||||||
color="secondary.light"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{t("teamPanel.addTeamMember.addMemberMenu")}
|
{t("teamPanel.addTeamMember.addMemberMenu")}
|
||||||
|
|||||||
@@ -167,11 +167,7 @@ const PasswordPanel = () => {
|
|||||||
value={localData.password}
|
value={localData.password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors[idToName["edit-current-password"]] ? true : false}
|
error={errors[idToName["edit-current-password"]] ? true : false}
|
||||||
helperText={
|
helperText={errors[idToName["edit-current-password"]]}
|
||||||
errors[idToName["edit-current-password"]]
|
|
||||||
? t(errors[idToName["edit-current-password"]])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
endAdornment={<PasswordEndAdornment />}
|
endAdornment={<PasswordEndAdornment />}
|
||||||
flex={1}
|
flex={1}
|
||||||
/>
|
/>
|
||||||
@@ -197,11 +193,7 @@ const PasswordPanel = () => {
|
|||||||
value={localData.newPassword}
|
value={localData.newPassword}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors[idToName["edit-new-password"]] ? true : false}
|
error={errors[idToName["edit-new-password"]] ? true : false}
|
||||||
helperText={
|
helperText={errors[idToName["edit-new-password"]]}
|
||||||
errors[idToName["edit-new-password"]]
|
|
||||||
? t(errors[idToName["edit-new-password"]])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
endAdornment={<PasswordEndAdornment />}
|
endAdornment={<PasswordEndAdornment />}
|
||||||
flex={1}
|
flex={1}
|
||||||
/>
|
/>
|
||||||
@@ -227,11 +219,7 @@ const PasswordPanel = () => {
|
|||||||
value={localData.confirm}
|
value={localData.confirm}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors[idToName["edit-confirm-password"]] ? true : false}
|
error={errors[idToName["edit-confirm-password"]] ? true : false}
|
||||||
helperText={
|
helperText={errors[idToName["edit-confirm-password"]]}
|
||||||
errors[idToName["edit-confirm-password"]]
|
|
||||||
? t(errors[idToName["edit-confirm-password"]])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
endAdornment={<PasswordEndAdornment />}
|
endAdornment={<PasswordEndAdornment />}
|
||||||
flex={1}
|
flex={1}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -210,11 +210,7 @@ const ProfilePanel = () => {
|
|||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors[idToName["edit-first-name"]] ? true : false}
|
error={errors[idToName["edit-first-name"]] ? true : false}
|
||||||
helperText={
|
helperText={errors[idToName["edit-first-name"]]}
|
||||||
errors[idToName["edit-first-name"]]
|
|
||||||
? t(errors[idToName["edit-first-name"]])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
flex={1}
|
flex={1}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -232,11 +228,7 @@ const ProfilePanel = () => {
|
|||||||
value={localData.lastName}
|
value={localData.lastName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors[idToName["edit-last-name"]] ? true : false}
|
error={errors[idToName["edit-last-name"]] ? true : false}
|
||||||
helperText={
|
helperText={errors[idToName["edit-last-name"]]}
|
||||||
errors[idToName["edit-last-name"]]
|
|
||||||
? t(errors[idToName["edit-last-name"]])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
flex={1}
|
flex={1}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||||
import Icon from "@/Components/v1/Icon";
|
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ROLES } from "../../../../Utils/roleUtils.js";
|
import { ROLES } from "../../../../Utils/roleUtils.js";
|
||||||
@@ -21,13 +21,11 @@ const RoleTable = ({ roles, handleDeleteRole }) => {
|
|||||||
render: (row) => {
|
render: (row) => {
|
||||||
if (row === ROLES.SUPERADMIN) return null;
|
if (row === ROLES.SUPERADMIN) return null;
|
||||||
return (
|
return (
|
||||||
<Icon
|
<DeleteOutlineRoundedIcon
|
||||||
name="Trash2"
|
|
||||||
size={20}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteRole(row);
|
handleDeleteRole(row);
|
||||||
}}
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const TeamPanel = () => {
|
|||||||
|
|
||||||
team = team.map((member) => ({
|
team = team.map((member) => ({
|
||||||
...member,
|
...member,
|
||||||
id: member.id,
|
id: member._id,
|
||||||
role: member.role.map((role) => ROLE_MAP[role]).join(","),
|
role: member.role.map((role) => ROLE_MAP[role]).join(","),
|
||||||
}));
|
}));
|
||||||
setData(team);
|
setData(team);
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { createToast } from "../../Utils/toastUtils.jsx";
|
|||||||
import { forgotPassword } from "../../Features/Auth/authSlice.js";
|
import { forgotPassword } from "../../Features/Auth/authSlice.js";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import Background from "@/assets/Images/background-grid.svg?react";
|
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 Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||||
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
import IconBox from "@/Components/v1/IconBox/index.jsx";
|
||||||
import Icon from "@/Components/v1/Icon";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const CheckEmail = () => {
|
const CheckEmail = () => {
|
||||||
@@ -160,7 +160,7 @@ const CheckEmail = () => {
|
|||||||
svgHeight={24}
|
svgHeight={24}
|
||||||
mb={theme.spacing(4)}
|
mb={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Icon name="Mail" />
|
<EmailIcon alt={t("auth.forgotPassword.imageAlts.email")} />
|
||||||
</IconBox>
|
</IconBox>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
|
||||||
|
|||||||
Loaded 100 of 473 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user