Compare commits

...
Author SHA1 Message Date
gorkem-bwl 35ee4d3bcb revert: Remove PageSpeed stats fix (already fixed on develop)
The PageSpeed stats fix has been implemented differently on develop branch
by Alex Holliday in commit 6447c315c. Reverting this change to avoid
conflicts and keep the incidents fixes.
2026-01-16 21:53:48 -05:00
gorkem-bwl 162b081858 Fix formatting 2026-01-16 15:26:34 -05:00
gorkem-bwl 6c0aea46a7 Fix PageSpeed stats and incidents page issues
## Changes
- Fix PageSpeed "checks since" and "last check" always showing "0s ago"
  by calculating durations from the checks array timestamps
- Fix incidents "Avg. Resolution Time" displaying decimal hours (e.g., "0.86 hours")
  by adding formatResolutionTime() to show proper format (e.g., "52m" or "2h 15m")
- Fix incidents monitor filter dropdown not working when selecting a specific monitor
  by resolving race condition between page reset and data fetch
2026-01-16 15:25:05 -05:00
Gorkem Cetin (BWL) f1a6afb860 Merge pull request #3133 from bluewave-labs/fix/ui-improvements-logs-incidents
Improve UI consistency for Logs and Incidents pages
2026-01-16 14:47:21 -05:00
gorkem-bwl 5c52a208b9 Fix build errors and formatting
- Replace CheckIcon SVG import with Lucide Icon component in
  LatestIncidentsPanel/index.jsx
- Format IncidentItem.jsx and StatisticsPanel.jsx with Prettier
2026-01-16 14:44:28 -05:00
gorkem-bwl 4fdc0bde5c Merge develop into fix/ui-improvements-logs-incidents
Resolve conflicts in IncidentItem.jsx and StatisticsPanel.jsx by
keeping the UI improvements from this branch while adopting the
new Lucide Icon component from develop (replacing old SVG imports).
2026-01-16 14:42:29 -05:00
Alexander Holliday 3d4ebe9588 Merge pull request #3143 from bluewave-labs/fix/responsive-layout-overflow
Fix horizontal overflow causing white space on narrow screens
2026-01-16 11:18:02 -08:00
gorkem-bwl 62ea7b0e5b Fix horizontal overflow causing white space on narrow screens
Add overflow-x: hidden and width: 100% to .home-layout to prevent
horizontal scrollbar. Add min-width: 0 to .home-content-wrapper to
allow flexbox child to shrink below content size at narrow widths.
2026-01-16 14:09:37 -05:00
Alexander Holliday 8747c38f40 Merge pull request #3142 from bluewave-labs/fix/infra-config
infra fix
2026-01-16 10:38:57 -08:00
Alex Holliday d633fc5ccb infra fix 2026-01-16 18:37:14 +00:00
Alexander Holliday 0d7a35367f Merge pull request #3141 from bluewave-labs/feat/job-queue-ts
feat/job queue ts
2026-01-16 10:25:40 -08:00
Alex Holliday af141ec772 build fix 2026-01-16 18:25:28 +00:00
Alex Holliday 5784fec3d1 format 2026-01-16 18:17:55 +00:00
Alex Holliday 95cd33ac06 queue and buffer refactor 2026-01-16 18:11:04 +00:00
Gorkem Cetin (BWL) 2a591ee503 Merge pull request #3139 from bluewave-labs/feat/lucide-icon-migration
Migrate icons from custom SVGs and MUI to Lucide React
2026-01-16 12:50:21 -05:00
gorkem-bwl f5aa24d77e fix: rename Image import to ImageIcon to avoid conflicts 2026-01-16 07:34:03 -05:00
gorkem-bwl 7874439e4c fix: address PR review feedback for icon migration
- Use AlertCircle for error and AlertTriangle for warning icons
- Use Icon color prop instead of CSS fill for Lucide stroke-based icons
- Replace wildcard import with explicit named imports for tree-shaking
- Replace sx prop with style prop on Icon components
- Add Trash2 to Icon component imports
2026-01-16 07:31:23 -05:00
gorkem-bwl 7140d436a5 style: apply Prettier formatting 2026-01-15 23:39:26 -05:00
gorkem-bwl ba0c89fc6c feat: migrate icons from custom SVGs and MUI to Lucide React
Replace custom SVG icons and @mui/icons-material with lucide-react for consistent,
lightweight icon management across the application.

