Compare commits

...
Author SHA1 Message Date
Alex Holliday 35c022df0f geo job 2026-02-23 00:00:45 +00:00
Alex Holliday 19fc85ae2d front end 2026-02-22 23:25:04 +00:00
Alex Holliday b4f8fa7d72 aggregation 2026-02-22 21:15:02 +00:00
Alex Holliday ef5629d8e4 remove unused 2026-02-22 20:35:13 +00:00
Alex Holliday 4018125224 initial commit 2026-02-22 20:28:42 +00:00
Alexander Holliday 2d023de568 Merge pull request #3325 from bluewave-labs/feat/pagespeed-date-range
feat: pagespeed date range
2026-02-21 13:09:58 -08:00
Alex Holliday f2c88cfd23 fe 2026-02-21 21:05:51 +00:00
Alex Holliday 910c675a49 aggregate pagespeed 2026-02-21 20:52:47 +00:00
Alex Holliday 882e91f6a7 hook up header 2026-02-21 20:31:56 +00:00
Alexander Holliday 06be65ec52 Merge pull request #3324 from bluewave-labs/fix/import
fix: import
2026-02-21 08:20:57 -08:00
Alex Holliday ea54736a39 remove string 2026-02-21 16:19:43 +00:00
Alex Holliday da38f2ee76 createBulkMonitors -> CreateMonitors 2026-02-21 16:17:23 +00:00
Alex Holliday 83d56f1ff6 fe->be 2026-02-21 16:03:45 +00:00
Alex Holliday f94afb63c6 backend 2026-02-21 03:43:11 +00:00
Alex Holliday 673f47983a FE implementation 2026-02-21 03:14:11 +00:00
Alex Holliday b708e83cce add TODOs 2026-02-20 15:11:37 -08:00
Alexander Holliday 288b677781 Merge pull request #3322 from bluewave-labs/fix/ts-error
ts error
2026-02-20 12:37:57 -08:00
Alex Holliday efcf2567b8 ts error 2026-02-20 20:37:29 +00:00
Alexander Holliday 3763b668ed Merge pull request #3320 from bluewave-labs/fix/no-cache
no cache
2026-02-20 12:20:12 -08:00
Alex Holliday 39c6d05196 no cache 2026-02-20 20:19:42 +00:00
Alexander Holliday 7bf83c12af Merge pull request #3310 from bluewave-labs/fix/be/status-page-access
Fix: Authentication Middleware in StatusPage
2026-02-20 10:45:11 -08:00
Br0wnHammer e25669ac6a Fix: Client Redirect 2026-02-21 00:12:17 +05:30
Br0wnHammer f9653888b1 Fix: One Route 2026-02-21 00:10:39 +05:30
Alexander Holliday 921a1c5145 Merge pull request #3316 from bluewave-labs/fix/settings-refactor
fix/settings refactor
2026-02-20 10:29:46 -08:00
Alex Holliday d45e6ec97c remove v2 dir 2026-02-20 18:13:39 +00:00
Alex Holliday ef774b44f2 remove v1 theme and components 2026-02-20 18:10:43 +00:00
Alex Holliday 8d104ee30a remove v1 theme provider 2026-02-20 17:59:08 +00:00
Alex Holliday fc6300d057 remove old, add about and export 2026-02-20 17:36:04 +00:00
Alex Holliday 73f313e4d5 remove old settings strings 2026-02-20 17:22:43 +00:00
Alex Holliday 42fe6ade3e target blank 2026-02-20 17:22:20 +00:00
Alex Holliday b78b584844 add link text 2026-02-20 17:20:43 +00:00
Alex Holliday b64cf3f6b6 email 2026-02-19 19:23:40 +00:00
Br0wnHammer c27bb0e7f1 fix: build error 2026-02-20 00:52:36 +05:30
Br0wnHammer 1a52531faf feat: new middleware 2026-02-20 00:34:43 +05:30
Br0wnHammer 0974c84b8d feat: fe integration of new endpoint 2026-02-20 00:34:23 +05:30
Br0wnHammer 600c937062 feat: API Middleware 2026-02-20 00:31:42 +05:30
Br0wnHammer b87fd5438e feat: verifyStatusPage middleware 2026-02-20 00:30:06 +05:30
Alex Holliday 6a04ce5eee demo monitors, remove monitors 2026-02-19 18:26:40 +00:00
Br0wnHammer dae2709d31 Fix: Authentication Middleware in StatusPage 2026-02-19 23:55:43 +05:30
Alex Holliday 889bb07946 min values 2026-02-19 18:04:34 +00:00
Alex Holliday 9283890b5f 0 handling 2026-02-19 18:01:15 +00:00
Alex Holliday f2779160a1 port 2026-02-19 17:55:55 +00:00
Alex Holliday 216c7753b4 add global thresholds configbox 2026-02-19 17:33:28 +00:00
Alex Holliday a3a25b2add clear-stats 2026-02-19 17:28:00 +00:00
Alex Holliday b5f2a7b26c Fix pagespeed key issue 2026-02-18 21:06:29 +00:00
Alex Holliday d4e72ee8a6 url settigns 2026-02-18 21:02:08 +00:00
Alex Holliday a59f7fd830 pagespeed 2026-02-18 20:35:46 +00:00
Alex Holliday d8e7f9fc0f add pagespeed config 2026-02-18 20:20:45 +00:00
Alex Holliday cdbf5fd0c0 simplify form 2026-02-18 20:14:35 +00:00
Alex Holliday bbb2da3935 fix state type 2026-02-18 20:04:53 +00:00
Alex Holliday 248482a406 initial commit 2026-02-18 19:59:03 +00:00
Alexander Holliday 1c1474cd6f Merge pull request #3302 from bluewave-labs/fix/settings-form
fix: settings form
2026-02-18 11:25:34 -08:00
Alex Holliday db800e2259 simplify form 2026-02-18 19:23:03 +00:00
Alex Holliday b6765830a5 fix client/server validation 2026-02-18 19:13:52 +00:00
Alex Holliday 8cbde4a705 initial commit 2026-02-18 18:47:58 +00:00
Alexander Holliday 6fcaabbdb6 Merge pull request #3301 from bluewave-labs/fix/settings-form
fix logical errors in settings form
2026-02-18 10:12:44 -08:00
Alex Holliday 7cfbc85d9b fix logical errors in settings form 2026-02-18 18:06:00 +00:00
Alexander Holliday ab834d400e Merge pull request #3300 from bluewave-labs/fix/remove-constraint
fix: remove unique constraint
2026-02-18 09:45:15 -08:00
Alex Holliday 1fcf06a415 remove unique constraint 2026-02-18 17:43:32 +00:00
Alexander Holliday dab465b8c5 Merge pull request #3288 from bluewave-labs/settings-zod-schema
Settings zod schema
2026-02-18 09:32:28 -08:00
Alex Holliday 59d245b424 remove pre tag 2026-02-18 17:27:59 +00:00
karenvicent e921b20fe3 fix zod patterns 2026-02-17 22:21:56 -05:00
Gorkem Cetin 0e6680366d Remove Owaise from the contributing section 2026-02-17 19:53:24 -05:00
Alexander Holliday 7f7a9f40df Merge pull request #3298 from bluewave-labs/fix/monitor-stats-duplication
fix monitor stats bug
2026-02-17 16:08:44 -08:00
Alex Holliday 31698bd002 fix monitor stats bug 2026-02-18 00:06:02 +00:00
Alexander Holliday c7ccdafec2 Merge pull request #3297 from bluewave-labs/fix/avg-response-time-calc
fix: increment before calculate
2026-02-17 15:42:22 -08:00
Alex Holliday c920191984 increment before calculate 2026-02-17 23:40:14 +00:00
Alexander Holliday 374b6aa6ae Merge pull request #3296 from bluewave-labs/feat/max-response-time-rolling-stats
feat: max response time rolling stats
2026-02-17 15:34:35 -08:00
Alex Holliday 3d5c81fe2f Add maxResponseTime to client-side MonitorStats type 2026-02-17 22:50:44 +00:00
Alex Holliday 3bd6ec5d28 max response time 2026-02-17 22:47:37 +00:00
Alexander Holliday 7bb47ef432 Merge pull request #3295 from bluewave-labs/feat/cleanup-job
feat: cleanup job
2026-02-17 14:34:42 -08:00
Alex Holliday 7856317e8f remvoe space 2026-02-17 22:29:30 +00:00
Alex Holliday edbd8f2abb fix check bug 2026-02-17 22:27:52 +00:00
Alex Holliday cddf8b32e6 cleanup incidents 2026-02-17 19:39:24 +00:00
Alex Holliday 0032765bd1 remove orphaned checks 2026-02-17 19:20:08 +00:00
Alex Holliday 6bc9194e8f initial commit 2026-02-17 19:03:17 +00:00
Alexander Holliday ef15e5d6e0 Merge pull request #3294 from bluewave-labs/fix/remove-incidents-on-delete-monitor
fix: remove incidents on delete
2026-02-17 10:32:16 -08:00
Alex Holliday 97750da889 remove incidents on delete 2026-02-17 18:30:22 +00:00
Alexander Holliday af8083bdaa Merge pull request #3293 from bluewave-labs/fix/active-incidents
fix: add missing pluralization strings
2026-02-17 10:16:45 -08:00
Alexander Holliday 54b33ec835 Merge pull request #3292 from bluewave-labs/fix/incident-reason
fix/incident reason
2026-02-17 10:15:41 -08:00
Alex Holliday 847904a274 add missing pluralization strings 2026-02-17 18:15:30 +00:00
Alex Holliday 7bc03c976b remove comment 2026-02-17 17:53:00 +00:00
Alex Holliday 73f05ab78e refactor to remove duplication 2026-02-17 17:49:56 +00:00
Alex Holliday 584e4633d3 incident details 2026-02-17 17:42:11 +00:00
Alexander Holliday 433c57b42e Merge pull request #3291 from bluewave-labs/feat/notification-refactor
feat: notification refactor
2026-02-17 09:31:47 -08:00
Alexander Holliday a2cf2afc09 Merge pull request #3279 from bluewave-labs/enhancement/uptime-graph-avg-max-response
enhancement: add average and max response time to histogram
2026-02-17 09:31:25 -08:00
Alex Holliday 232be7a405 cleanup 2026-02-17 17:28:43 +00:00
mannilakash 7d66bb5668 fix: unnecessary custom styling and originalResponseTime usage 2026-02-17 10:27:22 -05:00
mannilakash 566fded55c Merge branch 'develop' of github.com:bluewave-labs/Checkmate into enhancement/uptime-graph-avg-max-response 2026-02-17 10:20:37 -05:00
Alex Holliday fbb0e940c6 emails 2026-02-17 01:47:57 +00:00
Alex Holliday f983863c72 new template 2026-02-17 01:25:30 +00:00
Alex Holliday e0a75c78db refactor discord 2026-02-17 01:16:00 +00:00
Alex Holliday 2cd2a6d917 pagerduty 2026-02-16 21:58:58 +00:00
Alex Holliday 6f55e4934f matrix 2026-02-16 21:49:42 +00:00
Alex Holliday 0e1dc3e3fe slack 2026-02-16 21:48:03 +00:00
Alex Holliday e18681606c add webhook 2026-02-16 20:55:00 +00:00
Alex Holliday d2d75cd035 initial commit 2026-02-16 20:18:25 +00:00
Alex Holliday a65515fc08 logger type 2026-02-16 19:42:10 +00:00
Alex Holliday 302ac6f4f1 update gitignore 2026-02-16 19:32:28 +00:00
Alexander Holliday b51d40fe77 Merge pull request #3290 from bluewave-labs/feat/incident-on-threshold-exception
Feat/incident on threshold exception
2026-02-16 10:47:24 -08:00
Alex Holliday 27f3def9bd correct logger type 2026-02-16 18:43:03 +00:00
Alex Holliday e7a7c0e603 remove grouping 2026-02-16 18:39:41 +00:00
Alex Holliday 55885ea007 remove global decision 2026-02-16 18:25:35 +00:00
karenvicent 475801e91c fix traslation object 2026-02-16 00:47:23 -05:00
karenvicent cd501e46cd Merge branch 'develop' into settings-zod-schema 2026-02-16 00:12:08 -05:00
karenvicent 93c15588a4 implement schema zod and validation on settings 2026-02-16 00:09:08 -05:00
Alex Holliday a8ecd44abf inject settings service, extract client host 2026-02-16 00:39:34 +00:00
Alex Holliday 412a3b7cc9 remove redundant method 2026-02-16 00:17:51 +00:00
Alex Holliday be8580ee99 update providers 2026-02-15 18:35:54 +00:00
Alex Holliday 8020bfde21 refactor handleIncidents 2026-02-15 18:20:42 +00:00
Alex Holliday 7061eb8eb0 use decision object in incidentService 2026-02-15 18:09:21 +00:00
Alex Holliday 70ae15725e conditinally render breached stauts box 2026-02-14 23:09:27 +00:00
Alex Holliday 32a7bd6543 add breached status to summary header 2026-02-14 22:57:27 +00:00
Alex Holliday 68db4b8701 breached status 2026-02-14 22:52:14 +00:00
Alex Holliday b75c1a4fcb create incident on hardware breach 2026-02-14 22:29:06 +00:00
Alex Holliday e50240885d debug logging 2026-02-14 22:25:15 +00:00
Alex Holliday 22645d86f7 handle infra thresholds 2026-02-14 22:14:51 +00:00
Alex Holliday 62778886e1 use counters 2026-02-14 21:55:47 +00:00
Alex Holliday 9c467c763d add threshold counters 2026-02-14 21:49:32 +00:00
Alex Holliday 97ace1e058 remove unused checks 2026-02-14 21:38:00 +00:00
Alex Holliday 2fb6cb0b6f remove alertThreshold 2026-02-14 21:29:30 +00:00
Alex Holliday a7b0ce4d8f remove thresholds 2026-02-14 21:21:18 +00:00
Alex Holliday 4a73170583 initial commit 2026-02-14 21:05:31 +00:00
Alexander Holliday 41310cb552 Merge pull request #3286 from bluewave-labs/feat/infra-thresholds
feat: infra thresholds
2026-02-13 17:23:49 -08:00
Alex Holliday 111b68b8df revert slider size change 2026-02-14 01:21:02 +00:00
Alex Holliday 2430bbeebb remove slider hover 2026-02-14 01:15:12 +00:00
Alex Holliday f694917dd4 remove unused 2026-02-14 01:07:46 +00:00
Alex Holliday 47b18e1d9d use sliderwithlabels 2026-02-14 01:02:15 +00:00
Alexander Holliday 0d863aefe8 Merge pull request #3285 from bluewave-labs/fix/create-monitors-port
fix port input
2026-02-13 16:45:01 -08:00
Alex Holliday c319738048 add config box for thresholds 2026-02-14 00:40:59 +00:00
Alex Holliday 002a82bb9a fix port input 2026-02-14 00:19:01 +00:00
Alexander Holliday 8719efab5d Merge pull request #3284 from bluewave-labs/fix/routes
fix: routes
2026-02-13 16:17:57 -08:00
Alex Holliday 86fd535eaf routes to TS 2026-02-14 00:08:28 +00:00
Alex Holliday 2f6dbe9a3a update gitignore 2026-02-13 23:56:35 +00:00
Alexander Holliday 649eaae712 Merge pull request #3283 from bluewave-labs/fix/string-cleanup
clean up strings
2026-02-13 13:22:34 -08:00
Alex Holliday 1888ee0478 remove unused monitor import 2026-02-13 21:19:55 +00:00
Alex Holliday fd2c54d3d3 remove unused 2026-02-13 20:42:26 +00:00
Alex Holliday bd941db771 clean up strings 2026-02-13 20:41:27 +00:00
Alexander Holliday 88fceddbbf Merge pull request #3282 from bluewave-labs/fix/monitor-stats
fix: initializing stats
2026-02-13 11:18:54 -08:00
Alex Holliday 13251ca7c5 fix initializing stats 2026-02-13 19:04:19 +00:00
Alexander Holliday 6d6b597f99 Merge pull request #3281 from bluewave-labs/fix/cleanup
fix: cleanup
2026-02-13 11:01:43 -08:00
Alex Holliday dfc1733820 remove icons 2026-02-13 18:51:29 +00:00
mannilakash 39a449e594 fix: padding fix for stats text 2026-02-13 11:14:25 -05:00
mannilakash 974d33b3de enhancement: add average and max response time for histogram 2026-02-13 10:32:52 -05:00
Alex Holliday afe2b9d3bb remove unused images 2026-02-12 23:37:08 +00:00
Alex Holliday 5ab5cbe622 status box fix 2026-02-12 23:31:37 +00:00
Alex Holliday b6b1b8464c remove old DB references 2026-02-12 23:03:16 +00:00
Alex Holliday f337ca5ff0 initial commit 2026-02-12 22:37:40 +00:00
Alexander Holliday f5bc8e78e4 Merge pull request #3278 from bluewave-labs/fix/failed-check-warn
Fix/failed check warn
2026-02-12 11:44:48 -08:00
Alex Holliday 8089e71f96 warn instead of throw on failure to build check 2026-02-12 19:42:58 +00:00
karenvicent 6195650dc1 Merge branch 'develop' into settings-zod-schema 2026-02-12 14:25:05 -05:00
karenvicent f59d8bf1bf Merge branch 'develop' into settings-zod-schema 2026-02-12 12:18:05 -05:00
karenvicent 0941f3ce1a zod schema and hook 2026-02-12 12:13:36 -05:00
268 changed files with 4839 additions and 7765 deletions