## Changes
- Add lucide-react package and remove @mui/icons-material
- Create theme-aware Icon wrapper component with color resolution
- Create iconMap.js for legacy icon name mappings
- Migrate 64 components to use new Icon component
- Remove 74 unused SVG files from assets/icons/

## Benefits
- Consistent icon API across the application
- Smaller bundle size with tree-shakeable imports
- Theme-aware color support via MUI theme paths
- Simpler maintenance with single icon library
2026-01-15 23:37:50 -05:00
Alexander Holliday b5caabed20 Merge pull request #3135 from bluewave-labs/fix/pagespeed-monitor-delete
Fix pagespeed monitor deletion failing with undefined ID
2026-01-15 18:29:34 -08:00
gorkem-bwl 8814596d6e Fix pagespeed monitor deletion failing with undefined ID
The delete handler was using monitor._id from state, which was undefined
because the fetched monitor object didn't have _id populated. Changed to
use monitorId directly from URL params, which is already available and
used by other handlers in the same component (e.g., handlePause).

Fixes "Failed to delete monitor" error when removing pagespeed monitors.
2026-01-15 21:27:37 -05:00
gorkem-bwl c60d07c3a0 Improve Latest Incidents panel layout
- Simplify incident rows to single line with grid layout
- Remove redundant "Monitor:" and "Status:" labels
- Align columns vertically (monitor name, status badge, duration)
- Center-align status badge column
- Match icon styling with General Statistics (18px, strokeWidth 1.5)
- Remove pointer cursor (click not implemented)
- Add dividers and minHeight to Statistics panel for alignment
2026-01-15 21:19:34 -05:00
gorkem-bwl 54ecf02d9a Fix Prettier formatting in StatisticsPanel 2026-01-15 20:48:15 -05:00
gorkem-bwl 1ab31d19f3 Improve UI consistency for Logs and Incidents pages
## Changes

### Logs page
- Convert server logs from monospace text to DataTable with pagination
- Add table columns: Timestamp, Level, Service, Method, Message
- Add colored level badges (info=green, warn=yellow, error=red, debug=blue)
- Add pagination with configurable rows per page
- Align diagnostics gauge cards with 16px gap and responsive widths

### Incidents page
- Redesign summary cards with consistent styling
- Remove shadows and use 1px border matching other sections
- Reduce icon sizes to 18-24px (sidebar-style)
- Reduce font sizes (13px titles, 18-32px values)
- Add titles to all three summary cards
- Fix "Most Affected Monitor" showing "Unknown Monitor" when no incidents
- Show "N/A" for avg resolution time when no incidents
- Remove cluttered "Resolutions" progress bar section
- Remove horizontal dividers between statistics rows
- Standardize card padding and gap to 16px

### Uptime page
- Fix oversized monitor names in table (removed h6 variant)
2026-01-15 20:44:51 -05:00
Alex Holliday 0f764ca084 typing 2026-01-15 23:45:40 +00:00
Alex Holliday ed80837810 related serverices to TS 2026-01-15 22:33:25 +00:00
Alex Holliday cf67c82f5e convert job queue and job queue helper 2026-01-15 22:06:46 +00:00
Alex Holliday 5a7b51bfec convert super simple queue to ts 2026-01-15 21:49:48 +00:00
173 changed files with 2452 additions and 1647 deletions

No files matched your search

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

Before

Width:  |  Height:  |  Size: 817 B

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

Before

Width:  |  Height:  |  Size: 915 B

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

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

Before

Width:  |  Height:  |  Size: 591 B

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

Before

Width:  |  Height:  |  Size: 830 B

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

Before

Width:  |  Height:  |  Size: 578 B

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

Before

Width:  |  Height:  |  Size: 237 B

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

Before

Width:  |  Height:  |  Size: 332 B

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

Before

Width:  |  Height:  |  Size: 456 B

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

Before

Width:  |  Height:  |  Size: 363 B

@@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 6C0 2.68629 2.68629 0 6 0H14C17.3137 0 20 2.68629 20 6V14C20 17.3137 17.3137 20 14 20H6C2.68629 20 0 17.3137 0 14V6Z" fill="#1570EF"/>
<path d="M14.6668 6.5L8.25016 12.9167L5.3335 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 380 B

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12" fill="#079455"/>
<circle cx="12" cy="12" r="8" fill="#17B26A"/>
<circle cx="12" cy="12" r="3" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 243 B

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 6C0.5 2.96243 2.96243 0.5 6 0.5H14C17.0376 0.5 19.5 2.96243 19.5 6V14C19.5 17.0376 17.0376 19.5 14 19.5H6C2.96243 19.5 0.5 17.0376 0.5 14V6Z" stroke="#848b97"/>
</svg>

Before

Width:  |  Height:  |  Size: 278 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12" fill="#D92D20"/>
<circle cx="12" cy="12" r="8" fill="#F04438"/>
<circle cx="12" cy="12" r="3" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 243 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 17H21.5L16.5 22H21.5M21.9506 13C21.9833 12.6711 22 12.3375 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C12.1677 22 12.3344 21.9959 12.5 21.9877C12.6678 21.9795 12.8345 21.9671 13 21.9506M12 6V12L15.7384 13.8692" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

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

Before

Width:  |  Height:  |  Size: 330 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 2V4M15 2V4M9 20V22M15 20V22M20 9H22M20 14H22M2 9H4M2 14H4M8.8 20H15.2C16.8802 20 17.7202 20 18.362 19.673C18.9265 19.3854 19.3854 18.9265 19.673 18.362C20 17.7202 20 16.8802 20 15.2V8.8C20 7.11984 20 6.27976 19.673 5.63803C19.3854 5.07354 18.9265 4.6146 18.362 4.32698C17.7202 4 16.8802 4 15.2 4H8.8C7.11984 4 6.27976 4 5.63803 4.32698C5.07354 4.6146 4.6146 5.07354 4.32698 5.63803C4 6.27976 4 7.11984 4 8.8V15.2C4 16.8802 4 17.7202 4.32698 18.362C4.6146 18.9265 5.07354 19.3854 5.63803 19.673C6.27976 20 7.11984 20 8.8 20ZM10.6 15H13.4C13.9601 15 14.2401 15 14.454 14.891C14.6422 14.7951 14.7951 14.6422 14.891 14.454C15 14.2401 15 13.9601 15 13.4V10.6C15 10.0399 15 9.75992 14.891 9.54601C14.7951 9.35785 14.6422 9.20487 14.454 9.10899C14.2401 9 13.9601 9 13.4 9H10.6C10.0399 9 9.75992 9 9.54601 9.10899C9.35785 9.20487 9.20487 9.35785 9.10899 9.54601C9 9.75992 9 10.0399 9 10.6V13.4C9 13.9601 9 14.2401 9.10899 14.454C9.20487 14.6422 9.35785 14.7951 9.54601 14.891C9.75992 15 10.0399 15 10.6 15Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 17H16M11.0177 2.764L4.23539 8.03912C3.78202 8.39175 3.55534 8.56806 3.39203 8.78886C3.24737 8.98444 3.1396 9.20478 3.07403 9.43905C3 9.70352 3 9.9907 3 10.5651V17.8C3 18.9201 3 19.4801 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21H17.8C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4801 21 18.9201 21 17.8V10.5651C21 9.9907 21 9.70352 20.926 9.43905C20.8604 9.20478 20.7526 8.98444 20.608 8.78886C20.4447 8.56806 20.218 8.39175 19.7646 8.03913L12.9823 2.764C12.631 2.49075 12.4553 2.35412 12.2613 2.3016C12.0902 2.25526 11.9098 2.25526 11.7387 2.3016C11.5447 2.35412 11.369 2.49075 11.0177 2.764Z" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 885 B