No files matched your search

@@ -107,6 +107,7 @@ jobs:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
no-cache: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
platforms: linux/amd64,linux/arm64
+1
View File
@@ -102,6 +102,7 @@ jobs:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
no-cache: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
platforms: linux/amd64,linux/arm64
+3 -1
View File
@@ -3,4 +3,6 @@
.VSCodeCounter
*.sh
mongo
node_modules/
node_modules/
docs/architecture
docs/reviews
+1 -1
View File
@@ -147,7 +147,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
## Contributing
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers.
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe.
+5 -3
View File
@@ -2472,11 +2472,13 @@
}
},
"node_modules/axios": {
"version": "1.13.4",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
+3 -6
View File
@@ -3,16 +3,14 @@ import { useSelector } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { Routes } from "./Routes";
import AppLayout from "@/Components/v2/layout/AppLayout";
import AppLayout from "@/Components/layout/AppLayout";
import type { RootState } from "@/Types/state";
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
function App() {
const mode = useSelector((state: RootState) => state.ui.mode);
const theme = mode === "light" ? lightTheme : darkTheme;
return (
@@ -21,8 +19,7 @@ function App() {
<GlobalStyles
styles={{
body: {
// @ts-ignore
backgroundColor: theme.palette.background.main,
backgroundColor: theme.palette.background.default,
},
}}
/>
@@ -2,7 +2,7 @@ import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { CheckSnapshot } from "@/Types/Check";
import { getResponseColor } from "@/Utils/DataUtils";
import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip";
import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip";
import type { SxProps } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
@@ -0,0 +1,155 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
interface HistogramResponseTimeProps {
checks: CheckSnapshot[];
height?: ResponsiveStyleValue<number | string>;
gap?: ResponsiveStyleValue<number | string>;
showStats?: boolean;
statsPosition?: "left" | "right";
}
interface ResponseTimeStats {
max: number | string;
avg: number | string;
}
const DEFAULT_HEIGHT = 50;
const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => {
if (!Array.isArray(checks) || checks.length === 0) {
return { max: "-", avg: "-" };
}
const validChecks = checks.filter((check) => check.originalResponseTime != null);
if (validChecks.length === 0) {
return { max: "-", avg: "-" };
}
const responseTimes = validChecks.map((check) => check.originalResponseTime);
const max = Math.max(...responseTimes);
const avg = Math.round(
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
);
return { max, avg };
};
export const HistogramResponseTime = ({
checks,
height = DEFAULT_HEIGHT,
gap,
showStats = true,
statsPosition = "right",
}: HistogramResponseTimeProps) => {
const theme = useTheme();
const { t } = useTranslation();
const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]);
if (!Array.isArray(checks) || checks.length === 0) return null;
const data =
checks.length !== 25
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
: checks;
const chartHeight = typeof height === "number" ? `${height}px` : height;
const gridGap = gap ?? theme.spacing(0.5);
const statsContent = showStats &&
(typeof stats.max === "number" || typeof stats.avg === "number") && (
<Stack
justifyContent="center"
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
minWidth={70}
pr={statsPosition === "left" ? theme.spacing(8) : 0}
pl={statsPosition === "right" ? theme.spacing(8) : 0}
>
<Typography variant="body2">
{t("common.charts.histogram.avg", { value: stats.avg })}
</Typography>
<Typography variant="body2">
{t("common.charts.histogram.max", { value: stats.max })}
</Typography>
</Stack>
);
return (
<Stack
direction={statsPosition === "left" ? "row-reverse" : "row"}
alignItems="center"
sx={{ width: "100%" }}
>
<Box sx={{ flex: 1 }}>
<Box
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: gridGap,
alignItems: "end",
height: chartHeight,
}}
>
{data.map((check, index) => {
const isPlaceholder = (check as any).status === "placeholder";
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
const barColor =
check.status === true
? theme.palette.success.light
: theme.palette.error.light;
const bar = (
<Box
sx={{
position: "relative",
width: "100%",
height: "100%",
borderRadius: theme.spacing(1),
bgcolor: theme.palette.action.hover,
overflow: "hidden",
}}
>
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
height: isPlaceholder ? 0 : heightPct,
bgcolor: barColor,
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
);
return isPlaceholder ? (
<Box
key={index}
sx={{ height: "100%" }}
>
{bar}
</Box>
) : (
<HeatmapResponseTimeTooltip
key={index}
check={check}
>
{bar}
</HeatmapResponseTimeTooltip>
);
})}
</Box>
</Box>
{statsContent}
</Stack>
);
};
@@ -1,7 +1,7 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
import { ToggleButtonGroup, ToggleButton } from "@/Components/inputs";
import { useTheme } from "@mui/material/styles";
import CircularProgress from "@mui/material/CircularProgress";
import { useTranslation } from "react-i18next";
+5
View File
@@ -0,0 +1,5 @@
export * from "./charts/HistogramResponseTime";
export * from "./charts/HeatmapResponseTime";
export * from "./charts/HeatmapResponseTimeTooltip";
export * from "./controls/HeaderCreate";
export * from "./controls/HeaderTimeRange";
@@ -1,4 +1,4 @@
import { BaseBox } from "../design-elements";
import { BaseBox } from ".";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
@@ -8,8 +8,8 @@ import {
EmptyFallback,
EmptyMonitorFallback,
BaseFallback,
} from "@/Components/v2/design-elements/Fallback";
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
} from "@/Components/design-elements/Fallback";
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
import CircularProgress from "@mui/material/CircularProgress";
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
@@ -1,7 +1,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements";
import { Button } from "@/Components/v2/inputs";
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
import { Button } from "@/Components/inputs";
import { useNavigate } from "react-router";
import useMediaQuery from "@mui/material/useMediaQuery";
@@ -85,12 +85,7 @@ export const EmptyFallback = ({
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Typography component="h1">{title}</Typography>
<Stack
sx={{
flexWrap: "wrap",
@@ -1,4 +1,4 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
@@ -1,11 +1,11 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/v2/design-elements";
import { PulseDot, Dot } from "@/Components/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import { typographyLevels } from "@/Utils/Theme/Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
@@ -19,7 +19,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Typography
fontSize={typographyLevels.xl}
fontWeight={500}
color={theme.palette.primary.contrastText}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
@@ -4,8 +4,8 @@ import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { lighten } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements";
import type { PaletteKey } from "@/Utils/Theme/Theme";
import { BaseBox, TooltipWithInfo } from "@/Components/design-elements";
import type { SxProps } from "@mui/material";
type GradientBox = React.PropsWithChildren<{
@@ -1,8 +1,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/design-elements";
import Background from "@/assets/Images/background-grid.svg?react";
import { BaseBox } from "@/Components/design-elements";
import { useTranslation } from "react-i18next";
import type { SxProps } from "@mui/material";
@@ -18,6 +17,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
return (
<BaseBox
sx={{
backgroundColor: theme.palette.background.default,
overflow: "hidden",
position: "relative",
flex: 1,
@@ -27,11 +27,23 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
top={0}
left={0}
right={0}
bottom={0}
sx={{
pointerEvents: "none",
backgroundImage: `
linear-gradient(${theme.palette.divider} 1px, transparent 1px),
linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px)
`,
backgroundSize: "24px 24px",
maskImage:
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
WebkitMaskImage:
"linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
}}
/>
{children}
</BaseBox>
);
@@ -127,6 +139,17 @@ export const InitializingStatusBox = ({ n }: { n: number }) => {
/>
);
};
export const BreachedStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("pages.common.monitors.status.breached")}
n={n}
color={theme.palette.warning.main}
/>
);
};
export const TotalChecksBox = ({ n }: { n: number }) => {
const theme = useTheme();
@@ -1,5 +1,5 @@
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/design-elements";
import { BaseBox } from "@/Components/design-elements";
import Typography from "@mui/material/Typography";
import type { MonitorStatus } from "@/Types/Monitor";
@@ -21,6 +21,8 @@ export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProp
return t("pages.common.monitors.status.up");
} else if (status === "down") {
return t("pages.common.monitors.status.down");
} else if (status === "breached") {
return t("pages.common.monitors.status.breached");
} else if (status === "maintenance") {
return t("pages.common.monitors.status.maintenance");
} else if (status === "paused") {
@@ -10,7 +10,7 @@ interface TextLinkProps extends StackProps {
text: string;
linkText: string;
href: string;
target?: string;
target?: "_self" | "_blank" | "_parent" | "_top";
}
export const TextLink = ({
@@ -34,6 +34,7 @@ export const TextLink = ({
to={href}
component={RouterLink}
target={target}
{...(target === "_blank" && { rel: "noopener noreferrer" })}
>
{linkText}
</Link>
@@ -1,6 +1,6 @@
import Autocomplete from "@mui/material/Autocomplete";
import type { AutocompleteProps } from "@mui/material/Autocomplete";
import { TextField, Checkbox } from "@/Components/v2/inputs";
import { TextField, Checkbox } from "@/Components/inputs";
import ListItem from "@mui/material/ListItem";
import Stack from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
@@ -1,9 +1,9 @@
import { MuiColorInput } from "mui-color-input";
import type { MuiColorInputProps } from "mui-color-input";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "@/Components/v2/inputs/FieldLabel";
import { FieldLabel } from "@/Components/inputs/FieldLabel";
interface ColorPickerProps extends MuiColorInputProps {
fieldLabel?: string;
@@ -4,8 +4,8 @@ import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import { Button } from "@/Components/v2/inputs";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import { Button } from "@/Components/inputs";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTranslation } from "react-i18next";
import type { ReactNode } from "react";
@@ -38,11 +38,11 @@ export const ImageUpload = ({
const isValidType = accept.some((type) => file.type.includes(type));
const isValidSize = file.size <= maxSize;
if (!isValidType) {
setLocalError(t("common.errors.invalidFileFormat"));
setLocalError(t("components.imageUpload.errors.invalidFileFormat"));
return;
}
if (!isValidSize) {
setLocalError(t("common.errors.invalidFileSize"));
setLocalError(t("components.imageUpload.errors.invalidFileSize"));
return;
}
setLocalError(null);
@@ -137,15 +137,15 @@ export const ImageUpload = ({
color="primary"
fontWeight={500}
>
{t("common.imageUpload.clickToUpload")}
{t("components.imageUpload.clickToUpload")}
</Typography>{" "}
{t("common.imageUpload.orDragAndDrop")}
{t("components.imageUpload.orDragAndDrop")}
</Typography>
<Typography
variant="caption"
color="text.disabled"
>
{accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "}
{accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "}
{Math.round(maxSize / 1024 / 1024)}MB
</Typography>
</Stack>
@@ -1,7 +1,7 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/Components/v2/inputs";
import { Select } from "@/Components/inputs";
import "flag-icons/css/flag-icons.min.css";
import { useTranslation } from "react-i18next";
@@ -41,7 +41,10 @@ export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
"& .MuiSlider-thumb": {
backgroundColor: "#fff",
"&:hover, &.Mui-focusVisible": {
boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`,
boxShadow: "none",
},
"&:active": {
boxShadow: "none",
},
},
"& .MuiSlider-valueLabel": {
@@ -1,7 +1,7 @@
import { forwardRef } from "react";
import TextField from "@mui/material/TextField";
import type { TextFieldProps } from "@mui/material";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
@@ -1,11 +1,10 @@
import Box from "@mui/material/Box";
import { useState, useEffect, useRef } from "react";
import { ThemeProvider, useTheme } from "@mui/material/styles";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import BackgroundSVG from "@/assets/Images/background.svg";
import type { RootState } from "@/Types/state";
import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
import { OfflineBanner } from "@/Components/v2/design-elements";
import { OfflineBanner } from "@/Components/design-elements";
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
interface AppLayoutProps {
@@ -15,8 +14,6 @@ interface AppLayoutProps {
const AppLayout = ({ children }: AppLayoutProps) => {
const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const v2theme = mode === "dark" ? darkTheme : lightTheme;
const [serverUnreachable, setServerUnreachable] = useState(false);
const retryIntervalRef = useRef<number | null>(null);
@@ -49,18 +46,14 @@ const AppLayout = ({ children }: AppLayoutProps) => {
<Box
sx={{
minHeight: "100vh",
// @ts-expect-error custom palette property
backgroundColor: theme.palette.primaryBackground.main,
backgroundColor: theme.palette.background.default,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
<ThemeProvider theme={v2theme}>
<OfflineBanner visible={serverUnreachable} />
</ThemeProvider>
<OfflineBanner visible={serverUnreachable} />
{children}
</Box>
);
@@ -1,26 +1,19 @@
import { Sidebar } from "@/Components/v2/sidebar";
import { Sidebar } from "@/Components/sidebar";
import { Outlet } from "react-router";
import Stack from "@mui/material/Stack";
import { useMediaQuery } from "@mui/material";
import { useSidebar } from "@/Hooks/useSidebar";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
import { ThemeProvider, useTheme } from "@mui/material";
import { useTheme } from "@mui/material";
const RootLayout = () => {
const mode = useSelector((state: RootState) => state.ui.mode);
const v2theme = mode === "dark" ? darkTheme : lightTheme;
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { collapsedWidth } = useSidebar();
return (
<Stack flexDirection="row">
<ThemeProvider theme={v2theme}>
<Sidebar />
</ThemeProvider>
<Sidebar />
<Stack
flex={1}
padding={6}
@@ -1,5 +1,5 @@
import Stack from "@mui/material/Stack";
import { Select, Button } from "@/Components/v2/inputs";
import { Select, Button } from "@/Components/inputs";
import MenuItem from "@mui/material/MenuItem";
import useMediaQuery from "@mui/material/useMediaQuery";
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
import { Icon, MonitorStatus } from "@/Components/v2/design-elements";
import { Button } from "@/Components/v2/inputs";
import { Icon, MonitorStatus } from "@/Components/design-elements";
import { Button } from "@/Components/inputs";
import { Settings, Pause, Play, Mail, Bug, Trash } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -3,7 +3,8 @@ import {
DownStatusBox,
PausedStatusBox,
InitializingStatusBox,
} from "@/Components/v2/design-elements";
BreachedStatusBox,
} from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import type { MonitorsSummary } from "@/Types/Monitor";
@@ -11,9 +12,13 @@ import { useTheme } from "@mui/material";
interface MonitorsSummaryProps {
summary: MonitorsSummary | null;
showBreached?: boolean;
}
export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
export const HeaderMonitorsSummary = ({
summary,
showBreached = false,
}: MonitorsSummaryProps) => {
const theme = useTheme();
return (
<Stack
@@ -22,6 +27,7 @@ export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
>
<UpStatusBox n={summary?.upMonitors || 0} />
<DownStatusBox n={summary?.downMonitors || 0} />
{showBreached && <BreachedStatusBox n={summary?.breachedMonitors || 0} />}
<PausedStatusBox n={summary?.pausedMonitors || 0} />
<InitializingStatusBox n={summary?.initializingMonitors || 0} />
</Stack>
@@ -1,5 +1,5 @@
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/v2/design-elements";
import { StatBox } from "@/Components/design-elements";
import prettyMilliseconds from "pretty-ms";
import { useTheme } from "@mui/material/styles";
@@ -1,4 +1,4 @@
import { BaseChart, BaseBox } from "@/Components/v2/design-elements";
import { BaseChart, BaseBox } from "@/Components/design-elements";
import { Clock } from "lucide-react";
import {
AreaChart,
@@ -1,4 +1,4 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import {
AreaChart,
Area,
@@ -8,7 +8,7 @@ import {
ResponsiveContainer,
} from "recharts";
import { Fragment, useId } from "react";
import { XTick } from "@/Components/v2/monitors";
import { XTick } from "@/Components/monitors";
import { useTheme } from "@mui/material/styles";
import type { HardwareCheckStats } from "@/Types/Monitor";
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip";
import { HistogramPageSpeedTooltip } from "@/Components/monitors/charts/HistogramPageSpeedTooltip";
import { useTheme } from "@mui/material/styles";
import type { CheckSnapshot } from "@/Types/Check";
import type { MonitorStatus } from "@/Types/Monitor";
@@ -1,6 +1,6 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import { TrendingUp } from "lucide-react";
import { XTick } from "@/Components/v2/monitors/";
import { XTick } from "@/Components/monitors";
import {
XAxis,
AreaChart,
@@ -9,10 +9,10 @@ import {
CartesianGrid,
ResponsiveContainer,
} from "recharts";
import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors";
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
import { useTheme } from "@mui/material/styles";
import type { CheckSnapshot } from "@/Types/Check";
import type { PageSpeedGroupedCheck } from "@/Types/Check";
import type { Palette } from "@mui/material/styles";
type PaletteColorKey = Extract<
keyof Palette,
@@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({
checks,
range,
}: {
checks: CheckSnapshot[];
checks: PageSpeedGroupedCheck[];
range: string;
}) => {
const theme = useTheme();
@@ -73,7 +73,7 @@ export const HistogramPageSpeedDetails = ({
>
<AreaChart data={checks}>
<XAxis
dataKey={"createdAt"}
dataKey={"bucketDate"}
tick={(props) => (
<XTick
{...props}
@@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { ConfigItem } from "@/Components/v2/monitors";
import type { ConfigItem } from "@/Components/monitors";
import type { TooltipProps } from "recharts";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "@/Utils/TimeUtils";
@@ -1,4 +1,4 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import { FileText } from "lucide-react";
import type { CheckAudits, CheckSnapshot } from "@/Types/Check";
import { Pie, PieChart, ResponsiveContainer, Label } from "recharts";
@@ -1,7 +1,7 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { BarChart3 } from "lucide-react";
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import Typography from "@mui/material/Typography";
import type { CheckSnapshot } from "@/Types/Check";
@@ -1,4 +1,4 @@
import { BaseChart } from "@/Components/v2/design-elements";
import { BaseChart } from "@/Components/design-elements";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Gauge } from "lucide-react";
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useNavigate } from "react-router";
import { MoreVertical, LogOut } from "lucide-react";
import { Avatar, Icon } from "@/Components/v2/design-elements";
import { Avatar, Icon } from "@/Components/design-elements";
import { clearAuthState } from "@/Features/Auth/authSlice.js";
import type { RootState } from "@/Types/state.js";
@@ -1,4 +1,4 @@
import { Icon } from "@/Components/v2/design-elements";
import { Icon } from "@/Components/design-elements";
import {
Globe,
@@ -1,5 +1,5 @@
import { Typography, IconButton, Stack, Box } from "@mui/material";
import { Icon } from "@/Components/v2/design-elements";
import { Icon } from "@/Components/design-elements";
import { X } from "lucide-react";
import { useTheme } from "@mui/material";
@@ -4,11 +4,11 @@ import Stack from "@mui/material/Stack";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import { useSidebar } from "@/Hooks/useSidebar.js";
import { Logo } from "@/Components/v2/sidebar/Logo";
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/v2/sidebar/Menu";
import { NavItem } from "@/Components/v2/sidebar/NavItem";
import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt";
import { AuthFooter } from "@/Components/v2/sidebar/Authfooter";
import { Logo } from "@/Components/sidebar/Logo";
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { NavItem } from "@/Components/sidebar/NavItem";
import { StarPrompt } from "@/Components/sidebar/StarPrompt";
import { AuthFooter } from "@/Components/sidebar/Authfooter";
import { useNavigate, useLocation } from "react-router";
import { useTranslation } from "react-i18next";
@@ -1,29 +0,0 @@
import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowRight = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<Icon
name="ChevronsRight"
color={color}
{...props}
/>
);
} else {
return (
<Icon
name="ChevronRight"
color={color}
{...props}
/>
);
}
};
ArrowRight.propTypes = {
type: PropTypes.oneOf(["double", "default"]),
color: PropTypes.string,
};
export default ArrowRight;
@@ -1,21 +0,0 @@
.MuiBreadcrumbs-root {
min-height: 34px;
}
.MuiBreadcrumbs-root svg {
width: 16px;
min-height: 16px;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
font-weight: 400;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
cursor: pointer;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a {
font-weight: 500;
opacity: 1;
cursor: default;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-separator {
margin: 0;
}
@@ -1,78 +0,0 @@
import PropTypes from "prop-types";
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import ArrowRight from "../ArrowRight/index.jsx";
import "./index.css";
/**
* Breadcrumbs component that displays a list of breadcrumb items.
*
* @param {Object} props
* @param {Array} props.list - Array of breadcrumb items. Each item should have `name` and `path` properties.
* @param {string} props.list.name - The name to display for the breadcrumb.
* @param {string} props.list.path - The path to navigate to when the breadcrumb is clicked.
*
* @returns {JSX.Element} The rendered Breadcrumbs component.
*/
const Breadcrumbs = ({ list }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<MUIBreadcrumbs
separator={<ArrowRight />}
aria-label="breadcrumb"
px={theme.spacing(2)}
py={theme.spacing(3.5)}
width="fit-content"
backgroundColor={theme.palette.secondary.main}
borderRadius={theme.shape.borderRadius}
lineHeight="18px"
sx={{
"& .MuiBreadcrumbs-li a": {
transition: "background-color 0.2s ease-in-out, color 0.2s ease-in-out",
},
"& .MuiBreadcrumbs-li:not(:last-of-type):hover a": {
backgroundColor: theme.palette.secondary.contrastText,
color: theme.palette.secondary.main,
},
}}
>
{list.map((item, index) => {
return (
<Box
component="a"
key={`${item.name}-${index}`}
px={theme.spacing(4)}
pt={theme.spacing(2)}
pb={theme.spacing(3)}
borderRadius={theme.shape.borderRadius}
onClick={() => navigate(item.path)}
sx={{
opacity: 0.8,
textTransform: "capitalize",
"&, &:hover": {
color: theme.palette.secondary.contrastText,
},
}}
>
{item.name}
</Box>
);
})}
</MUIBreadcrumbs>
);
};
Breadcrumbs.propTypes = {
list: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
}).isRequired
).isRequired,
};
export default Breadcrumbs;
@@ -1,39 +0,0 @@
import { Stack, styled } from "@mui/material";
const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(2),
"& > *": {
paddingTop: theme.spacing(15),
paddingBottom: theme.spacing(15),
},
"& > div:first-of-type": {
flex: 0.7,
borderRight: 1,
borderRightStyle: "solid",
borderRightColor: theme.palette.primary.lowContrast,
paddingRight: theme.spacing(15),
paddingLeft: theme.spacing(15),
backgroundColor: theme.palette.tertiary.background,
"& :is(h1, h2):first-of-type": {
fontWeight: 600,
marginBottom: theme.spacing(4),
},
},
"& > div:last-of-type": {
flex: 1,
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(20),
},
"& h1, & h2": {
color: theme.palette.primary.contrastTextSecondary,
},
}));
export default ConfigBox;
@@ -1,73 +0,0 @@
import { useId } from "react";
import PropTypes from "prop-types";
import { Modal, Stack, Typography } from "@mui/material";
const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => {
const titleId = useId();
const descriptionId = useId();
const ariaDescribedBy = description?.length > 0 ? descriptionId : "";
return (
<Modal
aria-labelledby={titleId}
aria-describedby={ariaDescribedBy}
open={open}
onClose={onClose}
onClick={(e) => e.stopPropagation()}
>
<Stack
gap={theme.spacing(2)}
width={width}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
minWidth: 400,
bgcolor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id={titleId}
component="h2"
fontSize={16}
color={theme.palette.primary.contrastText}
fontWeight={600}
marginBottom={theme.spacing(4)}
>
{title}
</Typography>
{description && (
<Typography
id={descriptionId}
color={theme.palette.primary.contrastTextTertiary}
marginBottom={theme.spacing(4)}
>
{description}
</Typography>
)}
{children}
</Stack>
</Modal>
);
};
GenericDialog.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
.isRequired,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
};
export { GenericDialog };
-63
View File
@@ -1,63 +0,0 @@
import PropTypes from "prop-types";
import { Button, Stack } from "@mui/material";
import { GenericDialog } from "./genericDialog.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const Dialog = ({
title,
description,
open,
onCancel,
confirmationButtonLabel,
onConfirm,
isLoading,
}) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<GenericDialog
title={title}
description={description}
open={open}
onClose={onCancel}
theme={theme}
>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="contained"
color="secondary"
onClick={onCancel}
>
{t("cancel", "Cancel")}
</Button>
<Button
variant="contained"
color="error"
loading={isLoading}
onClick={onConfirm}
>
{confirmationButtonLabel}
</Button>
</Stack>
</GenericDialog>
);
};
Dialog.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
open: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
confirmationButtonLabel: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
};
export default Dialog;
-120
View File
@@ -1,120 +0,0 @@
/**
* Maps legacy icon names to Lucide icon names
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
*/
export const iconMap = {
// Navigation/Sidebar icons
monitors: "Globe",
incidents: "AlertTriangle",
integrations: "Link",
"page-speed": "Gauge",
settings: "Settings",
notifications: "Bell",
maintenance: "Wrench",
"status-pages": "Wifi",
docs: "FileText",
discussions: "MessageCircle",
support: "HelpCircle",
changeLog: "Code",
logs: "Database",
// User icons
user: "User",
"user-two": "Users",
"user-edit": "UserPen",
groups: "Users",
lock: "Lock",
key: "Key",
logout: "LogOut",
// Action icons
edit: "Pencil",
"trash-bin": "Trash2",
search: "Search",
"close-icon": "X",
check: "Check",
"check-icon": "CheckCircle",
"check-outlined": "CheckCircle",
// Arrow icons
"left-arrow": "ChevronLeft",
"right-arrow": "ChevronRight",
"up-arrow": "ChevronUp",
"down-arrow": "ChevronDown",
"left-arrow-double": "ChevronsLeft",
"right-arrow-double": "ChevronsRight",
"left-arrow-long": "ArrowLeft",
"top-right-arrow": "ArrowUpRight",
"open-in-new-page": "ExternalLink",
"selector-vertical": "ChevronsUpDown",
// Status/Alert icons
"alert-icon": "Bell",
"warning-icon": "AlertCircle",
"history-icon": "History",
// Data/Chart icons
dashboard: "LayoutGrid",
"monitor-graph-line": "TrendingUp",
"response-time-icon": "TrendingUp",
"uptime-icon": "Activity",
"speedometer-icon": "Gauge",
"spedometer-icon": "Gauge",
"ruler-icon": "Ruler",
"performance-report": "Layers",
"average-response-icon": "BarChart3",
"distributed-uptime": "Network",
// Misc icons
calendar: "Calendar",
"calendar-check": "CalendarCheck",
"clock-snooze": "Clock",
mail: "Mail",
email: "Mail",
folder: "Folder",
"dots-vertical": "MoreVertical",
"pause-icon": "PauseCircle",
"resume-icon": "PlayCircle",
"checkbox-filled": "CheckSquare",
"checkbox-outline": "Square",
"radio-checked": "Circle",
"cpu-chip": "Cpu",
certificate: "Award",
"interval-check": "RefreshCw",
};
/**
* MUI icon to Lucide mapping
*/
export const muiToLucideMap = {
AddCircleOutline: "PlusCircle",
ArrowDownwardRounded: "ArrowDown",
ArrowUpwardRounded: "ArrowUp",
ArrowDropDown: "ChevronDown",
ArrowOutward: "ExternalLink",
CheckCircle: "CheckCircle2",
Clear: "X",
Close: "X",
CloseRounded: "X",
CloudUpload: "Upload",
Delete: "Trash2",
DeleteOutlineRounded: "Trash2",
Email: "Mail",
ErrorOutline: "AlertTriangle",
ErrorOutlineOutlined: "AlertTriangle",
Image: "ImageIcon",
InfoOutlined: "Info",
KeyboardArrowDown: "ChevronDown",
Menu: "Menu",
PauseCircleOutline: "PauseCircle",
PauseOutlined: "Pause",
PlayArrowOutlined: "Play",
PlayCircleOutlineRounded: "PlayCircle",
RadioButtonChecked: "Circle",
ReorderRounded: "GripVertical",
SettingsOutlined: "Settings",
Share: "Share2",
Visibility: "Eye",
VisibilityOff: "EyeOff",
WarningAmberOutlined: "AlertTriangle",
};
-198
View File
@@ -1,198 +0,0 @@
import { useTheme } from "@mui/material";
import PropTypes from "prop-types";
import {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckCircle2,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image as ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
} from "lucide-react";
/**
* Map of icon names to Lucide icon components.
* Only icons explicitly imported here will be included in the bundle (tree-shaking).
* To add a new icon: import it above and add it to this object.
*/
const iconComponents = {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckCircle2,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image: ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
};
/**
* Theme-aware icon component wrapping Lucide icons
*
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
* @param {number} size - Icon size in pixels (default: 20)
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
* @param {number} strokeWidth - Stroke width (default: 1.5)
* @param {string} fill - Fill color (default: "none")
*/
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
const theme = useTheme();
// Resolve color from theme path or use direct value
const resolveColor = (colorValue) => {
if (!colorValue) {
return theme.palette.primary.contrastTextTertiary; // Default icon color
}
// If it's a theme path like "primary.contrastText"
if (typeof colorValue === "string" && colorValue.includes(".")) {
const parts = colorValue.split(".");
let resolved = theme.palette;
for (const part of parts) {
resolved = resolved?.[part];
}
return resolved || colorValue;
}
// Direct color value
return colorValue;
};
const LucideIcon = iconComponents[name];
if (!LucideIcon) {
console.warn(`Icon "${name}" not found in Icon component`);
return null;
}
return (
<LucideIcon
size={size}
color={resolveColor(color)}
strokeWidth={strokeWidth}
fill={fill}
{...props}
/>
);
};
Icon.propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.number,
color: PropTypes.string,
strokeWidth: PropTypes.number,
fill: PropTypes.string,
};
export default Icon;
@@ -1,116 +0,0 @@
import PropTypes from "prop-types";
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
/**
* Checkbox Component
*
* A customized checkbox component using Material-UI that supports custom sizing,
* disabled states, and custom icons.
*
* @component
* @param {Object} props - Component properties
* @param {string} props.id - Unique identifier for the checkbox input
* @param {string} [props.name] - Optional name attribute for the checkbox
* @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox
* @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon
* @param {boolean} props.isChecked - Current checked state of the checkbox
* @param {string} [props.value] - Optional value associated with the checkbox
* @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
* @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
*
* @returns {React.ReactElement} Rendered Checkbox component
*
* @example
* // Basic usage
* <Checkbox
* id="terms-checkbox"
* label="I agree to terms"
* isChecked={agreed}
* onChange={handleAgree}
* />
*
* @example
* // With custom size and disabled state
* <Checkbox
* id="advanced-checkbox"
* label="Advanced Option"
* size="large"
* isChecked={isAdvanced}
* isDisabled={!canModify}
* onChange={handleAdvancedToggle}
* />
*/
const Checkbox = ({
id,
name,
label,
size = "medium",
isChecked,
value,
onChange,
isDisabled,
}) => {
/* TODO move sizes to theme */
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
return (
<FormControlLabel
className="checkbox-wrapper"
control={
<MuiCheckbox
checked={isDisabled ? false : isChecked}
name={name}
value={value}
onChange={onChange}
icon={<Icon name="Square" />}
checkedIcon={<Icon name="CheckSquare" />}
inputProps={{
"aria-label": "controlled checkbox",
id: id,
}}
sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
}}
/>
}
label={label}
disabled={isDisabled}
sx={{
borderRadius: theme.shape.borderRadius,
p: theme.spacing(2.5),
"& .MuiButtonBase-root": {
width: theme.spacing(10),
p: 0,
mr: theme.spacing(6),
},
"&:not(:has(.Mui-disabled)):hover": {
backgroundColor: theme.palette.tertiary.main,
},
"& span.MuiTypography-root": {
fontSize: 13,
color: theme.palette.primary.contrastTextTertiary,
},
".MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.primary.contrastTextTertiary,
opacity: 0.25,
},
}}
/>
);
};
Checkbox.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
isChecked: PropTypes.bool.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onChange: PropTypes.func,
isDisabled: PropTypes.bool,
};
export default Checkbox;
@@ -1,63 +0,0 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { MuiColorInput } from "mui-color-input";
/**
*
* @param {*} id The ID of the component
* @param {*} value The color value of the component
* @param {*} error The error of the component
* @param {*} onChange The Change handler function
* @param {*} onBlur The Blur handler function
* @returns The ColorPicker component
* Example usage:
* <ColorPicker
* id="color"
* value={form.color}
* error={errors["color"]}
* onChange={handleColorChange}
* onBlur={handleBlur}
* >
* </ColorPicker>
*/
const ColorPicker = ({ id, name, value, error, onChange, onBlur }) => {
const theme = useTheme();
return (
<Stack gap={theme.spacing(4)}>
<MuiColorInput
format="hex"
name={name}
type="color-picker"
value={value}
id={id}
onChange={(color) => onChange({ target: { name, value: color } })}
onBlur={onBlur}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
);
};
ColorPicker.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string,
error: PropTypes.string,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func,
name: PropTypes.string,
};
export default ColorPicker;
@@ -1,51 +0,0 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const DEFAULT_GAP = 6;
const FieldWrapper = ({
label,
children,
gap,
labelMb,
labelFontWeight = 500,
labelVariant = "h3",
labelSx = {},
sx = {},
}) => {
const theme = useTheme();
return (
<Stack
gap={gap ?? theme.spacing(DEFAULT_GAP)}
sx={sx}
>
{label && (
<Typography
component={labelVariant}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={labelFontWeight}
sx={{
...(labelMb !== undefined && { mb: theme.spacing(labelMb) }),
...labelSx,
}}
>
{label}
</Typography>
)}
{children}
</Stack>
);
};
FieldWrapper.propTypes = {
label: PropTypes.node,
children: PropTypes.node.isRequired,
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
labelMb: PropTypes.number,
labelFontWeight: PropTypes.number,
labelVariant: PropTypes.string,
labelSx: PropTypes.object,
sx: PropTypes.object,
};
export default FieldWrapper;
@@ -1,249 +0,0 @@
// Components
import { Box, Stack, Typography } from "@mui/material";
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";
// Utils
import PropTypes from "prop-types";
import { useCallback, useState, useRef, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
/**
* ImageUpload component allows users to upload images with drag-and-drop functionality.
* It supports file size and format validation.
*
* @component
* @param {Object} props - Component props
* @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round
* @param {string} [props.src] - Source URL of the image to display
* @param {function} props.onChange - Callback function to handle file change, takes a file as an argument
* @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB)
* @param {Array<string>} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats
* @param {Object} [props.errors] - Object containing error messages
* @returns {JSX.Element} The rendered component
*/
const ImageUpload = ({
previewIsRound = false,
src,
onChange,
maxSize = 3 * 1024 * 1024,
accept = ["jpg", "jpeg", "png"],
error,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const [uploadComplete, setUploadComplete] = useState(false);
const [completedFile, setCompletedFile] = useState(null);
const [file, setFile] = useState(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const intervalRef = useRef(null);
const [localError, setLocalError] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
const handleImageChange = useCallback(
(file) => {
if (!file) return;
const isValidType = accept.some((type) => file.type.includes(type));
const isValidSize = file.size <= maxSize;
if (!isValidType) {
setLocalError(t("invalidFileFormat"));
return;
}
if (!isValidSize) {
setLocalError(t("invalidFileSize"));
return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
useEffect(() => {
if (uploadComplete && completedFile) {
onChange?.(completedFile);
setUploadComplete(false);
setCompletedFile(null);
}
}, [uploadComplete, completedFile, onChange]);
return (
<>
{src ? (
<Stack
direction="row"
justifyContent="center"
>
<Image
alt="Uploaded preview"
src={src}
width="250px"
height="250px"
sx={{ ...roundStyle }}
/>
</Stack>
) : (
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDrop={() => setIsDragging(false)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.primary.lowContrast,
backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent",
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
>
<TextField
type="file"
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
alignItems="center"
gap="4px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 0,
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
boxShadow: theme.shape.boxShadow,
}}
>
<Icon
name="Upload"
size={24}
/>
</IconButton>
<Typography
component="h2"
color={theme.palette.primary.contrastTextTertiary}
>
<Typography
component="span"
fontSize="inherit"
color="info"
fontWeight={500}
>
{t("ClickUpload")}
</Typography>{" "}
or {t("DragandDrop")}
</Typography>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB)
</Typography>
</Stack>
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={
<Icon
name="Image"
size={20}
/>
}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
onClick={() => {
clearInterval(intervalRef.current);
setFile(null);
setProgress({ value: 0, isLoading: false });
setLocalError(null);
onChange(undefined);
}}
error={localError || error}
/>
)}
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
{t("SupportedFormats")}: {accept.join(", ").toUpperCase()}
</Typography>
</>
)}
</>
);
};
ImageUpload.propTypes = {
previewIsRound: PropTypes.bool,
src: PropTypes.string,
onChange: PropTypes.func,
maxSize: PropTypes.number,
accept: PropTypes.array,
error: PropTypes.string,
};
export default ImageUpload;
Whitespace-only changes.
@@ -1,117 +0,0 @@
import PropTypes from "prop-types";
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
import "./index.css";
/**
* Radio component.
*
* @component
* @example
* // Usage:
* <Radio
* title="Radio Button Title"
* desc="Radio Button Description"
* size="small"
* />
*
* @param {Object} props - The component
* @param {string} id - The id of the radio button.
* @param {string} title - The title of the radio button.
* @param {string} [desc] - The description of the radio button.
* @param {string} [size="small"] - The size of the radio button.
* @returns {JSX.Element} - The rendered Radio component.
*/
const Radio = ({
name,
checked,
value,
id,
size,
title,
desc,
onChange,
labelSpacing,
}) => {
const theme = useTheme();
return (
<FormControlLabel
className="custom-radio-button"
name={name}
checked={checked}
value={value}
control={
<MUIRadio
id={id}
size={size}
checkedIcon={
<Icon
name="CircleDot"
size={16}
/>
}
sx={{
color: "transparent",
width: 16,
height: 16,
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
"&:not(.Mui-checked)": {
boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline
},
mt: theme.spacing(0.5),
}}
/>
}
onChange={onChange}
label={
<>
<Typography
component="p"
mb={
labelSpacing !== undefined ? theme.spacing(labelSpacing) : theme.spacing(2)
}
>
{title}
</Typography>
<Typography
component="h6"
mt={theme.spacing(1)}
color={theme.palette.primary.contrastTextSecondary}
>
{desc}
</Typography>
</>
}
labelPlacement="end"
sx={{
alignItems: "flex-start",
p: theme.spacing(2.5),
m: theme.spacing(-2.5),
borderRadius: theme.shape.borderRadius,
"&:hover": {
backgroundColor: theme.palette.tertiary.main,
},
"& .MuiButtonBase-root": {
p: 0,
mr: theme.spacing(6),
},
}}
/>
);
};
Radio.propTypes = {
title: PropTypes.string,
desc: PropTypes.string,
size: PropTypes.string,
name: PropTypes.string,
checked: PropTypes.bool,
value: PropTypes.string,
id: PropTypes.string,
onChange: PropTypes.func,
};
export default Radio;
@@ -1,315 +0,0 @@
import PropTypes from "prop-types";
import {
Box,
ListItem,
Autocomplete,
TextField,
Typography,
Checkbox,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import Icon from "../../Icon";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import FieldWrapper from "../FieldWrapper/index.jsx";
/**
* Search component using Material UI's Autocomplete.
*
* @param {Object} props
* @param {string} props.id - Unique identifier for the Autocomplete component
* @param {Array<Object>} props.options - Options to display in the Autocomplete dropdown
* @param {string} props.filteredBy - Key to access the option label from the options
* @param {string} props.value - Current input value for the Autocomplete
* @param {Function} props.handleChange - Function to call when the input changes
* @param {Function} Prop.onBlur - Function to call when the input is blured
* @param {Object} props.sx - Additional styles to apply to the component
* @param {string} props.unit - Label to identify type of options
* @returns {JSX.Element} The rendered Search component
*/
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box height={16}>
<Icon
name="Search"
size={16}
color="primary.contrastTextTertiary"
strokeWidth={1.2}
/>
</Box>
);
};
//TODO keep search state inside of component.
const Search = ({
label,
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
startAdornment,
endAdornment,
onBlur,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
unit = "option",
maxWidth = "100%",
}) => {
const theme = useTheme();
const { t } = useTranslation();
const [selectAll, setSelectAll] = React.useState(false);
const [open, setOpen] = React.useState(false);
const enhancedOptions = React.useMemo(() => {
return multiple && isAdorned
? [
{
[filteredBy]: t("selectAll"),
isSelectAll: true,
_id: "select_all",
id: "select_all",
},
...options,
]
: options;
}, [multiple, isAdorned, options, filteredBy]);
const isOptionSelected = (option) => {
if (!multiple && !isAdorned) return false;
if (Array.isArray(value)) {
return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id));
}
return false;
};
const handleSelectAll = (isSelectAll) => {
const newValue = isSelectAll ? [...options] : [];
handleChange(newValue);
setSelectAll(isSelectAll);
};
useEffect(() => {
const allSelected =
Array.isArray(value) && Array.isArray(options) && value.length === options.length;
if (selectAll !== allSelected) setSelectAll(allSelected);
}, [value, options]);
return (
<Autocomplete
onBlur={onBlur}
multiple={multiple}
id={id}
value={value}
open={open}
onOpen={() => setOpen(true)}
onClose={(event, reason) => {
if (reason === "blur" || reason === "escape") {
setOpen(false);
}
}}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
onChange={(_, newValue) => {
if (multiple && isAdorned) {
const hasSelectAllSelected =
Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
if (hasSelectAllSelected) {
handleSelectAll(!selectAll);
} else {
handleChange(newValue);
setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
}
} else {
handleChange(newValue);
setOpen(false);
}
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={enhancedOptions}
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) =>
(option._id ?? option.id) === (value?._id ?? value?.id)
} // Compare by unique identifier
renderInput={(params) => (
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
slotProps={{
input: {
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
...(startAdornment && { startAdornment: startAdornment }),
...(endAdornment && { endAdornment: endAdornment }),
},
}}
sx={{}}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</FieldWrapper>
)}
filterOptions={(options, { inputValue }) => {
if (inputValue.trim() === "" && multiple && isAdorned) {
return enhancedOptions;
}
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
if (filtered.length === 0) {
return [
{
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
noOptions: true,
},
];
}
return filtered;
}}
getOptionKey={(option) => {
return option._id ?? option.id;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
const port = option["port"];
const selected = isOptionSelected(option);
return (
<ListItem
key={key}
{...optionProps}
sx={
option.noOptions
? {
pointerEvents: "none",
backgroundColor: theme.palette.primary.main,
}
: option.isSelectAll
? {
fontWeight: "bold",
backgroundColor: theme.palette.primary.light,
"&:hover": {
backgroundColor: theme.palette.primary.light,
},
}
: {}
}
>
{multiple && isAdorned && !option.noOptions && (
<Checkbox
checked={option.isSelectAll ? selectAll : selected}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"&.Mui-checked": {
color: theme.palette.secondary.main,
},
padding: 0,
}}
/>
)}
{option[filteredBy] +
(hasSecondaryLabel
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
: "")}
</ListItem>
);
}}
slotProps={{
popper: {
keepMounted: true,
sx: {
"& ul": { p: 2, backgroundColor: theme.palette.primary.main },
"& li.MuiAutocomplete-option": {
color: theme.palette.primary.contrastTextSecondary,
px: 4,
borderRadius: theme.shape.borderRadius,
},
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
backgroundColor: theme.palette.primary.lowContrast,
color: "red",
},
"& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": {
color: theme.palette.secondary.contrastText,
backgroundColor: theme.palette.secondary.main,
},
"& .MuiAutocomplete-noOptions": {
px: theme.spacing(6),
py: theme.spacing(5),
},
},
},
}}
sx={{
/* height: 34,*/
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
},
...sx,
}}
/>
);
};
Search.propTypes = {
label: PropTypes.string,
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
startAdornment: PropTypes.object,
endAdornment: PropTypes.object,
onBlur: PropTypes.func,
unit: PropTypes.string,
};
export default Search;
@@ -1,7 +0,0 @@
.select-wrapper .select-component > .MuiSelect-select {
padding: 0 10px;
min-height: 34px;
display: flex;
align-items: center;
line-height: 1;
}
@@ -1,213 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
import Icon from "../../Icon";
import FieldWrapper from "../FieldWrapper/index.jsx";
import "./index.css";
/**
* @component
* @param {object} props
* @param {string} props.id - The ID attribute for the select element.
* @param {string} props.placeholder - The label of the select element.
* @param {string} props.placeholder - The placeholder text when no option is selected.
* @param {boolean} props.isHidden - Whether the placeholder should be hidden.
* @param {(string | number | boolean)} props.value - The currently selected value.
* @param {object[]} props.items - The array of items to populate in the select dropdown.
* @param {(string | number | boolean)} props.items._id - The unique identifier of each item.
* @param {string} props.items.name - The display name of each item.
* @param {function} props.onChange - The function to handle onChange event.
* @param {object} props.sx - The custom styles object for MUI Select component.
* @param {number} props.maxWidth - Maximum width in pixels for the select component. Enables responsive text truncation.
* @returns {JSX.Element}
*
* @example
* const frequencies = [
* { _id: 1, name: "1 minute" },
* { _id: 2, name: "2 minutes" },
* { _id: 3, name: "3 minutes" },
* ];
*
* <Select
* id="frequency-id"
* name="my-name"
* label="Check frequency"
* placeholder="Select frequency"
* value={value}
* onChange={handleChange}
* items={frequencies}
* />
*/
const Select = ({
id,
label,
placeholder,
isHidden,
value,
items,
onChange,
onBlur,
sx,
error = false,
name = "",
labelControlSpacing = 6,
maxWidth,
//FieldWrapper's props
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
fieldWrapperSx = {},
}) => {
const theme = useTheme();
const getItemValue = (item) => item?._id ?? item?.id;
const itemStyles = {
color: theme.palette.primary.contrastTextTertiary,
borderRadius: theme.shape.borderRadius,
margin: theme.spacing(2),
};
const responsiveMaxWidth = {
xs: `${maxWidth * 0.5}px`,
sm: `${maxWidth * 0.75}px`,
md: `${maxWidth * 0.9}px`,
lg: `${maxWidth}px`,
};
return (
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={labelControlSpacing}
sx={{
...fieldWrapperSx,
}}
>
<MuiSelect
className="select-component"
value={value}
onChange={onChange}
onBlur={onBlur}
displayEmpty
error={error}
name={name}
inputProps={{ id: id }}
IconComponent={(props) => (
<Icon
name="ChevronDown"
size={20}
{...props}
/>
)}
MenuProps={{ disableScrollLock: true }}
sx={{
fontSize: 13,
minWidth: "125px",
...(maxWidth && { maxWidth: responsiveMaxWidth }),
"& fieldset": {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.primary.lowContrast,
},
"&:not(.Mui-focused):hover fieldset": {
borderColor: theme.palette.primary.lowContrast,
},
"& svg path": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& .MuiSelect-select": {
padding: "0",
minHeight: "34px",
display: "flex",
alignItems: "center",
lineHeight: 1,
},
...sx,
}}
renderValue={(selected) => {
const selectedItem = items.find((item) => getItemValue(item) === selected);
const displayName = selectedItem ? selectedItem.name : placeholder;
return (
<Typography
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
title={displayName}
>
{displayName}
</Typography>
);
}}
>
{placeholder && (
<MenuItem
className="select-placeholder"
value="0"
sx={{
display: isHidden ? "none" : "flex",
visibility: isHidden ? "none" : "visible",
...itemStyles,
}}
>
{placeholder}
</MenuItem>
)}
{items
.map((item) => {
const itemValue = getItemValue(item);
if (itemValue === undefined || itemValue === null) {
return null;
}
return (
<MenuItem
value={itemValue}
key={`${id}-${itemValue}`}
sx={{
...itemStyles,
}}
>
{item.name}
</MenuItem>
);
})
.filter(Boolean)}
</MuiSelect>
</FieldWrapper>
);
};
Select.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,
error: PropTypes.bool,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
name: PropTypes.string.isRequired,
})
).isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func,
sx: PropTypes.object,
labelControlSpacing: PropTypes.number,
/**
* Maximum width in pixels. Used to control text truncation and element width.
* Responsive breakpoints will be calculated as percentages of this value.
*/
maxWidth: PropTypes.number,
};
export default Select;
@@ -1,74 +0,0 @@
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import Icon from "../../../Icon";
export const HttpAdornment = ({ https }) => {
const theme = useTheme();
return (
<Stack
direction="row"
alignItems="center"
height="100%"
sx={{
borderRight: `solid 1px ${theme.palette.primary.lowContrast}`,
backgroundColor: theme.palette.tertiary.main,
pl: theme.spacing(6),
}}
>
<Typography
component="h5"
color={theme.palette.primary.contrastTextSecondary}
sx={{ lineHeight: 1, opacity: 0.8 }}
>
{https ? "https" : "http"}
</Typography>
</Stack>
);
};
HttpAdornment.propTypes = {
https: PropTypes.bool.isRequired,
prefix: PropTypes.string,
};
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
const theme = useTheme();
return (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setFieldType(fieldType === "password" ? "text" : "password")}
sx={{
color: theme.palette.primary.lowContrast,
padding: theme.spacing(1),
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
>
{fieldType === "password" ? (
<Icon
name="EyeOff"
size={20}
/>
) : (
<Icon
name="Eye"
size={20}
/>
)}
</IconButton>
</InputAdornment>
);
};
PasswordEndAdornment.propTypes = {
fieldType: PropTypes.string,
setFieldType: PropTypes.func,
};
@@ -1,181 +0,0 @@
import { Stack, TextField, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { forwardRef, useState, cloneElement } from "react";
import PropTypes from "prop-types";
import FieldWrapper from "../FieldWrapper/index.jsx";
const getSx = (theme, type, maxWidth) => {
const sx = {
maxWidth: maxWidth,
"& .MuiFormHelperText-root": {
position: "absolute",
bottom: `-${theme.spacing(24)}`,
minHeight: theme.spacing(24),
},
};
if (type === "url") {
return {
...sx,
"& .MuiInputBase-root": { padding: 0 },
"& .MuiStack-root": {
borderTopLeftRadius: theme.shape.borderRadius,
borderBottomLeftRadius: theme.shape.borderRadius,
},
};
}
return sx;
};
const Required = () => {
const theme = useTheme();
return (
<Typography
component="span"
ml={theme.spacing(1)}
color={theme.palette.error.main}
>
*
</Typography>
);
};
const Optional = ({ optionalLabel }) => {
const theme = useTheme();
return (
<Typography
component="span"
fontSize="inherit"
fontWeight={400}
ml={theme.spacing(2)}
sx={{ opacity: 0.6 }}
>
{optionalLabel || "(optional)"}
</Typography>
);
};
Optional.propTypes = {
optionalLabel: PropTypes.string,
};
const TextInput = forwardRef(
(
{
id,
name,
type,
value,
placeholder,
isRequired,
isOptional,
optionalLabel,
onChange,
onBlur,
error = false,
helperText = null,
startAdornment = null,
endAdornment = null,
label = null,
maxWidth = "100%",
flex,
marginTop,
marginRight,
marginBottom,
marginLeft,
disabled = false,
hidden = false,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
sx = {},
},
ref
) => {
const [fieldType, setFieldType] = useState(type);
const theme = useTheme();
const labelContent = label && (
<>
{label}
{isRequired && <Required />}
{isOptional && <Optional optionalLabel={optionalLabel} />}
</>
);
return (
<FieldWrapper
label={labelContent}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
flex,
display: hidden ? "none" : "",
mt: marginTop,
mr: marginRight,
mb: marginBottom,
ml: marginLeft,
...sx,
}}
>
<TextField
id={id}
name={name}
type={fieldType}
value={value}
placeholder={placeholder}
onChange={onChange}
onBlur={onBlur}
error={error}
helperText={helperText}
inputRef={ref}
sx={getSx(theme, type, maxWidth)}
slotProps={{
input: {
startAdornment: startAdornment,
endAdornment: endAdornment
? cloneElement(endAdornment, { fieldType, setFieldType })
: null,
},
}}
disabled={disabled}
/>
</FieldWrapper>
);
}
);
TextInput.displayName = "TextInput";
TextInput.propTypes = {
type: PropTypes.string,
id: PropTypes.string,
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
placeholder: PropTypes.string,
isRequired: PropTypes.bool,
isOptional: PropTypes.bool,
optionalLabel: PropTypes.string,
onChange: PropTypes.func,
onBlur: PropTypes.func,
error: PropTypes.bool,
helperText: PropTypes.string,
startAdornment: PropTypes.node,
endAdornment: PropTypes.node,
label: PropTypes.string,
maxWidth: PropTypes.string,
flex: PropTypes.number,
marginTop: PropTypes.string,
marginRight: PropTypes.string,
marginBottom: PropTypes.string,
marginLeft: PropTypes.string,
disabled: PropTypes.bool,
hidden: PropTypes.bool,
};
export default TextInput;
-73
View File
@@ -1,73 +0,0 @@
import { Link as MuiLink, useTheme } from "@mui/material";
import { Link as RouterLink } from "react-router-dom";
import PropTypes from "prop-types";
/**
* @component
* @param {Object} props
* @param {'primary' | 'secondary' | 'tertiary' | 'error'} props.level - The level of the link
* @param {string} props.label - The label of the link
* @param {string} props.url - The URL of the link
* @returns {JSX.Element}
*/
const Link = ({ level, label, url, external = true }) => {
const theme = useTheme();
const levelConfig = {
primary: {
color: theme.palette.primary.contrastTextTertiary,
sx: {
":hover": {
color: theme.palette.primary.contrastTextSecondary,
},
},
},
secondary: {
color: theme.palette.primary.contrastTextSecondary,
sx: {
":hover": {
color: theme.palette.primary.contrastTextSecondary,
},
},
},
tertiary: {
color: theme.palette.primary.contrastTextTertiary,
sx: {
textDecoration: "underline",
textDecorationStyle: "dashed",
textDecorationColor: theme.palette.primary.main,
textUnderlineOffset: "1px",
":hover": {
color: theme.palette.primary.contrastTextTertiary,
textDecorationColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.lowContrast,
},
},
},
error: {},
};
const { sx, color } = levelConfig[level];
return (
<MuiLink
component={external ? "a" : RouterLink}
to={external ? undefined : url}
href={external ? url : undefined}
sx={{ width: "fit-content", ...sx }}
color={color}
{...(external && { target: "_blank", rel: "noreferrer" })}
>
{label}
</MuiLink>
);
};
Link.propTypes = {
url: PropTypes.string.isRequired,
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
label: PropTypes.string.isRequired,
external: PropTypes.bool,
};
export default Link;
Whitespace-only changes.
Loaded 100 of 268 files, more files were not shown because too many files have changed in this diff. Show more