-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 20C15.9706 20 20 15.9706 20 11C20 6.02944 15.9706 2 11 2C6.02944 2 2 6.02944 2 11C2 12.9021 2.5901 14.6665 3.59721 16.1199C3.70168 16.2707 3.7226 16.4653 3.64529 16.6317L2.42556 19.2519C2.23082 19.6399 2.57262 20.0754 2.97992 19.9193L6.0988 18.7595C6.25727 18.7007 6.44031 18.7151 6.58838 18.7985C7.9427 19.5534 9.4987 20 11 20Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 8.5H17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 12.5H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 16.5H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 923 B

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4.5H18.3C19.4201 4.5 19.9802 4.5 20.408 4.71799C20.7843 4.90973 21.0903 5.21569 21.282 5.59202C21.5 6.01984 21.5 6.57989 21.5 7.7V9C21.5 9.93188 21.5 10.3978 21.3478 10.7654C21.1448 11.2554 20.7554 11.6448 20.2654 11.8478C19.8978 12 19.4319 12 18.5 12M13 19.5H5.7C4.5799 19.5 4.01984 19.5 3.59202 19.282C3.21569 19.0903 2.90973 18.7843 2.71799 18.408C2.5 17.9802 2.5 17.4201 2.5 16.3V15C2.5 14.0681 2.5 13.6022 2.65224 13.2346C2.85523 12.7446 3.24458 12.3552 3.73463 12.1522C4.10218 12 4.56812 12 5.5 12M10.3 14.5H13.7C13.98 14.5 14.12 14.5 14.227 14.4455C14.3211 14.3976 14.3976 14.3211 14.4455 14.227C14.5 14.12 14.5 13.98 14.5 13.7V10.3C14.5 10.02 14.5 9.87996 14.4455 9.773C14.3976 9.67892 14.3211 9.60243 14.227 9.5545C14.12 9.5 13.98 9.5 13.7 9.5H10.3C10.02 9.5 9.87996 9.5 9.773 9.5545C9.67892 9.60243 9.60243 9.67892 9.5545 9.773C9.5 9.87996 9.5 10.02 9.5 10.3V13.7C9.5 13.98 9.5 14.12 9.5545 14.227C9.60243 14.3211 9.67892 14.3976 9.773 14.4455C9.87996 14.5 10.02 14.5 10.3 14.5ZM17.8 22H21.2C21.48 22 21.62 22 21.727 21.9455C21.8211 21.8976 21.8976 21.8211 21.9455 21.727C22 21.62 22 21.48 22 21.2V17.8C22 17.52 22 17.38 21.9455 17.273C21.8976 17.1789 21.8211 17.1024 21.727 17.0545C21.62 17 21.48 17 21.2 17H17.8C17.52 17 17.38 17 17.273 17.0545C17.1789 17.1024 17.1024 17.1789 17.0545 17.273C17 17.38 17 17.52 17 17.8V21.2C17 21.48 17 21.62 17.0545 21.727C17.1024 21.8211 17.1789 21.8976 17.273 21.9455C17.38 22 17.52 22 17.8 22ZM2.8 7H6.2C6.48003 7 6.62004 7 6.727 6.9455C6.82108 6.89757 6.89757 6.82108 6.9455 6.727C7 6.62004 7 6.48003 7 6.2V2.8C7 2.51997 7 2.37996 6.9455 2.273C6.89757 2.17892 6.82108 2.10243 6.727 2.0545C6.62004 2 6.48003 2 6.2 2H2.8C2.51997 2 2.37996 2 2.273 2.0545C2.17892 2.10243 2.10243 2.17892 2.0545 2.273C2 2.37996 2 2.51997 2 2.8V6.2C2 6.48003 2 6.62004 2.0545 6.727C2.10243 6.82108 2.17892 6.89757 2.273 6.9455C2.37996 7 2.51997 7 2.8 7Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 684 B

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 6C12.5523 6 13 5.55228 13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5C11 5.55228 11.4477 6 12 6Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 20C12.5523 20 13 19.5523 13 19C13 18.4477 12.5523 18 12 18C11.4477 18 11 18.4477 11 19C11 19.5523 11.4477 20 12 20Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 733 B

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