Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad1e261a4 | ||
|
|
05442af050 | ||
|
|
25d538504c | ||
|
|
e823533809 | ||
|
|
a03996d54b |
No files matched your search
@@ -88,4 +88,3 @@ jobs:
|
|||||||
docker compose down
|
docker compose down
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
docker system prune -af
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
*.sh
|
|
||||||
mongo
|
|
||||||
+3
-28
@@ -10,31 +10,6 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Quick Setup Checklist
|
|
||||||
|
|
||||||
Before you dive in, make sure you have these installed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Node.js (v16+ required)
|
|
||||||
node --version
|
|
||||||
|
|
||||||
# Check npm
|
|
||||||
npm --version
|
|
||||||
|
|
||||||
# Check Docker
|
|
||||||
docker --version
|
|
||||||
|
|
||||||
# Check Git
|
|
||||||
git --version
|
|
||||||
```
|
|
||||||
|
|
||||||
**New to contributing?** Start here:
|
|
||||||
1. Pick a [`good-first-issue`](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
|
||||||
2. Comment that you'd like to work on it
|
|
||||||
3. Follow the [setup guide](#set-up-checkmate-locally) below
|
|
||||||
4. Join our [Discord](https://discord.com/invite/NAb6H3UTjK) if you get stuck
|
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [How do I...?](#how-do-i)
|
- [How do I...?](#how-do-i)
|
||||||
@@ -91,7 +66,7 @@ cd Checkmate
|
|||||||
Navigate to the Docker dev directory:
|
Navigate to the Docker dev directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker/dev
|
cd server/docker/dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the Docker images:
|
Build the Docker images:
|
||||||
@@ -103,13 +78,13 @@ Build the Docker images:
|
|||||||
Run MongoDB container:
|
Run MongoDB container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
|
docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo
|
||||||
```
|
```
|
||||||
|
|
||||||
Navigate back to the root directory:
|
Navigate back to the root directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ../..
|
cd ../../..
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Step 3: Set Up the Backend (Server)
|
#### Step 3: Set Up the Backend (Server)
|
||||||
|
|||||||
-11325
File diff suppressed because it is too large.
Load diff
@@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
|
|||||||
|
|
||||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||||
|
|
||||||
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||||
|
|
||||||
### Using a Custom CA
|
### Using a Custom CA
|
||||||
|
|
||||||
@@ -91,7 +91,6 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
|
|||||||
- Website monitoring
|
- Website monitoring
|
||||||
- Page speed monitoring
|
- Page speed monitoring
|
||||||
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||||
- Selective disk monitoring with mountpoint selection
|
|
||||||
- Docker monitoring
|
- Docker monitoring
|
||||||
- Ping monitoring
|
- Ping monitoring
|
||||||
- SSL monitoring
|
- SSL monitoring
|
||||||
@@ -163,7 +162,6 @@ Here's how you can contribute:
|
|||||||
4. Open an issue if you believe you've encountered a bug.
|
4. Open an issue if you believe you've encountered a bug.
|
||||||
5. Check for good-first-issue's if you are a newcomer.
|
5. Check for good-first-issue's if you are a newcomer.
|
||||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||||
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
|
||||||
|
|
||||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ cd checkmate/charts/helm/checkmate
|
|||||||
Edit `values.yaml` to update:
|
Edit `values.yaml` to update:
|
||||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||||
- `server.protocol` (usually http or https)
|
- `server.protocol` (usually http or https)
|
||||||
- **If upgrading**: Migrate persistence settings from flat structure to nested:
|
|
||||||
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
|
|
||||||
- Old: `persistence.redisSize` → New: `persistence.redis.size`
|
|
||||||
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
|
|
||||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||||
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
|
|
||||||
|
|
||||||
### 3. Deploy the Helm chart
|
### 3. Deploy the Helm chart
|
||||||
```bash
|
```bash
|
||||||
@@ -42,75 +37,3 @@ kubectl get svc
|
|||||||
```
|
```
|
||||||
|
|
||||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||||
|
|
||||||
## Enabling TLS/HTTPS with cert-manager
|
|
||||||
|
|
||||||
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- cert-manager installed in your cluster
|
|
||||||
- A ClusterIssuer or Issuer configured (e.g., `letsencrypt-prod`)
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
Edit `values.yaml` to enable TLS (and update protocols to https):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
client:
|
|
||||||
protocol: https
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
host: checkmate.example.com
|
|
||||||
className: nginx
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
secretName: checkmate-client-tls
|
|
||||||
|
|
||||||
server:
|
|
||||||
protocol: https
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
host: checkmate.example.com
|
|
||||||
className: nginx
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
secretName: checkmate-server-tls
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternative: Using --set flags
|
|
||||||
|
|
||||||
You can also enable TLS during installation using Helm's `--set` flags:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
helm install checkmate ./charts/helm/checkmate \
|
|
||||||
--set client.protocol=https \
|
|
||||||
--set server.protocol=https \
|
|
||||||
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
|
||||||
--set client.ingress.tls.enabled=true \
|
|
||||||
--set client.ingress.tls.secretName=checkmate-client-tls \
|
|
||||||
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
|
||||||
--set server.ingress.tls.enabled=true \
|
|
||||||
--set server.ingress.tls.secretName=checkmate-server-tls
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
|
|
||||||
After deployment, cert-manager will automatically create the TLS secrets. You can verify the certificate status:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check certificates
|
|
||||||
kubectl get certificates
|
|
||||||
|
|
||||||
# Check certificate details
|
|
||||||
kubectl describe certificate checkmate-client-tls
|
|
||||||
kubectl describe certificate checkmate-server-tls
|
|
||||||
|
|
||||||
# Verify the secrets were created
|
|
||||||
kubectl get secrets | grep checkmate-tls
|
|
||||||
```
|
|
||||||
|
|
||||||
The ingress will automatically use these secrets to enable HTTPS access to your Checkmate instance.
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||||
|
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||||
|
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- $protocol := .Values.server.protocol }}
|
||||||
|
{{- if not (or (eq $protocol "http") (eq $protocol "https")) }}
|
||||||
|
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||||
|
{{- end }}
|
||||||
@@ -11,12 +11,6 @@ metadata:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: {{ .Values.client.ingress.className }}
|
ingressClassName: {{ .Values.client.ingress.className }}
|
||||||
{{- if .Values.client.ingress.tls.enabled }}
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .Values.client.ingress.host }}
|
|
||||||
secretName: {{ default (printf "%s-client-tls" .Release.Name) .Values.client.ingress.tls.secretName }}
|
|
||||||
{{- end }}
|
|
||||||
rules:
|
rules:
|
||||||
- host: {{ .Values.client.ingress.host }}
|
- host: {{ .Values.client.ingress.host }}
|
||||||
http:
|
http:
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ spec:
|
|||||||
- metadata:
|
- metadata:
|
||||||
name: checkmate-mongo-persistent-storage
|
name: checkmate-mongo-persistent-storage
|
||||||
spec:
|
spec:
|
||||||
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
|
storageClassName: "gp3"
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.persistence.mongo.size | default "5Gi" | quote }}
|
storage: {{ .Values.persistence.mongodbSize }}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
|
||||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
|
||||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- $serverProtocol := .Values.server.protocol }}
|
|
||||||
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
|
|
||||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- $clientProtocol := .Values.client.protocol }}
|
|
||||||
{{- if not (or (eq $clientProtocol "http") (eq $clientProtocol "https")) }}
|
|
||||||
{{- fail "client.protocol must be either 'http' or 'https'" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/* Enforce protocol when TLS is enabled to avoid mixed-content */}}
|
|
||||||
{{- if and .Values.client.ingress.tls.enabled (ne $clientProtocol "https") }}
|
|
||||||
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
|
|
||||||
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/* If client runs on https, API must also be https to avoid mixed content */}}
|
|
||||||
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
|
|
||||||
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
|
|
||||||
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
|
|
||||||
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
|
|
||||||
|
|
||||||
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
|
|
||||||
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
|
|
||||||
|
|
||||||
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
|
|
||||||
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
|
|
||||||
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
|
|
||||||
@@ -25,9 +25,9 @@ spec:
|
|||||||
- metadata:
|
- metadata:
|
||||||
name: checkmate-redis-persistent-storage
|
name: checkmate-redis-persistent-storage
|
||||||
spec:
|
spec:
|
||||||
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
|
storageClassName: "gp3"
|
||||||
accessModes: ["ReadWriteOnce"]
|
accessModes: ["ReadWriteOnce"]
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
|
storage: {{ .Values.persistence.redisSize }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -18,12 +18,6 @@ metadata:
|
|||||||
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
|
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: {{ .Values.server.ingress.className }}
|
ingressClassName: {{ .Values.server.ingress.className }}
|
||||||
{{- if .Values.server.ingress.tls.enabled }}
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .Values.server.ingress.host }}
|
|
||||||
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
|
|
||||||
{{- end }}
|
|
||||||
rules:
|
rules:
|
||||||
- host: {{ .Values.server.ingress.host }}
|
- host: {{ .Values.server.ingress.host }}
|
||||||
http:
|
http:
|
||||||
|
|||||||
@@ -7,15 +7,6 @@ client:
|
|||||||
host: change_me
|
host: change_me
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations: {}
|
annotations: {}
|
||||||
# Example annotations for cert-manager:
|
|
||||||
# annotations:
|
|
||||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
enabled: false
|
|
||||||
# secretName: {{ .Release.Name }}-client-tls # Optional; defaults to <release>-client-tls if omitted
|
|
||||||
# Note: when enabling TLS, also set client.protocol: https and add
|
|
||||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
|
||||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
|
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
|
||||||
@@ -26,15 +17,6 @@ server:
|
|||||||
host: change_me
|
host: change_me
|
||||||
className: nginx
|
className: nginx
|
||||||
annotations: {}
|
annotations: {}
|
||||||
# Example annotations for cert-manager:
|
|
||||||
# annotations:
|
|
||||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
tls:
|
|
||||||
enabled: false
|
|
||||||
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
|
|
||||||
# Note: when enabling TLS, also set server.protocol: https and add
|
|
||||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
|
||||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -62,9 +44,5 @@ secrets:
|
|||||||
# REFRESH_TOKEN_TTL: 99d
|
# REFRESH_TOKEN_TTL: 99d
|
||||||
|
|
||||||
persistence:
|
persistence:
|
||||||
mongo:
|
mongodbSize: 5Gi
|
||||||
size: 5Gi
|
redisSize: 1Gi
|
||||||
storageClass: ""
|
|
||||||
redis:
|
|
||||||
size: 1Gi
|
|
||||||
storageClass: ""
|
|
||||||
@@ -29,4 +29,3 @@ dist-ssr
|
|||||||
# Scripts
|
# Scripts
|
||||||
*.sh
|
*.sh
|
||||||
!env.sh
|
!env.sh
|
||||||
.env.development
|
|
||||||
Generated
+20
-624
@@ -97,7 +97,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -409,7 +408,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -453,7 +451,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
|
||||||
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
|
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -499,262 +496,6 @@
|
|||||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||||
@@ -771,134 +512,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
|
||||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@@ -1376,7 +989,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz",
|
||||||
"integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==",
|
"integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.26.0",
|
"@babel/runtime": "^7.26.0",
|
||||||
"@mui/core-downloads-tracker": "^6.4.11",
|
"@mui/core-downloads-tracker": "^6.4.11",
|
||||||
@@ -1541,6 +1153,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
|
||||||
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
|
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/utils": "^7.1.0",
|
"@mui/utils": "^7.1.0",
|
||||||
@@ -1568,6 +1181,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
||||||
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.2",
|
||||||
@@ -1598,6 +1212,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
|
||||||
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
|
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@emotion/cache": "^11.13.5",
|
"@emotion/cache": "^11.13.5",
|
||||||
@@ -1673,6 +1288,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
|
||||||
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.1",
|
"@babel/runtime": "^7.27.1",
|
||||||
"@mui/types": "^7.4.2",
|
"@mui/types": "^7.4.2",
|
||||||
@@ -2080,123 +1696,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.41.0",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
|
||||||
@@ -2210,71 +1709,6 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.41.0",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
|
||||||
@@ -2301,45 +1735,6 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/address": {
|
"node_modules/@sideway/address": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||||
@@ -2738,7 +2133,6 @@
|
|||||||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
@@ -2760,7 +2154,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz",
|
||||||
"integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==",
|
"integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2824,7 +2217,6 @@
|
|||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3131,7 +2523,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001716",
|
"caniuse-lite": "^1.0.30001716",
|
||||||
"electron-to-chromium": "^1.5.149",
|
"electron-to-chromium": "^1.5.149",
|
||||||
@@ -3544,8 +2935,7 @@
|
|||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
@@ -3952,7 +3342,6 @@
|
|||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -4760,7 +4149,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6"
|
"@babel/runtime": "^7.27.6"
|
||||||
},
|
},
|
||||||
@@ -6028,7 +5416,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -6042,7 +5429,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
||||||
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -6246,8 +5632,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/redux-persist": {
|
"node_modules/redux-persist": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
@@ -6385,7 +5770,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
|
||||||
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
|
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.7"
|
"@types/estree": "1.0.7"
|
||||||
},
|
},
|
||||||
@@ -7021,7 +6405,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -7141,7 +6524,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -7362,6 +6744,20 @@
|
|||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const langMap = {
|
|||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { language = "en" } = useSelector((state) => state.ui);
|
const { language } = useSelector((state) => state.ui);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const handleChange = (event) => {
|
const handleChange = (event) => {
|
||||||
const newLang = event.target.value;
|
const newLang = event.target.value;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
import { createToast } from "../../../Utils/toastUtils.jsx";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
|
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js";
|
||||||
|
|
||||||
const ActionsMenu = ({
|
const ActionsMenu = ({
|
||||||
monitor,
|
monitor,
|
||||||
@@ -31,8 +31,7 @@ const ActionsMenu = ({
|
|||||||
const handleRemove = async (event) => {
|
const handleRemove = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let monitor = { id: actions.id };
|
let monitor = { _id: actions.id };
|
||||||
console.log(monitor);
|
|
||||||
await deleteMonitor({ monitor });
|
await deleteMonitor({ monitor });
|
||||||
updateRowCallback();
|
updateRowCallback();
|
||||||
};
|
};
|
||||||
@@ -40,7 +39,7 @@ const ActionsMenu = ({
|
|||||||
const handlePause = async () => {
|
const handlePause = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await pauseMonitor({ monitorId: monitor.id });
|
await pauseMonitor({ monitorId: monitor._id });
|
||||||
pauseCallback();
|
pauseCallback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createToast({ body: "Failed to pause monitor." });
|
createToast({ body: "Failed to pause monitor." });
|
||||||
@@ -73,7 +72,7 @@ const ActionsMenu = ({
|
|||||||
aria-label="monitor actions"
|
aria-label="monitor actions"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
|
openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
"&:focus": {
|
"&:focus": {
|
||||||
@@ -198,7 +197,6 @@ const ActionsMenu = ({
|
|||||||
confirmationButtonLabel="Delete"
|
confirmationButtonLabel="Delete"
|
||||||
/* Do we need stop propagation? */
|
/* Do we need stop propagation? */
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
console.log(e);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRemove(e);
|
handleRemove(e);
|
||||||
}}
|
}}
|
||||||
@@ -212,7 +210,7 @@ const ActionsMenu = ({
|
|||||||
|
|
||||||
ActionsMenu.propTypes = {
|
ActionsMenu.propTypes = {
|
||||||
monitor: PropTypes.shape({
|
monitor: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
_id: PropTypes.string,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const BarChart = ({ checks = [] }) => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
key={`check-${check?.id}`}
|
key={`check-${check?._id}`}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
className: "bar-tooltip",
|
className: "bar-tooltip",
|
||||||
@@ -145,7 +145,7 @@ const BarChart = ({ checks = [] }) => {
|
|||||||
position="relative"
|
position="relative"
|
||||||
width="9px"
|
width="9px"
|
||||||
height="100%"
|
height="100%"
|
||||||
backgroundColor={theme.palette.primary.lowContrast}
|
backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: theme.spacing(1.5),
|
borderRadius: theme.spacing(1.5),
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ const GenericFallback = ({ children }) => {
|
|||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||||
|
Background="transparent"
|
||||||
alt="Loading animation"
|
alt="Loading animation"
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: theme.spacing(8),
|
borderRadius: theme.spacing(8),
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: "transparent",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Stack, Typography } from "@mui/material";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import Dot from "../Dot/index.jsx";
|
import Dot from "../Dot/index.jsx";
|
||||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
|
||||||
/**
|
/**
|
||||||
* Host component.
|
* Host component.
|
||||||
* This subcomponent receives a params object and displays the host details.
|
* This subcomponent receives a params object and displays the host details.
|
||||||
@@ -15,7 +14,7 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
|||||||
* @param {number} params.percentage - The percentage to display.
|
* @param {number} params.percentage - The percentage to display.
|
||||||
* @returns {React.ElementType} Returns a div element with the host details.
|
* @returns {React.ElementType} Returns a div element with the host details.
|
||||||
*/
|
*/
|
||||||
const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
const Host = ({ url, title, percentageColor, percentage, showURL }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -23,36 +22,21 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
|||||||
direction="row"
|
direction="row"
|
||||||
position="relative"
|
position="relative"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(5)}
|
gap={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Typography
|
{title}
|
||||||
variant="h6"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{percentageColor && percentage && (
|
{percentageColor && percentage && (
|
||||||
<>
|
<>
|
||||||
<Dot />
|
<Dot />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
|
||||||
color: percentageColor,
|
color: percentageColor,
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{percentage}%
|
{percentage}%
|
||||||
</Typography>
|
</Typography>
|
||||||
{status && (
|
|
||||||
<StatusLabel
|
|
||||||
status={status}
|
|
||||||
text={status}
|
|
||||||
customStyles={{ textTransform: "capitalize" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -67,7 +51,6 @@ Host.propTypes = {
|
|||||||
percentage: PropTypes.string,
|
percentage: PropTypes.string,
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
showURL: PropTypes.bool,
|
showURL: PropTypes.bool,
|
||||||
status: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Host;
|
export default Host;
|
||||||
@@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
const I18nLoader = () => {
|
const I18nLoader = () => {
|
||||||
const language = useSelector((state) => state.ui.language ?? "en");
|
const language = useSelector((state) => state.ui.language);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (language && i18n.language !== language) {
|
if (language && i18n.language !== language) {
|
||||||
|
|||||||
@@ -85,12 +85,7 @@ const Search = ({
|
|||||||
const enhancedOptions = React.useMemo(() => {
|
const enhancedOptions = React.useMemo(() => {
|
||||||
return multiple && isAdorned
|
return multiple && isAdorned
|
||||||
? [
|
? [
|
||||||
{
|
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
||||||
[filteredBy]: t("selectAll"),
|
|
||||||
isSelectAll: true,
|
|
||||||
_id: "select_all",
|
|
||||||
id: "select_all",
|
|
||||||
},
|
|
||||||
...options,
|
...options,
|
||||||
]
|
]
|
||||||
: options;
|
: options;
|
||||||
@@ -98,7 +93,7 @@ const Search = ({
|
|||||||
const isOptionSelected = (option) => {
|
const isOptionSelected = (option) => {
|
||||||
if (!multiple && !isAdorned) return false;
|
if (!multiple && !isAdorned) return false;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id));
|
return value.some((item) => item._id === option._id);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -150,9 +145,7 @@ const Search = ({
|
|||||||
disableClearable
|
disableClearable
|
||||||
options={enhancedOptions}
|
options={enhancedOptions}
|
||||||
getOptionLabel={(option) => option[filteredBy]}
|
getOptionLabel={(option) => option[filteredBy]}
|
||||||
isOptionEqualToValue={(option, value) =>
|
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||||
(option._id ?? option.id) === (value?._id ?? value?.id)
|
|
||||||
} // Compare by unique identifier
|
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
label={label}
|
label={label}
|
||||||
@@ -213,7 +206,7 @@ const Search = ({
|
|||||||
return filtered;
|
return filtered;
|
||||||
}}
|
}}
|
||||||
getOptionKey={(option) => {
|
getOptionKey={(option) => {
|
||||||
return option._id ?? option.id;
|
return option._id;
|
||||||
}}
|
}}
|
||||||
renderOption={(props, option) => {
|
renderOption={(props, option) => {
|
||||||
const { key, ...optionProps } = props;
|
const { key, ...optionProps } = props;
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ const Select = ({
|
|||||||
fieldWrapperSx = {},
|
fieldWrapperSx = {},
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const getItemValue = (item) => item?._id ?? item?.id;
|
|
||||||
const itemStyles = {
|
const itemStyles = {
|
||||||
fontSize: "var(--env-var-font-size-medium)",
|
fontSize: "var(--env-var-font-size-medium)",
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
color: theme.palette.primary.contrastTextTertiary,
|
||||||
@@ -124,7 +123,7 @@ const Select = ({
|
|||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
renderValue={(selected) => {
|
renderValue={(selected) => {
|
||||||
const selectedItem = items.find((item) => getItemValue(item) === selected);
|
const selectedItem = items.find((item) => item._id === selected);
|
||||||
const displayName = selectedItem ? selectedItem.name : placeholder;
|
const displayName = selectedItem ? selectedItem.name : placeholder;
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
@@ -153,25 +152,17 @@ const Select = ({
|
|||||||
{placeholder}
|
{placeholder}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{items
|
{items.map((item) => (
|
||||||
.map((item) => {
|
<MenuItem
|
||||||
const itemValue = getItemValue(item);
|
value={item._id}
|
||||||
if (itemValue === undefined || itemValue === null) {
|
key={`${id}-${item._id}`}
|
||||||
return null;
|
sx={{
|
||||||
}
|
...itemStyles,
|
||||||
return (
|
}}
|
||||||
<MenuItem
|
>
|
||||||
value={itemValue}
|
{item.name}
|
||||||
key={`${id}-${itemValue}`}
|
</MenuItem>
|
||||||
sx={{
|
))}
|
||||||
...itemStyles,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter(Boolean)}
|
|
||||||
</MuiSelect>
|
</MuiSelect>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
);
|
);
|
||||||
@@ -188,8 +179,8 @@ Select.propTypes = {
|
|||||||
.isRequired,
|
.isRequired,
|
||||||
items: PropTypes.arrayOf(
|
items: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
.isRequired,
|
||||||
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -147,9 +147,11 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
width={7}
|
||||||
|
height={7}
|
||||||
bgcolor={theme.palette[themeColor].lowContrast}
|
bgcolor={theme.palette[themeColor].lowContrast}
|
||||||
borderRadius="0%"
|
borderRadius="50%"
|
||||||
marginRight="1px"
|
marginRight="5px"
|
||||||
/>
|
/>
|
||||||
</BaseLabel>
|
</BaseLabel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import MenuList from "@mui/material/MenuList";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
import { createToast } from "../../../Utils/toastUtils.jsx";
|
||||||
import { useExportMonitors } from "../../../Hooks/monitorHooks.js";
|
import { useExportMonitors } from "../../../Hooks/v1/monitorHooks.js";
|
||||||
|
|
||||||
const MonitorActions = ({ isLoading }) => {
|
const MonitorActions = ({ isLoading }) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import EmailIcon from "@mui/icons-material/Email";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { usePauseMonitor } from "../../../Hooks/monitorHooks.js";
|
import { usePauseMonitor } from "../../../Hooks/v1/monitorHooks.js";
|
||||||
import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js";
|
import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTestAllNotifications } from "../../../Hooks/useNotifications.js";
|
import { useTestAllNotifications } from "../../../Hooks/v1/useNotifications.js";
|
||||||
/**
|
/**
|
||||||
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
||||||
* It includes status display, pause/resume button, and a configure button for admins.
|
* It includes status display, pause/resume button, and a configure button for admins.
|
||||||
@@ -66,7 +66,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
gap={theme.spacing(2)}
|
gap={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={monitor?.id}
|
key={monitor?._id}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={tooltipTitle}
|
title={tooltipTitle}
|
||||||
>
|
>
|
||||||
@@ -78,7 +78,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
startIcon={<EmailIcon />}
|
startIcon={<EmailIcon />}
|
||||||
disabled={isTestNotificationsDisabled}
|
disabled={isTestNotificationsDisabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
testAllNotifications({ monitorId: monitor?.id });
|
testAllNotifications({ monitorId: monitor?._id });
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
@@ -92,7 +92,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
navigate(`/incidents/${monitor?.id}`);
|
navigate(`/incidents/${monitor?._id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("menu.incidents")}
|
{t("menu.incidents")}
|
||||||
@@ -107,7 +107,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pauseMonitor({
|
pauseMonitor({
|
||||||
monitorId: monitor?.id,
|
monitorId: monitor?._id,
|
||||||
triggerUpdate,
|
triggerUpdate,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -120,7 +120,7 @@ const MonitorDetailsControlHeader = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={<SettingsOutlinedIcon />}
|
startIcon={<SettingsOutlinedIcon />}
|
||||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
|
||||||
>
|
>
|
||||||
Configure
|
Configure
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Dot from "../Dot/index.jsx";
|
|||||||
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
|
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
|
||||||
import { formatMonitorUrl } from "../../../Utils/utils.js";
|
import { formatMonitorUrl } from "../../../Utils/utils.js";
|
||||||
/**
|
/**
|
||||||
* Status component displays the status information of a monitor.
|
* Status component displays the status information of a monitor.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Fallback from "../Fallback/index.jsx";
|
|||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
const PageStateWrapper = ({
|
const PageStateWrapper = ({
|
||||||
networkError,
|
networkError,
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const getMenu = (t) => [
|
|||||||
path: "notifications",
|
path: "notifications",
|
||||||
icon: <Notifications />,
|
icon: <Notifications />,
|
||||||
},
|
},
|
||||||
{ name: t("menu.checks"), path: "checks", icon: <Docs /> },
|
|
||||||
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||||
|
|
||||||
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||||
@@ -84,7 +83,7 @@ const Sidebar = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Redux state
|
// Redux state
|
||||||
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
|
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||||
|
|
||||||
const menu = getMenu(t);
|
const menu = getMenu(t);
|
||||||
const otherMenuItems = getOtherMenuItems(t);
|
const otherMenuItems = getOtherMenuItems(t);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
|
|||||||
import Image from "../Image/index.jsx";
|
import Image from "../Image/index.jsx";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StatBox Component
|
* StatBox Component
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Settings from "@/assets/icons/settings-bold.svg?react";
|
||||||
|
|
||||||
|
export type ActionMenuItem = {
|
||||||
|
id: number | string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
action: Function;
|
||||||
|
closeMenu?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | any>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<any>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (e: React.MouseEvent<any>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton onClick={handleClick}>
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.id}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (item.closeMenu) handleClose(e);
|
||||||
|
item.action();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const ArrowLeft = ({
|
||||||
|
type,
|
||||||
|
color = "#667085",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
type?: string;
|
||||||
|
color?: string | undefined;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => {
|
||||||
|
if (type === "double") {
|
||||||
|
return (
|
||||||
|
<LeftArrowDouble
|
||||||
|
style={{ color }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === "long") {
|
||||||
|
return (
|
||||||
|
<LeftArrowLong
|
||||||
|
style={{ color }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<LeftArrow
|
||||||
|
style={{ color }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import RightArrow from "@/assets/icons/right-arrow.svg?react";
|
||||||
|
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
|
||||||
|
|
||||||
|
export const ArrowRight = ({
|
||||||
|
type,
|
||||||
|
color = "#667085",
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
type?: string;
|
||||||
|
color?: string | undefined;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => {
|
||||||
|
if (type === "double") {
|
||||||
|
return (
|
||||||
|
<RightArrowDouble
|
||||||
|
style={{ color }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<RightArrow
|
||||||
|
style={{ color }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { HeaderAuth } from "@/Components/v2/Auth";
|
||||||
|
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||||
|
|
||||||
|
import type { StackProps } from "@mui/material/Stack";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
|
||||||
|
interface AuthBasePageProps extends StackProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthBasePage: React.FC<AuthBasePageProps> = ({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(10)}
|
||||||
|
minHeight="100vh"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<HeaderAuth />
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
margin="auto"
|
||||||
|
width="100%"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={{ xs: 60, sm: 70, md: 80 }}
|
||||||
|
mb={theme.spacing(10)}
|
||||||
|
>
|
||||||
|
<Logo style={{ width: "100%", height: "100%" }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h1">{title}</Typography>
|
||||||
|
<Typography variant="h1">{subtitle}</Typography>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs";
|
||||||
|
|
||||||
|
export const HeaderAuth = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
width={"100%"}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
py={theme.spacing(4)}
|
||||||
|
px={theme.spacing(12)}
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<LanguageSelector />
|
||||||
|
<ThemeSwitch color="red" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { HeaderAuth } from "./HeaderAuth";
|
||||||
|
export { AuthBasePage } from "./AuthBasePage";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { SxProps } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
|
||||||
|
|
||||||
|
export const BaseBox: React.FC<BaseBoxProps> = ({ children, sx }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
border: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.palette.primary.lowContrast,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { ErrorFallback, EmptyFallback } from "./Fallback";
|
||||||
|
|
||||||
|
import type { StackProps } from "@mui/material/Stack";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
interface BasePageProps extends StackProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasePage: React.FC<BasePageProps> = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
spacing={theme.spacing(10)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BasePageWithStatesProps extends StackProps {
|
||||||
|
loading: boolean;
|
||||||
|
error: any;
|
||||||
|
items: any[];
|
||||||
|
page: string;
|
||||||
|
actionLink?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmpty = (items: any[]) => {
|
||||||
|
if (!items) return true;
|
||||||
|
if (Array.isArray(items) && items.length === 0) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BasePageWithStates: React.FC<BasePageWithStatesProps> = ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
page,
|
||||||
|
actionLink,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BasePageWithStatesProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorFallback
|
||||||
|
title="Something went wrong..."
|
||||||
|
subtitle="Please try again later"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(items)) {
|
||||||
|
return (
|
||||||
|
<EmptyFallback
|
||||||
|
page={page}
|
||||||
|
title={t(`${page}Monitor.fallback.title`)}
|
||||||
|
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
|
||||||
|
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
|
||||||
|
actionLink={actionLink || ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BasePage {...props}>{children}</BasePage>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const BulletPointCheck = ({
|
||||||
|
text,
|
||||||
|
variant = "info",
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
noHighlightText?: string;
|
||||||
|
variant?: "success" | "error" | "info";
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const colors: Record<string, string | undefined> = {
|
||||||
|
success: theme.palette.success.main,
|
||||||
|
error: theme.palette.error.main,
|
||||||
|
info: theme.palette.primary.contrastTextSecondary,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
className="check"
|
||||||
|
gap={theme.spacing(6)}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<CheckOutlined />
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
color={
|
||||||
|
variant === "info"
|
||||||
|
? theme.palette.primary.contrastTextSecondary
|
||||||
|
: colors[variant]
|
||||||
|
}
|
||||||
|
fontWeight={450}
|
||||||
|
sx={{
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const Dot = ({
|
||||||
|
color = "gray",
|
||||||
|
size = "4px",
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
color?: string;
|
||||||
|
size?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
content: '""',
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: color,
|
||||||
|
opacity: 0.8,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import OutputAnimation from "@/assets/Animations/output.gif";
|
||||||
|
import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { BulletPointCheck } from "@/Components/v2/DesignElements";
|
||||||
|
import { Button } from "@/Components/v2/Inputs";
|
||||||
|
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import type { BoxProps } from "@mui/material";
|
||||||
|
|
||||||
|
interface BaseFallbackProps extends BoxProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseFallback: React.FC<BaseFallbackProps> = ({ children, ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const mode = useSelector((state: any) => state.ui.mode);
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
margin={isSmall ? "inherit" : "auto"}
|
||||||
|
marginTop={isSmall ? "33%" : "auto"}
|
||||||
|
width={{
|
||||||
|
sm: "90%",
|
||||||
|
md: "70%",
|
||||||
|
lg: "50%",
|
||||||
|
xl: "40%",
|
||||||
|
}}
|
||||||
|
padding={theme.spacing(16)}
|
||||||
|
bgcolor={theme.palette.primary.main}
|
||||||
|
position="relative"
|
||||||
|
border={1}
|
||||||
|
borderColor={theme.palette.primary.lowContrast}
|
||||||
|
borderRadius={theme.shape.borderRadius}
|
||||||
|
overflow="hidden"
|
||||||
|
sx={{
|
||||||
|
borderStyle: "dashed",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(20)}
|
||||||
|
sx={{
|
||||||
|
width: "fit-content",
|
||||||
|
margin: "auto",
|
||||||
|
marginTop: "100px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||||
|
bgcolor="transparent"
|
||||||
|
alt="Loading animation"
|
||||||
|
width="100%"
|
||||||
|
sx={{
|
||||||
|
zIndex: 1,
|
||||||
|
border: "none",
|
||||||
|
borderRadius: theme.spacing(8),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
alignItems="center"
|
||||||
|
maxWidth={"300px"}
|
||||||
|
zIndex={1}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorFallback = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
marginY={theme.spacing(4)}
|
||||||
|
color={theme.palette.primary.contrastTextTertiary}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{subtitle}</Typography>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyFallback = ({
|
||||||
|
page,
|
||||||
|
title,
|
||||||
|
bullets,
|
||||||
|
actionButtonText,
|
||||||
|
actionLink,
|
||||||
|
}: {
|
||||||
|
page: string;
|
||||||
|
title: string;
|
||||||
|
bullets: any;
|
||||||
|
actionButtonText: string;
|
||||||
|
actionLink: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(10)}
|
||||||
|
zIndex={1}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h1"
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bullets?.map((bullet: string, index: number) => (
|
||||||
|
<BulletPointCheck
|
||||||
|
text={bullet}
|
||||||
|
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="accent"
|
||||||
|
onClick={() => navigate(actionLink)}
|
||||||
|
>
|
||||||
|
{actionButtonText}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
export const PulseDot = ({ color }: { color: string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
width="26px"
|
||||||
|
height="24px"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
minWidth="18px"
|
||||||
|
minHeight="18px"
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
backgroundColor: color,
|
||||||
|
borderRadius: "50%",
|
||||||
|
"&::before": {
|
||||||
|
content: `""`,
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
borderRadius: "50%",
|
||||||
|
animation: "ripple 1.8s ease-out infinite",
|
||||||
|
},
|
||||||
|
"&::after": {
|
||||||
|
content: `""`,
|
||||||
|
position: "absolute",
|
||||||
|
width: "7px",
|
||||||
|
height: "7px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: theme.palette.accent.contrastText,
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
export const SplitBox = ({
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}: {
|
||||||
|
left: React.ReactNode;
|
||||||
|
right: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction={isSmall ? "column" : "row"}
|
||||||
|
bgcolor={theme.palette.primary.main}
|
||||||
|
border={1}
|
||||||
|
borderColor={theme.palette.primary.lowContrast}
|
||||||
|
borderRadius={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
padding={theme.spacing(15)}
|
||||||
|
borderRight={isSmall ? 0 : 1}
|
||||||
|
borderBottom={isSmall ? 1 : 0}
|
||||||
|
borderColor={theme.palette.primary.lowContrast}
|
||||||
|
flex={0.7}
|
||||||
|
>
|
||||||
|
{left}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
padding={theme.spacing(15)}
|
||||||
|
>
|
||||||
|
{right}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigBox = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
rightContent,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
rightContent: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<SplitBox
|
||||||
|
left={
|
||||||
|
<Fragment>
|
||||||
|
<Typography
|
||||||
|
component="h2"
|
||||||
|
variant="h2"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography component="p">{subtitle}</Typography>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
right={rightContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
|
||||||
|
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||||
|
|
||||||
|
type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
|
||||||
|
|
||||||
|
export const GradientBox: React.FC<GradientBox> = ({ children, palette }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
const bg = palette
|
||||||
|
? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
|
||||||
|
: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||||
|
width: isSmall
|
||||||
|
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
|
||||||
|
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||||
|
|
||||||
|
background: bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatBoxProps = React.PropsWithChildren<{
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
palette?: PaletteKey;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const StatBox: React.FC<StatBoxProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
palette,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientBox palette={palette}>
|
||||||
|
<Stack>
|
||||||
|
<Typography color={textColor}>{title}</Typography>
|
||||||
|
<Typography color={textColor}>{subtitle}</Typography>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</GradientBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||||
|
import Background from "@/assets/Images/background-grid.svg?react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type StatusBoxProps = React.PropsWithChildren<{}>;
|
||||||
|
|
||||||
|
export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
flex: 1,
|
||||||
|
padding: theme.spacing(8),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="-10%"
|
||||||
|
left="5%"
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBox = ({
|
||||||
|
label,
|
||||||
|
n,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
n: number;
|
||||||
|
color: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BGBox>
|
||||||
|
<Stack spacing={theme.spacing(8)}>
|
||||||
|
<Typography
|
||||||
|
variant={"h2"}
|
||||||
|
textTransform="uppercase"
|
||||||
|
color={theme.palette.primary.contrastTextTertiary}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</BGBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("monitorStatus.up")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.success.lowContrast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DownStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("monitorStatus.down")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.error.lowContrast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("monitorStatus.paused")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.warning.lowContrast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||||
|
import type { MonitorStatus } from "@/Types/Monitor";
|
||||||
|
|
||||||
|
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const StatusLabel = ({
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
status: MonitorStatus;
|
||||||
|
isActive?: boolean;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const palette = getStatusPalette(status);
|
||||||
|
const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: theme.spacing(3, 5),
|
||||||
|
color: theme.palette[palette].main,
|
||||||
|
borderColor: theme.palette[palette].lowContrast,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={7}
|
||||||
|
height={7}
|
||||||
|
bgcolor={theme.palette[palette].lowContrast}
|
||||||
|
borderRadius="50%"
|
||||||
|
marginRight="5px"
|
||||||
|
/>
|
||||||
|
{isActive === false ? "Paused" : transformedText}
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||||
|
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||||
|
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
|
||||||
|
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import type { TablePaginationProps } from "@mui/material/TablePagination";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
export type Header<T> = {
|
||||||
|
id: number | string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||||
|
render: (row: T) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
||||||
|
headers: Header<T>[];
|
||||||
|
data: T[];
|
||||||
|
onRowClick?: (row: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataTable<
|
||||||
|
T extends {
|
||||||
|
id?: string | number;
|
||||||
|
_id?: string | number;
|
||||||
|
onRowClick?: (row: T) => void;
|
||||||
|
},
|
||||||
|
>({ headers, data, onRowClick }: DataTableProps<T>) {
|
||||||
|
const theme = useTheme();
|
||||||
|
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table
|
||||||
|
stickyHeader
|
||||||
|
sx={{
|
||||||
|
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
||||||
|
paddingLeft: theme.spacing(8),
|
||||||
|
},
|
||||||
|
"& :is(th)": {
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
color: theme.palette.secondary.contrastText,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
"& :is(td)": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: theme.palette.primary.contrastTextSecondary,
|
||||||
|
},
|
||||||
|
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{headers.map((header, idx) => {
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
align={idx === 0 ? "left" : "center"}
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
{header.content}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((row) => {
|
||||||
|
const key = row.id || row._id || Math.random();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={key}
|
||||||
|
sx={{ cursor: onRowClick ? "pointer" : "default" }}
|
||||||
|
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||||
|
>
|
||||||
|
{headers.map((header, index) => {
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
align={index === 0 ? "left" : "center"}
|
||||||
|
key={header.id}
|
||||||
|
onClick={
|
||||||
|
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{header.render(row)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TablePaginationActionsProps {
|
||||||
|
count: number;
|
||||||
|
page: number;
|
||||||
|
rowsPerPage: number;
|
||||||
|
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TablePaginationActions(props: TablePaginationActionsProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { count, page, rowsPerPage, onPageChange } = props;
|
||||||
|
|
||||||
|
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ flexShrink: 0, ml: 2.5 }}
|
||||||
|
className="table-pagination-actions"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFirstPageButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="first page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="previous page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleNextButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="next page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleLastPageButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="last page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pagination: React.FC<TablePaginationProps> = ({ ...props }) => {
|
||||||
|
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<TablePagination
|
||||||
|
ActionsComponent={TablePaginationActions}
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
"& .MuiTablePagination-toolbar": {
|
||||||
|
display: isSmall ? "grid" : "flex",
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-selectLabel": {
|
||||||
|
gridColumn: "1",
|
||||||
|
gridRow: "1",
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-select": {
|
||||||
|
gridColumn: "2",
|
||||||
|
gridRow: "1",
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-displayedRows": {
|
||||||
|
gridColumn: "2",
|
||||||
|
gridRow: "2",
|
||||||
|
justifySelf: "center ",
|
||||||
|
},
|
||||||
|
"& .table-pagination-actions": {
|
||||||
|
gridColumn: "1",
|
||||||
|
gridRow: "2",
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
border: 1,
|
||||||
|
borderColor: theme.palette.primary.lowContrast,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
|
||||||
|
export { BasePage, BasePageWithStates } from "./BasePage";
|
||||||
|
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
|
||||||
|
export { DataTable as Table, Pagination } from "./Table";
|
||||||
|
export { GradientBox, StatBox } from "./StatBox";
|
||||||
|
export { BaseBox } from "./BaseBox";
|
||||||
|
export { StatusLabel } from "./StatusLabel";
|
||||||
|
export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback";
|
||||||
|
export { BulletPointCheck } from "./BulletPointCheck";
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
||||||
|
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||||
|
import { CheckboxInput } from "@/Components/v2/Inputs/Checkbox";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type AutoCompleteInputProps = Omit<
|
||||||
|
AutocompleteProps<any, boolean, boolean, boolean>,
|
||||||
|
"renderInput"
|
||||||
|
> & {
|
||||||
|
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutoCompleteInput: React.FC<AutoCompleteInputProps> = ({ ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
{...props}
|
||||||
|
disableCloseOnSelect
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextInput
|
||||||
|
{...params}
|
||||||
|
placeholder="Type to search"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
getOptionKey={(option) => option._id}
|
||||||
|
renderTags={() => null}
|
||||||
|
renderOption={(props, option, { selected }) => {
|
||||||
|
const { key, ...optionProps } = props;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
{...optionProps}
|
||||||
|
>
|
||||||
|
<CheckboxInput checked={selected} />
|
||||||
|
{option.name}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||||
|
padding: `0 ${theme.spacing(5)}`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import type { ButtonProps } from "@mui/material/Button";
|
||||||
|
|
||||||
|
export const ButtonInput: React.FC<ButtonProps> = ({ sx, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2, ...sx }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||||
|
import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
|
||||||
|
export const ButtonGroupInput: React.FC<ButtonGroupProps> = ({
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ButtonGroup
|
||||||
|
orientation={orientation}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
|
import type { CheckboxProps } from "@mui/material/Checkbox";
|
||||||
|
import CheckboxOutline from "@/assets/icons/checkbox-outline.svg?react";
|
||||||
|
import CheckboxFilled from "@/assets/icons/checkbox-filled.svg?react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
type CheckboxInputProps = CheckboxProps & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckboxInput: React.FC<CheckboxInputProps> = ({ label, ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
{...props}
|
||||||
|
icon={<CheckboxOutline />}
|
||||||
|
checkedIcon={<CheckboxFilled />}
|
||||||
|
sx={{
|
||||||
|
"&:hover": { backgroundColor: "transparent" },
|
||||||
|
"& svg": { width: theme.spacing(8), height: theme.spacing(8) },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
import { Select } from "@/Components/v2/Inputs";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { setLanguage } from "@/Features/UI/uiSlice";
|
||||||
|
import type { SelectChangeEvent } from "@mui/material/Select";
|
||||||
|
|
||||||
|
export const LanguageSelector = () => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const language = useSelector((state: any) => state.ui.language);
|
||||||
|
const languages = Object.keys(i18n.options.resources || {});
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
cs: "cz",
|
||||||
|
ja: "jp",
|
||||||
|
uk: "ua",
|
||||||
|
vi: "vn",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: SelectChangeEvent<unknown>) => {
|
||||||
|
const newLang = event.target.value;
|
||||||
|
dispatch(setLanguage(newLang));
|
||||||
|
};
|
||||||
|
|
||||||
|
const languagesForDisplay = languages.map((l) => {
|
||||||
|
let formattedLanguage = l === "en" ? "gb" : l;
|
||||||
|
formattedLanguage = formattedLanguage.includes("-")
|
||||||
|
? formattedLanguage.split("-")[1].toLowerCase()
|
||||||
|
: formattedLanguage;
|
||||||
|
formattedLanguage = languageMap[formattedLanguage] || formattedLanguage;
|
||||||
|
const flag = formattedLanguage ? `fi fi-${formattedLanguage}` : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={l}
|
||||||
|
value={l}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
{flag && <span className={flag} />}
|
||||||
|
<Typography textTransform={"uppercase"}>{l}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{languagesForDisplay}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import Radio from "@mui/material/Radio";
|
||||||
|
import type { RadioProps } from "@mui/material/Radio";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import RadioChecked from "@/assets/icons/radio-checked.svg?react";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
interface RadioInputProps extends RadioProps {}
|
||||||
|
|
||||||
|
export const RadioInput: React.FC<RadioInputProps> = ({ ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Radio
|
||||||
|
{...props}
|
||||||
|
checkedIcon={<RadioChecked />}
|
||||||
|
sx={{
|
||||||
|
color: "transparent",
|
||||||
|
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),
|
||||||
|
padding: 0,
|
||||||
|
"& .MuiSvgIcon-root": {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioWithDescription: React.FC<
|
||||||
|
RadioInputProps & { label: string; description: string }
|
||||||
|
> = ({ label, description, ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
control={<RadioInput {...props} />}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<Typography component="p">{label}</Typography>
|
||||||
|
<Typography
|
||||||
|
component="h6"
|
||||||
|
color={theme.palette.primary.contrastTextSecondary}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import type { SelectProps } from "@mui/material/Select";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const SelectInput: React.FC<SelectProps> = ({ ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
height: "34px",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
borderColor: theme.palette.primary.lowContrast,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.primary.lowContrast,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import type { TextFieldProps } from "@mui/material";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2/palette";
|
||||||
|
|
||||||
|
export const TextInput = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
|
function TextInput(props, ref) {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
inputRef={ref}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
height: 34,
|
||||||
|
fontSize: typographyLevels.base,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
TextInput.displayName = "TextInput";
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const TextLink = ({
|
||||||
|
text,
|
||||||
|
linkText,
|
||||||
|
href,
|
||||||
|
target = "_self",
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
linkText: string;
|
||||||
|
href: string;
|
||||||
|
target?: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<Typography>{text}</Typography>
|
||||||
|
<Link
|
||||||
|
color="accent"
|
||||||
|
to={href}
|
||||||
|
component={RouterLink}
|
||||||
|
target={target}
|
||||||
|
>
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { setMode } from "@/Features/UI/uiSlice.js";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
|
||||||
|
const SunAndMoonIcon = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="sun-and-moon"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
className="moon"
|
||||||
|
id="moon-mask"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="24"
|
||||||
|
cy="10"
|
||||||
|
r="6"
|
||||||
|
fill="#000"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<circle
|
||||||
|
className="sun"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="6"
|
||||||
|
fill={theme.palette.primary.contrastTextSecondary}
|
||||||
|
mask="url(#moon-mask)"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
className="sun-beams"
|
||||||
|
stroke={theme.palette.primary.contrastTextSecondary}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="1"
|
||||||
|
x2="12"
|
||||||
|
y2="3"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="21"
|
||||||
|
x2="12"
|
||||||
|
y2="23"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="4.22"
|
||||||
|
y1="4.22"
|
||||||
|
x2="5.64"
|
||||||
|
y2="5.64"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="18.36"
|
||||||
|
y1="18.36"
|
||||||
|
x2="19.78"
|
||||||
|
y2="19.78"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="1"
|
||||||
|
y1="12"
|
||||||
|
x2="3"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="21"
|
||||||
|
y1="12"
|
||||||
|
x2="23"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="4.22"
|
||||||
|
y1="19.78"
|
||||||
|
x2="5.64"
|
||||||
|
y2="18.36"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="18.36"
|
||||||
|
y1="5.64"
|
||||||
|
x2="19.78"
|
||||||
|
y2="4.22"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeSwitch = ({
|
||||||
|
width = 48,
|
||||||
|
height = 48,
|
||||||
|
}: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) => {
|
||||||
|
const mode = useSelector((state: any) => state.ui.mode);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
id="theme-toggle"
|
||||||
|
title={t("common.buttons.toggleTheme")}
|
||||||
|
className={`theme-${mode}`}
|
||||||
|
aria-label="auto"
|
||||||
|
aria-live="polite"
|
||||||
|
onClick={handleChange}
|
||||||
|
sx={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SunAndMoonIcon />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { ButtonInput as Button } from "./Button";
|
||||||
|
export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup";
|
||||||
|
export { TextInput } from "./TextInput";
|
||||||
|
export { SelectInput as Select } from "./Select";
|
||||||
|
export { LanguageSelector } from "./LanguageSelector";
|
||||||
|
export { ThemeSwitch } from "./ThemeSwitch";
|
||||||
|
export { TextLink } from "./TextLink";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Outlet } from "react-router";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { SideBar } from "@/Components/v2/Layouts/Sidebar";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
const RootLayout = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
minHeight="100vh"
|
||||||
|
>
|
||||||
|
<SideBar />
|
||||||
|
<Stack
|
||||||
|
flex={1}
|
||||||
|
padding={theme.spacing(12)}
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootLayout;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
|
export const BottomControls = ({}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
height={50}
|
||||||
|
py={theme.spacing(4)}
|
||||||
|
px={theme.spacing(8)}
|
||||||
|
gap={theme.spacing(2)}
|
||||||
|
></Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight";
|
||||||
|
import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
|
||||||
|
|
||||||
|
export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const arrowIcon = collapsed ? (
|
||||||
|
<ArrowRight
|
||||||
|
height={theme.spacing(8)}
|
||||||
|
width={theme.spacing(8)}
|
||||||
|
color={theme.palette.primary.contrastTextSecondary}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ArrowLeft
|
||||||
|
height={theme.spacing(8)}
|
||||||
|
width={theme.spacing(8)}
|
||||||
|
color={theme.palette.primary.contrastTextSecondary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||||
|
top: 60,
|
||||||
|
right: 0,
|
||||||
|
transform: `translate(50%, 0)`,
|
||||||
|
backgroundColor: theme.palette.tertiary.main,
|
||||||
|
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||||
|
p: theme.spacing(2.5),
|
||||||
|
|
||||||
|
"&:focus": { outline: "none" },
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.primary.lowContrast,
|
||||||
|
borderColor: theme.palette.primary.lowContrast,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(toggleSidebar());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{arrowIcon}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const Logo = ({ collapsed }: { collapsed: boolean }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
pt={theme.spacing(6)}
|
||||||
|
pb={theme.spacing(12)}
|
||||||
|
pl={theme.spacing(8)}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
pl={theme.spacing("1px")}
|
||||||
|
minWidth={theme.spacing(16)}
|
||||||
|
minHeight={theme.spacing(16)}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
alignItems={"center"}
|
||||||
|
bgcolor={theme.palette.accent.main}
|
||||||
|
borderRadius={theme.shape.borderRadius}
|
||||||
|
color={theme.palette.accent.contrastText}
|
||||||
|
fontSize={18}
|
||||||
|
>
|
||||||
|
C
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
overflow={"hidden"}
|
||||||
|
sx={{
|
||||||
|
transition: "opacity 900ms ease, width 900ms ease",
|
||||||
|
opacity: collapsed ? 0 : 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
width: collapsed ? 0 : "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
lineHeight={1}
|
||||||
|
mt={theme.spacing(2)}
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
variant="h2"
|
||||||
|
>
|
||||||
|
{t("common.appName")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// import Notifications from "@/assets/icons/notifications.svg?react";
|
||||||
|
import Monitors from "@/assets/icons/monitors.svg?react";
|
||||||
|
// import PageSpeed from "@/assets/icons/page-speed.svg?react";
|
||||||
|
// import Integrations from "@/assets/icons/integrations.svg?react";
|
||||||
|
// import Incidents from "@/assets/icons/incidents.svg?react";
|
||||||
|
// import StatusPages from "@/assets/icons/status-pages.svg?react";
|
||||||
|
// import Maintenance from "@/assets/icons/maintenance.svg?react";
|
||||||
|
// import Logs from "@/assets/icons/logs.svg?react";
|
||||||
|
// import Settings from "@/assets/icons/settings.svg?react";
|
||||||
|
import Support from "@/assets/icons/support.svg?react";
|
||||||
|
import Discussions from "@/assets/icons/discussions.svg?react";
|
||||||
|
import Docs from "@/assets/icons/docs.svg?react";
|
||||||
|
import ChangeLog from "@/assets/icons/changeLog.svg?react";
|
||||||
|
|
||||||
|
export const getMenu = (t: Function) => [
|
||||||
|
{ name: t("menu.uptime"), path: "v2/uptime", icon: <Monitors /> },
|
||||||
|
// { name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
||||||
|
|
||||||
|
// { name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
||||||
|
// {
|
||||||
|
// name: t("menu.notifications"),
|
||||||
|
// path: "notifications",
|
||||||
|
// icon: <Notifications />,
|
||||||
|
// },
|
||||||
|
// { name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||||
|
|
||||||
|
// { name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||||
|
// { name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
||||||
|
// { name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: t("menu.settings"),
|
||||||
|
// icon: <Settings />,
|
||||||
|
// path: "settings",
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getBottomMenu = (t: Function) => [
|
||||||
|
{ name: t("menu.support"), path: "support", icon: <Support />, url: "invite" },
|
||||||
|
{
|
||||||
|
name: t("menu.discussions"),
|
||||||
|
path: "discussions",
|
||||||
|
icon: <Discussions />,
|
||||||
|
url: "https://github.com/bluewave-labs/checkmate/discussions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("menu.docs"),
|
||||||
|
path: "docs",
|
||||||
|
icon: <Docs />,
|
||||||
|
url: "https://bluewavelabs.gitbook.io/checkmate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("menu.changelog"),
|
||||||
|
path: "changelog",
|
||||||
|
icon: <ChangeLog />,
|
||||||
|
url: "https://github.com/bluewave-labs/checkmate/releases",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export interface NavData {
|
||||||
|
name: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItem = ({
|
||||||
|
item,
|
||||||
|
collapsed,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
item: NavData;
|
||||||
|
collapsed: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const iconStroke = selected
|
||||||
|
? theme.palette.primary.contrastText
|
||||||
|
: theme.palette.primary.contrastTextTertiary;
|
||||||
|
|
||||||
|
const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
|
||||||
|
const buttonBgHoverColor = selected
|
||||||
|
? theme.palette.secondary.main
|
||||||
|
: theme.palette.tertiary.main;
|
||||||
|
const fontWeight = selected ? 600 : 400;
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
placement="right"
|
||||||
|
title={collapsed ? item.name : ""}
|
||||||
|
slotProps={{
|
||||||
|
popper: {
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "offset",
|
||||||
|
options: {
|
||||||
|
offset: [0, -16],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{
|
||||||
|
backgroundColor: buttonBgColor,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: buttonBgHoverColor,
|
||||||
|
},
|
||||||
|
height: 37,
|
||||||
|
gap: theme.spacing(4),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
px: theme.spacing(4),
|
||||||
|
pl: theme.spacing(5),
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<ListItemIcon
|
||||||
|
sx={{
|
||||||
|
minWidth: 0,
|
||||||
|
"& svg": {
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
opacity: 0.81,
|
||||||
|
},
|
||||||
|
"& svg path": {
|
||||||
|
stroke: iconStroke,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
transition: "opacity 900ms ease",
|
||||||
|
opacity: collapsed ? 0 : 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
sx={{
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ListItemButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import { setCollapsed } from "@/Features/UI/uiSlice";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import List from "@mui/material/List";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo";
|
||||||
|
import { getMenu, getBottomMenu } from "@/Components/v2/Layouts/Sidebar/Menu";
|
||||||
|
import { NavItem } from "@/Components/v2/Layouts/Sidebar/NavItem";
|
||||||
|
import { BottomControls } from "@/Components/v2/Layouts/Sidebar/BottomControls";
|
||||||
|
|
||||||
|
export const COLLAPSED_WIDTH = 64;
|
||||||
|
export const EXPANDED_WIDTH = 250;
|
||||||
|
|
||||||
|
export const SideBar = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const menu = getMenu(t);
|
||||||
|
const bottomMenu = getBottomMenu(t);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setCollapsed({ collapsed: isSmall }));
|
||||||
|
}, [isSmall]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
component="aside"
|
||||||
|
position="sticky"
|
||||||
|
top={0}
|
||||||
|
minHeight={"100vh"}
|
||||||
|
maxHeight={"100vh"}
|
||||||
|
paddingTop={theme.spacing(6)}
|
||||||
|
paddingBottom={theme.spacing(6)}
|
||||||
|
gap={theme.spacing(6)}
|
||||||
|
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||||
|
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
|
||||||
|
sx={{
|
||||||
|
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CollapseButton collapsed={collapsed} />
|
||||||
|
<Logo collapsed={collapsed} />
|
||||||
|
<List
|
||||||
|
component="nav"
|
||||||
|
disablePadding
|
||||||
|
sx={{
|
||||||
|
px: theme.spacing(6),
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menu.map((item) => {
|
||||||
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
key={item.path}
|
||||||
|
item={item}
|
||||||
|
collapsed={collapsed}
|
||||||
|
selected={selected}
|
||||||
|
onClick={() => navigate(`/${item.path}`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
<List
|
||||||
|
component="nav"
|
||||||
|
disablePadding
|
||||||
|
sx={{
|
||||||
|
px: theme.spacing(6),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bottomMenu.map((item) => {
|
||||||
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
key={item.path}
|
||||||
|
item={item}
|
||||||
|
collapsed={collapsed}
|
||||||
|
selected={selected}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.url) {
|
||||||
|
window.open(item.url, "_blank", "noreferrer");
|
||||||
|
} else {
|
||||||
|
navigate(`/${item.path}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
|
||||||
|
<BottomControls />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { BaseChart } from "./HistogramStatus";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
|
||||||
|
import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
||||||
|
|
||||||
|
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const chartData = [
|
||||||
|
{ name: "max", value: max - avg, color: "transparent" },
|
||||||
|
{ name: "avg", value: avg, color: "red" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const palette = getResponseTimeColor(avg);
|
||||||
|
const msg: Record<string, string> = {
|
||||||
|
success: "Excellent",
|
||||||
|
warning: "Average",
|
||||||
|
danger: "Poor",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseChart
|
||||||
|
icon={<AverageResponseIcon />}
|
||||||
|
title={"Average response time"}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
height="100%"
|
||||||
|
position={"relative"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
>
|
||||||
|
<ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height={155}
|
||||||
|
>
|
||||||
|
<RadialBarChart
|
||||||
|
cy="89%"
|
||||||
|
data={chartData}
|
||||||
|
startAngle={180}
|
||||||
|
endAngle={0}
|
||||||
|
innerRadius={"120%"}
|
||||||
|
outerRadius={"200%"}
|
||||||
|
>
|
||||||
|
<RadialBar
|
||||||
|
dataKey="value"
|
||||||
|
background={{ fill: theme.palette[palette].lowContrast }}
|
||||||
|
>
|
||||||
|
<Cell visibility={"hidden"} />
|
||||||
|
<Cell fill={theme.palette[palette].main} />
|
||||||
|
</RadialBar>
|
||||||
|
</RadialBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Stack
|
||||||
|
direction={"row"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">Low</Typography>
|
||||||
|
<Typography variant="body2">High</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
position="absolute"
|
||||||
|
top={"50%"}
|
||||||
|
right={"50%"}
|
||||||
|
sx={{
|
||||||
|
transform: "translate(50%, 0%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
textAlign={"center"}
|
||||||
|
>
|
||||||
|
{msg[palette]}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
textAlign={"center"}
|
||||||
|
>{`${avg?.toFixed()}ms`}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</BaseChart>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { BaseChart } from "./HistogramStatus";
|
||||||
|
import { BaseBox } from "../DesignElements";
|
||||||
|
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
|
||||||
|
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||||
|
import {
|
||||||
|
AreaChart,
|
||||||
|
Area,
|
||||||
|
XAxis,
|
||||||
|
Tooltip,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Text,
|
||||||
|
} from "recharts";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatDateWithTz,
|
||||||
|
tickDateFormatLookup,
|
||||||
|
tooltipDateFormatLookup,
|
||||||
|
} from "@/Utils/TimeUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { GroupedCheck } from "@/Types/Check";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
type XTickProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
payload: { value: any };
|
||||||
|
range: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const XTick: React.FC<XTickProps> = ({ x, y, payload, range }) => {
|
||||||
|
const format = tickDateFormatLookup(range);
|
||||||
|
const theme = useTheme();
|
||||||
|
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
x={x}
|
||||||
|
y={y + 10}
|
||||||
|
textAnchor="middle"
|
||||||
|
fill={theme.palette.primary.contrastTextTertiary}
|
||||||
|
fontSize={11}
|
||||||
|
fontWeight={400}
|
||||||
|
>
|
||||||
|
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseTimeToolTipProps = {
|
||||||
|
active?: boolean | undefined;
|
||||||
|
payload?: any[];
|
||||||
|
label?: string;
|
||||||
|
range: string;
|
||||||
|
theme: any;
|
||||||
|
uiTimezone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseTimeToolTip: React.FC<ResponseTimeToolTipProps> = ({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
label,
|
||||||
|
range,
|
||||||
|
theme,
|
||||||
|
uiTimezone,
|
||||||
|
}) => {
|
||||||
|
if (!label) return null;
|
||||||
|
if (!payload) return null;
|
||||||
|
if (!active) return null;
|
||||||
|
|
||||||
|
const format = tooltipDateFormatLookup(range);
|
||||||
|
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
|
||||||
|
return (
|
||||||
|
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||||
|
<Typography>{formatDateWithTz(label, format, uiTimezone)}</Typography>
|
||||||
|
<Typography>Response time: {responseTime} ms</Typography>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChartResponseTime = ({
|
||||||
|
checks,
|
||||||
|
range,
|
||||||
|
}: {
|
||||||
|
checks: GroupedCheck[];
|
||||||
|
range: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||||
|
const normalized = normalizeResponseTimes<GroupedCheck, "avgResponseTime">(
|
||||||
|
checks,
|
||||||
|
"avgResponseTime"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<BaseChart
|
||||||
|
icon={<ResponseTimeIcon />}
|
||||||
|
title="Response times"
|
||||||
|
>
|
||||||
|
<ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height={300}
|
||||||
|
>
|
||||||
|
<AreaChart data={normalized?.slice().reverse()}>
|
||||||
|
<CartesianGrid
|
||||||
|
stroke={theme.palette.primary.lowContrast}
|
||||||
|
strokeWidth={1}
|
||||||
|
strokeOpacity={1}
|
||||||
|
fill="transparent"
|
||||||
|
vertical={false}
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="colorUv"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="1"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
stopColor={theme.palette.accent.main}
|
||||||
|
stopOpacity={0.8}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
stopColor={theme.palette.accent.light}
|
||||||
|
stopOpacity={0}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<XAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
dataKey="_id"
|
||||||
|
tick={(props) => (
|
||||||
|
<XTick
|
||||||
|
{...props}
|
||||||
|
range={range}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={(props) => (
|
||||||
|
<ResponseTimeToolTip
|
||||||
|
{...props}
|
||||||
|
range={range}
|
||||||
|
theme={theme}
|
||||||
|
uiTimezone={uiTimezone}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="normalResponseTime"
|
||||||
|
stroke={theme.palette.accent.main}
|
||||||
|
fill="url(#colorUv)"
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</BaseChart>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus";
|
||||||
|
import { ButtonGroup, Button } from "@/Components/v2/Inputs";
|
||||||
|
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 BugReportOutlinedIcon from "@mui/icons-material/BugReportOutlined";
|
||||||
|
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
import type { IMonitor } from "@/Types/Monitor";
|
||||||
|
|
||||||
|
export const HeaderControls = ({
|
||||||
|
monitor,
|
||||||
|
patch,
|
||||||
|
isPatching,
|
||||||
|
refetch,
|
||||||
|
}: {
|
||||||
|
monitor: IMonitor;
|
||||||
|
patch: Function;
|
||||||
|
isPatching: boolean;
|
||||||
|
refetch: Function;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction={isSmall ? "column" : "row"}
|
||||||
|
spacing={isSmall ? theme.spacing(4) : 0}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
>
|
||||||
|
<MonitorStatus monitor={monitor} />
|
||||||
|
<Stack
|
||||||
|
direction={"row"}
|
||||||
|
spacing={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
<ButtonGroup
|
||||||
|
orientation={isSmall ? "vertical" : "horizontal"}
|
||||||
|
fullWidth={isSmall}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
<Button startIcon={<EmailIcon />}>{t("sendTestNotifications")}</Button>
|
||||||
|
<Button startIcon={<BugReportOutlinedIcon />}>{t("menu.incidents")}</Button>
|
||||||
|
<Button
|
||||||
|
loading={isPatching}
|
||||||
|
onClick={async () => {
|
||||||
|
await patch(`/monitors/${monitor._id}/active`);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
startIcon={
|
||||||
|
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{monitor?.isActive ? t("pause") : t("resume")}
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={<SettingsOutlinedIcon />}>{t("configure")}</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { Button } from "@/Components/v2/Inputs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
export const HeaderCreate = ({
|
||||||
|
label,
|
||||||
|
isLoading,
|
||||||
|
path,
|
||||||
|
}: {
|
||||||
|
label?: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
path: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="end"
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(6)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
loading={isLoading}
|
||||||
|
variant="contained"
|
||||||
|
color="accent"
|
||||||
|
onClick={() => navigate(path)}
|
||||||
|
>
|
||||||
|
{label || t("createNew")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { ButtonGroup, Button } from "@/Components/v2/Inputs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
export const HeaderRange = ({
|
||||||
|
range,
|
||||||
|
setRange,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
range: string;
|
||||||
|
setRange: Function;
|
||||||
|
loading: boolean;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(9)}
|
||||||
|
direction={isSmall ? "column" : "row"}
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
>
|
||||||
|
<Typography variant="body2">{`Showing statistics for past ${range}`}</Typography>
|
||||||
|
<ButtonGroup
|
||||||
|
orientation={isSmall ? "vertical" : "horizontal"}
|
||||||
|
fullWidth={isSmall}
|
||||||
|
variant="contained"
|
||||||
|
color={"primary"}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={range === "2h" ? "secondary" : "inherit"}
|
||||||
|
onClick={() => setRange("2h")}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Recent
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={range === "24h" ? "secondary" : "inherit"}
|
||||||
|
onClick={() => setRange("24h")}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={range === "7d" ? "secondary" : "inherit"}
|
||||||
|
onClick={() => setRange("7d")}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
7 days
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={range === "30d" ? "secondary" : "inherit"}
|
||||||
|
onClick={() => setRange("30d")}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
30 days
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { Check } from "@/Types/Check";
|
||||||
|
import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip";
|
||||||
|
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||||
|
|
||||||
|
export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
|
||||||
|
const normalChecks = normalizeResponseTimes(checks, "responseTime");
|
||||||
|
let data = Array<any>();
|
||||||
|
|
||||||
|
if (!normalChecks || normalChecks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (normalChecks.length !== 25) {
|
||||||
|
const placeholders = Array(25 - normalChecks.length).fill("placeholder");
|
||||||
|
data = [...normalChecks, ...placeholders];
|
||||||
|
} else {
|
||||||
|
data = normalChecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
flexWrap="nowrap"
|
||||||
|
gap={theme.spacing(1.5)}
|
||||||
|
height="50px"
|
||||||
|
width="fit-content"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
sx={{
|
||||||
|
cursor: "default",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((check, index) => {
|
||||||
|
if (check === "placeholder") {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={`${check}-${index}`}
|
||||||
|
position="relative"
|
||||||
|
width={theme.spacing(4.5)}
|
||||||
|
height="100%"
|
||||||
|
bgcolor={theme.palette.primary.lowContrast}
|
||||||
|
sx={{
|
||||||
|
borderRadius: theme.spacing(1.5),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<HistogramResponseTimeTooltip
|
||||||
|
key={`${check}-${index}`}
|
||||||
|
check={check}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
width="9px"
|
||||||
|
height="100%"
|
||||||
|
bgcolor={theme.palette.primary.lowContrast}
|
||||||
|
sx={{
|
||||||
|
borderRadius: theme.spacing(1.5),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom={0}
|
||||||
|
width="100%"
|
||||||
|
height={`${check.normalResponseTime}%`}
|
||||||
|
bgcolor={
|
||||||
|
check.status
|
||||||
|
? theme.palette.success.lowContrast
|
||||||
|
: theme.palette.error.lowContrast
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
borderRadius: theme.spacing(1.5),
|
||||||
|
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HistogramResponseTimeTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import type { LatestCheck } from "@/Types/Check";
|
||||||
|
|
||||||
|
export const HistogramResponseTimeTooltip: React.FC<{
|
||||||
|
children: React.ReactElement;
|
||||||
|
check: LatestCheck;
|
||||||
|
}> = ({ children, check }) => {
|
||||||
|
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<Stack>
|
||||||
|
<Typography>
|
||||||
|
{formatDateWithTz(check?.checkedAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
||||||
|
</Typography>
|
||||||
|
{check?.responseTime && (
|
||||||
|
<Typography>Response Time: {check.responseTime} ms</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||||
|
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||||
|
import UptimeIcon from "@/assets/icons/uptime-icon.svg?react";
|
||||||
|
import IncidentsIcon from "@/assets/icons/incidents.svg?react";
|
||||||
|
|
||||||
|
import type { GroupedCheck } from "@/Types/Check";
|
||||||
|
import type { MonitorStatus } from "@/Types/Monitor";
|
||||||
|
|
||||||
|
import { normalizeResponseTimes } from "@/Utils/DataUtils";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||||
|
|
||||||
|
const XLabel = ({
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
range,
|
||||||
|
}: {
|
||||||
|
p1: GroupedCheck;
|
||||||
|
p2: GroupedCheck;
|
||||||
|
range: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||||
|
const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<text
|
||||||
|
x={0}
|
||||||
|
y="100%"
|
||||||
|
dy={-3}
|
||||||
|
textAnchor="start"
|
||||||
|
fontSize={11}
|
||||||
|
fill={theme.palette.primary.contrastTextTertiary}
|
||||||
|
>
|
||||||
|
{formatDateWithTz(p1._id, dateFormat, uiTimezone)}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x="100%"
|
||||||
|
y="100%"
|
||||||
|
dy={-3}
|
||||||
|
textAnchor="end"
|
||||||
|
fontSize={11}
|
||||||
|
fill={theme.palette.primary.contrastTextTertiary}
|
||||||
|
>
|
||||||
|
{formatDateWithTz(p2._id, dateFormat, uiTimezone)}
|
||||||
|
</text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type BaseChartProps = React.PropsWithChildren<{
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const BaseChart: React.FC<BaseChartProps> = ({ children, icon, title }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
padding: theme.spacing(8),
|
||||||
|
display: "flex",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(8)}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: theme.palette.tertiary.main,
|
||||||
|
"& svg": {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
"& path": {
|
||||||
|
stroke: theme.palette.primary.contrastTextTertiary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</BaseBox>
|
||||||
|
<Typography variant="h2">{title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Box flex={1}>{children}</Box>
|
||||||
|
</Stack>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HistogramStatus = ({
|
||||||
|
checks,
|
||||||
|
status,
|
||||||
|
range,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
checks: GroupedCheck[];
|
||||||
|
status: MonitorStatus;
|
||||||
|
range: string;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||||
|
|
||||||
|
const icon = status === "up" ? <UptimeIcon /> : <IncidentsIcon />;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [idx, setIdx] = useState<number | null>(null);
|
||||||
|
const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D";
|
||||||
|
const normalChecks = normalizeResponseTimes(checks, "avgResponseTime");
|
||||||
|
|
||||||
|
if (normalChecks.length === 0) {
|
||||||
|
return (
|
||||||
|
<BaseChart
|
||||||
|
icon={icon}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
height={"100%"}
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h2">
|
||||||
|
{status === "up" ? "No checks yet" : "Great, no downtime yet!"}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</BaseChart>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalChecks = normalChecks.reduce((count, check) => {
|
||||||
|
return count + check.count;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseChart
|
||||||
|
icon={icon}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<Stack gap={theme.spacing(8)}>
|
||||||
|
<Stack
|
||||||
|
position="relative"
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Typography>Total checks</Typography>
|
||||||
|
{idx ? (
|
||||||
|
<Stack>
|
||||||
|
<Typography variant="h2">{normalChecks[idx].count}</Typography>
|
||||||
|
<Typography
|
||||||
|
position={"absolute"}
|
||||||
|
top={"100%"}
|
||||||
|
>
|
||||||
|
{formatDateWithTz(normalChecks[idx]._id, dateFormat, uiTimezone)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Typography variant="h2">{totalChecks}</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height={155}
|
||||||
|
>
|
||||||
|
<BarChart data={normalChecks}>
|
||||||
|
<XAxis
|
||||||
|
stroke={theme.palette.primary.lowContrast}
|
||||||
|
height={15}
|
||||||
|
tick={false}
|
||||||
|
label={
|
||||||
|
<XLabel
|
||||||
|
p1={normalChecks[0]}
|
||||||
|
p2={normalChecks[normalChecks.length - 1]}
|
||||||
|
range={range}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="normalResponseTime"
|
||||||
|
maxBarSize={7}
|
||||||
|
background={{ fill: "transparent" }}
|
||||||
|
>
|
||||||
|
{normalChecks?.map((groupedCheck, idx) => {
|
||||||
|
const fillColor = getResponseTimeColor(groupedCheck.normalResponseTime);
|
||||||
|
return (
|
||||||
|
<Cell
|
||||||
|
onMouseEnter={() => setIdx(idx)}
|
||||||
|
onMouseLeave={() => setIdx(null)}
|
||||||
|
key={groupedCheck._id}
|
||||||
|
fill={theme.palette[fillColor].main}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Stack>
|
||||||
|
</BaseChart>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { IMonitor } from "@/Types/Monitor";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { PulseDot } from "@/Components/v2/DesignElements/PulseDot";
|
||||||
|
import { Dot } from "@/Components/v2/DesignElements/Dot";
|
||||||
|
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import prettyMilliseconds from "pretty-ms";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2/palette";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
if (!monitor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
fontSize={typographyLevels.xl}
|
||||||
|
fontWeight={500}
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
overflow={"hidden"}
|
||||||
|
textOverflow={"ellipsis"}
|
||||||
|
whiteSpace={"nowrap"}
|
||||||
|
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||||
|
<Typography
|
||||||
|
color={theme.palette.primary.contrastTextSecondary}
|
||||||
|
fontSize={typographyLevels.l}
|
||||||
|
fontWeight={"bolder"}
|
||||||
|
fontFamily={"monospace"}
|
||||||
|
overflow={"hidden"}
|
||||||
|
textOverflow={"ellipsis"}
|
||||||
|
whiteSpace={"nowrap"}
|
||||||
|
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||||
|
>
|
||||||
|
{formatUrl(monitor?.url)}
|
||||||
|
</Typography>
|
||||||
|
{!isSmall && (
|
||||||
|
<>
|
||||||
|
<Dot />
|
||||||
|
<Typography>
|
||||||
|
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { HeaderCreate } from "./HeaderCreate";
|
||||||
@@ -5,7 +5,6 @@ import axios from "axios";
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
authToken: "",
|
authToken: "",
|
||||||
refreshToken: "",
|
|
||||||
user: "",
|
user: "",
|
||||||
success: null,
|
success: null,
|
||||||
msg: null,
|
msg: null,
|
||||||
@@ -139,7 +138,6 @@ const handleAuthFulfilled = (state, action) => {
|
|||||||
state.success = action.payload.success;
|
state.success = action.payload.success;
|
||||||
state.msg = action.payload.msg;
|
state.msg = action.payload.msg;
|
||||||
state.authToken = action.payload.data.token;
|
state.authToken = action.payload.data.token;
|
||||||
state.refreshToken = action.payload.data.refreshToken || "";
|
|
||||||
state.user = action.payload.data.user;
|
state.user = action.payload.data.user;
|
||||||
};
|
};
|
||||||
const handleAuthRejected = (state, action) => {
|
const handleAuthRejected = (state, action) => {
|
||||||
@@ -190,15 +188,11 @@ const authSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
clearAuthState: (state) => {
|
clearAuthState: (state) => {
|
||||||
state.authToken = "";
|
state.authToken = "";
|
||||||
state.refreshToken = "";
|
|
||||||
state.user = "";
|
state.user = "";
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.success = true;
|
state.success = true;
|
||||||
state.msg = "Logged out successfully";
|
state.msg = "Logged out successfully";
|
||||||
},
|
},
|
||||||
setAuthToken: (state, action) => {
|
|
||||||
state.authToken = action.payload;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// Register thunk
|
// Register thunk
|
||||||
@@ -252,4 +246,4 @@ const authSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default authSlice.reducer;
|
export default authSlice.reducer;
|
||||||
export const { clearAuthState, setAuthToken } = authSlice.actions;
|
export const { clearAuthState } = authSlice.actions;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const v2AuthSlice = createSlice({
|
||||||
|
name: "v2Auth",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setIsAuthenticated: (state, action) => {
|
||||||
|
const { authenticated } = action.payload;
|
||||||
|
state.isAuthenticated = authenticated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default v2AuthSlice.reducer;
|
||||||
|
export const { setIsAuthenticated } = v2AuthSlice.actions;
|
||||||
@@ -58,9 +58,6 @@ const uiSlice = createSlice({
|
|||||||
state.showURL = action.payload;
|
state.showURL = action.payload;
|
||||||
},
|
},
|
||||||
setGreeting(state, action) {
|
setGreeting(state, action) {
|
||||||
if (!state.greeting) {
|
|
||||||
state.greeting = { index: 0, lastUpdate: null };
|
|
||||||
}
|
|
||||||
state.greeting.index = action.payload.index;
|
state.greeting.index = action.payload.index;
|
||||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const useFetchChecksTeam = ({
|
const useFetchChecksTeam = ({
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const useFetchLogs = () => {
|
const useFetchLogs = () => {
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useMonitorUtils } from "./useMonitorUtils.js";
|
import { useMonitorUtils } from "./useMonitorUtils.js";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [monitors, setMonitors] = useState(undefined);
|
const [monitors, setMonitors] = useState(undefined);
|
||||||
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
||||||
@@ -37,7 +37,7 @@ export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) =>
|
|||||||
return [monitors, monitorsSummary, isLoading, networkError];
|
return [monitors, monitorsSummary, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchMonitorsWithChecks = ({
|
const useFetchMonitorsWithChecks = ({
|
||||||
types,
|
types,
|
||||||
limit,
|
limit,
|
||||||
page,
|
page,
|
||||||
@@ -50,7 +50,6 @@ export const useFetchMonitorsWithChecks = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [count, setCount] = useState(undefined);
|
const [count, setCount] = useState(undefined);
|
||||||
const [monitors, setMonitors] = useState(undefined);
|
const [monitors, setMonitors] = useState(undefined);
|
||||||
const [summary, setSummary] = useState(undefined);
|
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -68,14 +67,12 @@ export const useFetchMonitorsWithChecks = ({
|
|||||||
field,
|
field,
|
||||||
order,
|
order,
|
||||||
});
|
});
|
||||||
|
const { count, monitors } = res?.data?.data ?? {};
|
||||||
const { count, monitors, summary } = res?.data?.data ?? {};
|
|
||||||
const mappedMonitors = monitors.map((monitor) =>
|
const mappedMonitors = monitors.map((monitor) =>
|
||||||
getMonitorWithPercentage(monitor, theme)
|
getMonitorWithPercentage(monitor, theme)
|
||||||
);
|
);
|
||||||
setSummary(summary);
|
|
||||||
setMonitors(mappedMonitors);
|
setMonitors(mappedMonitors);
|
||||||
setCount(count || 0);
|
setCount(count?.monitorsCount ?? 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setNetworkError(true);
|
setNetworkError(true);
|
||||||
@@ -99,12 +96,25 @@ export const useFetchMonitorsWithChecks = ({
|
|||||||
types,
|
types,
|
||||||
monitorUpdateTrigger,
|
monitorUpdateTrigger,
|
||||||
]);
|
]);
|
||||||
return [summary, monitors, count, isLoading, networkError];
|
return [monitors, count, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
|
const useFetchMonitorsByTeamId = ({
|
||||||
|
types,
|
||||||
|
limit,
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
|
filter,
|
||||||
|
field,
|
||||||
|
order,
|
||||||
|
checkOrder,
|
||||||
|
normalize,
|
||||||
|
status,
|
||||||
|
updateTrigger,
|
||||||
|
}) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [monitors, setMonitors] = useState(undefined);
|
const [monitors, setMonitors] = useState(undefined);
|
||||||
|
const [summary, setSummary] = useState(undefined);
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,11 +122,20 @@ export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await networkService.getMonitorsByTeamId({
|
const res = await networkService.getMonitorsByTeamId({
|
||||||
|
limit,
|
||||||
types,
|
types,
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
filter,
|
filter,
|
||||||
|
field,
|
||||||
|
order,
|
||||||
|
checkOrder,
|
||||||
|
status,
|
||||||
|
normalize,
|
||||||
});
|
});
|
||||||
if (res?.data?.data) {
|
if (res?.data?.data?.filteredMonitors) {
|
||||||
setMonitors(res.data.data);
|
setMonitors(res.data.data.filteredMonitors);
|
||||||
|
setSummary(res.data.data.summary);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNetworkError(true);
|
setNetworkError(true);
|
||||||
@@ -128,11 +147,23 @@ export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchMonitors();
|
fetchMonitors();
|
||||||
}, [types, filter, updateTrigger]);
|
}, [
|
||||||
return [monitors, isLoading, networkError];
|
types,
|
||||||
|
limit,
|
||||||
|
page,
|
||||||
|
rowsPerPage,
|
||||||
|
filter,
|
||||||
|
field,
|
||||||
|
order,
|
||||||
|
updateTrigger,
|
||||||
|
checkOrder,
|
||||||
|
normalize,
|
||||||
|
status,
|
||||||
|
]);
|
||||||
|
return [monitors, summary, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchStatsByMonitorId = ({
|
const useFetchStatsByMonitorId = ({
|
||||||
monitorId,
|
monitorId,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
limit,
|
limit,
|
||||||
@@ -171,7 +202,7 @@ export const useFetchStatsByMonitorId = ({
|
|||||||
return [monitor, audits, isLoading, networkError];
|
return [monitor, audits, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchGames = async () => {
|
const fetchGames = async () => {
|
||||||
@@ -190,7 +221,7 @@ export const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
|||||||
return [isLoading];
|
return [isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof monitorId === "undefined") {
|
if (typeof monitorId === "undefined") {
|
||||||
@@ -213,7 +244,7 @@ export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) =>
|
|||||||
return [isLoading];
|
return [isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
const [monitor, setMonitor] = useState(undefined);
|
const [monitor, setMonitor] = useState(undefined);
|
||||||
@@ -239,34 +270,8 @@ export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigge
|
|||||||
}, [monitorId, dateRange, updateTrigger]);
|
}, [monitorId, dateRange, updateTrigger]);
|
||||||
return [monitor, isLoading, networkError];
|
return [monitor, isLoading, networkError];
|
||||||
};
|
};
|
||||||
export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [networkError, setNetworkError] = useState(false);
|
|
||||||
const [monitor, setMonitor] = useState(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||||
const fetchMonitor = async () => {
|
|
||||||
try {
|
|
||||||
if (!monitorId) {
|
|
||||||
return { monitor: undefined, isLoading: false, networkError: undefined };
|
|
||||||
}
|
|
||||||
const response = await networkService.getPageSpeedDetailsByMonitorId({
|
|
||||||
monitorId: monitorId,
|
|
||||||
dateRange: dateRange,
|
|
||||||
});
|
|
||||||
setMonitor(response.data.data);
|
|
||||||
} catch (error) {
|
|
||||||
setNetworkError(true);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchMonitor();
|
|
||||||
}, [monitorId, dateRange, updateTrigger]);
|
|
||||||
return [monitor, isLoading, networkError];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
|
||||||
const [networkError, setNetworkError] = useState(false);
|
const [networkError, setNetworkError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [monitor, setMonitor] = useState(undefined);
|
const [monitor, setMonitor] = useState(undefined);
|
||||||
@@ -294,7 +299,7 @@ export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) =>
|
|||||||
return [monitor, monitorStats, isLoading, networkError];
|
return [monitor, monitorStats, isLoading, networkError];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateMonitor = () => {
|
const useCreateMonitor = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const createMonitor = async ({ monitor, redirect }) => {
|
const createMonitor = async ({ monitor, redirect }) => {
|
||||||
@@ -314,7 +319,7 @@ export const useCreateMonitor = () => {
|
|||||||
return [createMonitor, isLoading];
|
return [createMonitor, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchGlobalSettings = () => {
|
const useFetchGlobalSettings = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [globalSettings, setGlobalSettings] = useState(undefined);
|
const [globalSettings, setGlobalSettings] = useState(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -336,14 +341,13 @@ export const useFetchGlobalSettings = () => {
|
|||||||
return [globalSettings, isLoading];
|
return [globalSettings, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteMonitor = () => {
|
const useDeleteMonitor = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const deleteMonitor = async ({ monitor, redirect }) => {
|
const deleteMonitor = async ({ monitor, redirect }) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log(monitor);
|
await networkService.deleteMonitorById({ monitorId: monitor._id });
|
||||||
await networkService.deleteMonitorById({ monitorId: monitor.id });
|
|
||||||
createToast({ body: "Monitor deleted successfully!" });
|
createToast({ body: "Monitor deleted successfully!" });
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
navigate(redirect);
|
navigate(redirect);
|
||||||
@@ -357,7 +361,7 @@ export const useDeleteMonitor = () => {
|
|||||||
return [deleteMonitor, isLoading];
|
return [deleteMonitor, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateMonitor = () => {
|
const useUpdateMonitor = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const updateMonitor = async ({ monitor, redirect }) => {
|
const updateMonitor = async ({ monitor, redirect }) => {
|
||||||
@@ -383,11 +387,10 @@ export const useUpdateMonitor = () => {
|
|||||||
...(monitor.type === "hardware" && {
|
...(monitor.type === "hardware" && {
|
||||||
thresholds: monitor.thresholds,
|
thresholds: monitor.thresholds,
|
||||||
secret: monitor.secret,
|
secret: monitor.secret,
|
||||||
selectedDisks: monitor.selectedDisks,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
await networkService.updateMonitor({
|
await networkService.updateMonitor({
|
||||||
monitorId: monitor.id,
|
monitorId: monitor._id,
|
||||||
updatedFields,
|
updatedFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -404,7 +407,7 @@ export const useUpdateMonitor = () => {
|
|||||||
return [updateMonitor, isLoading];
|
return [updateMonitor, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePauseMonitor = () => {
|
const usePauseMonitor = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState(undefined);
|
const [error, setError] = useState(undefined);
|
||||||
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
|
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
|
||||||
@@ -427,7 +430,7 @@ export const usePauseMonitor = () => {
|
|||||||
return [pauseMonitor, isLoading, error];
|
return [pauseMonitor, isLoading, error];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAddDemoMonitors = () => {
|
const useAddDemoMonitors = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const addDemoMonitors = async () => {
|
const addDemoMonitors = async () => {
|
||||||
@@ -444,7 +447,7 @@ export const useAddDemoMonitors = () => {
|
|||||||
return [addDemoMonitors, isLoading];
|
return [addDemoMonitors, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteAllMonitors = () => {
|
const useDeleteAllMonitors = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const deleteAllMonitors = async () => {
|
const deleteAllMonitors = async () => {
|
||||||
@@ -461,7 +464,7 @@ export const useDeleteAllMonitors = () => {
|
|||||||
return [deleteAllMonitors, isLoading];
|
return [deleteAllMonitors, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteMonitorStats = () => {
|
const useDeleteMonitorStats = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const deleteMonitorStats = async () => {
|
const deleteMonitorStats = async () => {
|
||||||
@@ -479,7 +482,7 @@ export const useDeleteMonitorStats = () => {
|
|||||||
return [deleteMonitorStats, isLoading];
|
return [deleteMonitorStats, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateBulkMonitors = () => {
|
const useCreateBulkMonitors = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const createBulkMonitors = async (file, user) => {
|
const createBulkMonitors = async (file, user) => {
|
||||||
@@ -502,7 +505,7 @@ export const useCreateBulkMonitors = () => {
|
|||||||
return [createBulkMonitors, isLoading];
|
return [createBulkMonitors, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useExportMonitors = () => {
|
const useExportMonitors = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -535,19 +538,23 @@ export const useExportMonitors = () => {
|
|||||||
return [exportMonitors, isLoading];
|
return [exportMonitors, isLoading];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetchJson = () => {
|
export {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
useFetchMonitorsWithSummary,
|
||||||
const fetchJson = async () => {
|
useFetchMonitorsWithChecks,
|
||||||
try {
|
useFetchMonitorsByTeamId,
|
||||||
setIsLoading(true);
|
useFetchStatsByMonitorId,
|
||||||
const res = await networkService.fetchJson();
|
useFetchMonitorById,
|
||||||
createToast({ body: "JSON fetched successfully" });
|
useFetchUptimeMonitorById,
|
||||||
return res?.data?.data ?? [];
|
useFetchHardwareMonitorById,
|
||||||
} catch (error) {
|
useCreateMonitor,
|
||||||
createToast({ body: "Failed to create monitor." });
|
useFetchGlobalSettings,
|
||||||
} finally {
|
useDeleteMonitor,
|
||||||
setIsLoading(false);
|
useUpdateMonitor,
|
||||||
}
|
usePauseMonitor,
|
||||||
};
|
useAddDemoMonitors,
|
||||||
return [fetchJson, isLoading];
|
useDeleteAllMonitors,
|
||||||
|
useDeleteMonitorStats,
|
||||||
|
useCreateBulkMonitors,
|
||||||
|
useExportMonitors,
|
||||||
|
useFetchMonitorGames,
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
|
const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
|
||||||
@@ -42,11 +42,11 @@ const useSaveSettings = ({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const settingsResponse = await networkService.updateAppSettings({ settings });
|
const settingsResponse = await networkService.updateAppSettings({ settings });
|
||||||
// if (settings.checkTTL) {
|
if (settings.checkTTL) {
|
||||||
// await networkService.updateChecksTTL({
|
await networkService.updateChecksTTL({
|
||||||
// ttl: settings.checkTTL,
|
ttl: settings.checkTTL,
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet);
|
setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet);
|
||||||
setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet);
|
setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet);
|
||||||
if (settingsResponse.data.data.pagespeedKeySet === true) {
|
if (settingsResponse.data.data.pagespeedKeySet === true) {
|
||||||
@@ -56,7 +56,6 @@ const useSaveSettings = ({
|
|||||||
setEmailPasswordHasBeenReset(false);
|
setEmailPasswordHasBeenReset(false);
|
||||||
}
|
}
|
||||||
setSettingsData(settingsResponse.data.data);
|
setSettingsData(settingsResponse.data.data);
|
||||||
|
|
||||||
createToast({ body: t("settingsSuccessSaved") });
|
createToast({ body: t("settingsSuccessSaved") });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createToast({ body: t("settingsFailedToSave") });
|
createToast({ body: t("settingsFailedToSave") });
|
||||||
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils.js";
|
import { NOTIFICATION_TYPES } from "../../Pages/v1/Notifications/utils.js";
|
||||||
|
|
||||||
const useCreateNotification = () => {
|
const useCreateNotification = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const useSendTestEmail = () => {
|
const useSendTestEmail = () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { networkService } from "../main.jsx";
|
import { networkService } from "../../main.jsx";
|
||||||
import { createToast } from "../Utils/toastUtils.jsx";
|
import { createToast } from "../../Utils/toastUtils.jsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const useGetUser = (userId) => {
|
export const useGetUser = (userId) => {
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import type { SWRConfiguration } from "swr";
|
||||||
|
import type { AxiosRequestConfig } from "axios";
|
||||||
|
import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper
|
||||||
|
|
||||||
|
export type ApiResponse = {
|
||||||
|
message: string;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic fetcher for GET requests
|
||||||
|
const fetcher = async <T,>(url: string, config?: AxiosRequestConfig) => {
|
||||||
|
const res = await get<T>(url, config);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGet = <T,>(
|
||||||
|
url: string,
|
||||||
|
axiosConfig?: AxiosRequestConfig,
|
||||||
|
swrConfig?: SWRConfiguration<T, Error>
|
||||||
|
) => {
|
||||||
|
const { data, error, isLoading, isValidating, mutate } = useSWR<T>(
|
||||||
|
url,
|
||||||
|
(url) => fetcher<T>(url, axiosConfig),
|
||||||
|
swrConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: data ?? null,
|
||||||
|
loading: isLoading,
|
||||||
|
isValidating,
|
||||||
|
error: error?.message ?? null,
|
||||||
|
refetch: mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePost = <B = any, R = any>() => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const postFn = async (
|
||||||
|
endpoint: string,
|
||||||
|
body: B,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<R | null> => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await post<R>(endpoint, body, config);
|
||||||
|
return res.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
|
setError(errMsg);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { post: postFn, loading, error };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePatch = <B = any, R = any>() => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const patchFn = async (
|
||||||
|
endpoint: string,
|
||||||
|
body?: B,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<R | null> => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await patch<R>(endpoint, body, config);
|
||||||
|
return res.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
|
setError(errMsg);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { patch: patchFn, loading, error };
|
||||||
|
};
|
||||||
@@ -2,12 +2,12 @@ import { useState } from "react";
|
|||||||
import { Button, Stack } from "@mui/material";
|
import { Button, Stack } from "@mui/material";
|
||||||
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog";
|
import { GenericDialog } from "@/Components/v1/Dialog/genericDialog";
|
||||||
import TextInput from "@/Components/v1/Inputs/TextInput";
|
import TextInput from "@/Components/v1/Inputs/TextInput";
|
||||||
import PasswordTooltip from "@/Pages/Auth/components/PasswordTooltip";
|
import PasswordTooltip from "@/Pages/v1/Auth/components/PasswordTooltip";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createToast } from "../../../../Utils/toastUtils";
|
import { createToast } from "../../../../Utils/toastUtils";
|
||||||
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments";
|
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments";
|
||||||
import usePasswordFeedback from "@/Pages/Auth/hooks/usePasswordFeedback";
|
import usePasswordFeedback from "@/Pages/v1/Auth/hooks/usePasswordFeedback";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => {
|
const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => {
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import PropTypes from "prop-types";
|
|
||||||
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 SummaryCard from "../SummaryCard/index.jsx";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ActiveIncidentsPanel Component
|
|
||||||
*
|
|
||||||
* Displays a quick overview of currently active incidents.
|
|
||||||
* Shows only essential information: monitor name, status badge, and duration.
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {Array} props.incidents - Array of active incidents
|
|
||||||
* @param {number} props.totalCount - Total count of active incidents (for badge)
|
|
||||||
* @param {boolean} props.isLoading - Loading state
|
|
||||||
* @param {Object} props.error - Error object if any
|
|
||||||
* @param {Function} props.onIncidentClick - Callback when incident is clicked (optional)
|
|
||||||
*/
|
|
||||||
const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <PanelSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<SummaryCard>
|
|
||||||
<Typography
|
|
||||||
color="error"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsActivePanelError")}
|
|
||||||
</Typography>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!totalCount || totalCount === 0) {
|
|
||||||
return (
|
|
||||||
<SummaryCard>
|
|
||||||
<Stack
|
|
||||||
direction="column"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
padding={theme.spacing(10)}
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
sx={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.success.main,
|
|
||||||
"& svg": {
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
|
|
||||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
|
||||||
},
|
|
||||||
mb: theme.spacing(2),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
color={theme.palette.primary.contrastTextSecondary}
|
|
||||||
sx={{
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: 600,
|
|
||||||
textAlign: "center",
|
|
||||||
color: theme.palette.success.lowContrast,
|
|
||||||
letterSpacing: theme.spacing(0.4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.allSystemsAreOperational")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SummaryCard isHighPriority={true}>
|
|
||||||
<Stack
|
|
||||||
direction="column"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
spacing={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.error.lowContrast,
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
"& svg": {
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
|
|
||||||
"& path": { stroke: "currentColor", strokeWidth: 2 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Incidents />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="h1"
|
|
||||||
sx={{
|
|
||||||
fontSize: `calc(${theme.typography.h1.fontSize} * 2.5)`,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: theme.palette.error.lowContrast,
|
|
||||||
lineHeight: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{totalCount}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="h2"
|
|
||||||
sx={{
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: theme.spacing(0.4),
|
|
||||||
paddingTop: theme.spacing(3),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsActivePanelTitle")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ActiveIncidentsPanel.propTypes = {
|
|
||||||
totalCount: PropTypes.number,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
error: PropTypes.object,
|
|
||||||
onIncidentClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActiveIncidentsPanel;
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
//Components
|
|
||||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
|
||||||
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
|
|
||||||
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
|
|
||||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
|
||||||
import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
|
|
||||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
|
||||||
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
|
|
||||||
|
|
||||||
//Utils
|
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Button, Typography, useTheme } from "@mui/material";
|
|
||||||
|
|
||||||
const IncidentTable = ({
|
|
||||||
incidents = [],
|
|
||||||
incidentsCount = 0,
|
|
||||||
isLoading = false,
|
|
||||||
networkError = false,
|
|
||||||
page = 0,
|
|
||||||
rowsPerPage = 10,
|
|
||||||
handleChangePage,
|
|
||||||
handleChangeRowsPerPage,
|
|
||||||
resolveIncident,
|
|
||||||
handleUpdateTrigger,
|
|
||||||
}) => {
|
|
||||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const handleResolveIncident = async (incidentId) => {
|
|
||||||
try {
|
|
||||||
await resolveIncident(incidentId);
|
|
||||||
handleUpdateTrigger();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(t("incidentsPage.errorResolvingIncident"), error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{
|
|
||||||
id: "monitorName",
|
|
||||||
content: t("incidentsTableMonitorName"),
|
|
||||||
render: (row) => row.monitorName ?? "N/A",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "status",
|
|
||||||
content: t("incidentsTableStatus"),
|
|
||||||
render: (row) => {
|
|
||||||
const status = row.status === true ? "down" : "up";
|
|
||||||
const statusText =
|
|
||||||
row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved");
|
|
||||||
return (
|
|
||||||
<StatusLabel
|
|
||||||
status={status}
|
|
||||||
text={statusText}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "startTime",
|
|
||||||
content: t("incidentsPage.startTime"),
|
|
||||||
render: (row) => {
|
|
||||||
const formattedDate = formatDateWithTz(
|
|
||||||
row.startTime || row.createdAt,
|
|
||||||
"YYYY-MM-DD HH:mm:ss A",
|
|
||||||
uiTimezone
|
|
||||||
);
|
|
||||||
return formattedDate;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "endTime",
|
|
||||||
content: t("incidentsPage.endTime"),
|
|
||||||
render: (row) => {
|
|
||||||
if (row.endTime) {
|
|
||||||
return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone);
|
|
||||||
}
|
|
||||||
return "-";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "resolutionType",
|
|
||||||
content: t("incidentsPage.resolutionType"),
|
|
||||||
render: (row) => {
|
|
||||||
if (row.resolutionType) {
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
textTransform: "capitalize",
|
|
||||||
color:
|
|
||||||
row.resolutionType === "manual"
|
|
||||||
? theme.palette.accent.main
|
|
||||||
: theme.palette.success.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.resolutionType}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return "-";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "statusCode",
|
|
||||||
content: t("incidentsTableStatusCode"),
|
|
||||||
render: (row) => <HttpStatusLabel status={row.statusCode} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "message",
|
|
||||||
content: t("incidentsTableMessage"),
|
|
||||||
render: (row) => row.message || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "action",
|
|
||||||
content: t("actions"),
|
|
||||||
render: (row) => {
|
|
||||||
if (row.status === true) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="accent"
|
|
||||||
sx={{
|
|
||||||
minHeight: "max-content",
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
handleResolveIncident(row._id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsTableActionResolveManually")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color={theme.palette.primary.contrastTextSecondary}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsTableResolved")}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isLoading) return <TableSkeleton />;
|
|
||||||
|
|
||||||
if (networkError) {
|
|
||||||
return (
|
|
||||||
<GenericFallback>
|
|
||||||
<NetworkError />
|
|
||||||
</GenericFallback>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && !networkError && incidents?.length === 0) {
|
|
||||||
return (
|
|
||||||
<GenericFallback>
|
|
||||||
{t("incidentsTableNoIncidents", "No incidents found")}
|
|
||||||
</GenericFallback>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const incidentsData = Array.isArray(incidents) ? incidents : [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DataTable
|
|
||||||
headers={headers}
|
|
||||||
data={incidentsData}
|
|
||||||
/>
|
|
||||||
<Pagination
|
|
||||||
paginationLabel={t("incidentsTablePaginationLabel", "Incidents")}
|
|
||||||
itemCount={incidentsCount || 0}
|
|
||||||
page={page}
|
|
||||||
rowsPerPage={rowsPerPage}
|
|
||||||
handleChangePage={handleChangePage}
|
|
||||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
IncidentTable.propTypes = {
|
|
||||||
incidents: PropTypes.array.isRequired, // Array of incident objects
|
|
||||||
incidentsCount: PropTypes.number.isRequired, // Total count for pagination
|
|
||||||
isLoading: PropTypes.bool.isRequired, // Loading state
|
|
||||||
networkError: PropTypes.bool, // Network error object
|
|
||||||
page: PropTypes.number.isRequired, // Current page number
|
|
||||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows per page
|
|
||||||
handleChangePage: PropTypes.func.isRequired, // Handler for page change
|
|
||||||
handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change
|
|
||||||
resolveIncident: PropTypes.func.isRequired,
|
|
||||||
handleUpdateTrigger: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IncidentTable;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
// Hooks
|
|
||||||
import useFetchIncidents from "../../hooks/useFetchIncidents.js";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
// Components
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import ActiveIncidentsPanel from "../ActiveIncidentsPanel/index.jsx";
|
|
||||||
import LatestIncidentsPanel from "../LatestIncidentsPanel/index.jsx";
|
|
||||||
import StatisticsPanel from "../StatisticsPanel/index.jsx";
|
|
||||||
|
|
||||||
const IncidentsSummaryPanel = ({ updateTrigger }) => {
|
|
||||||
const { fetchIncidentSummary } = useFetchIncidents();
|
|
||||||
const [summary, setSummary] = useState({
|
|
||||||
totalActive: 0,
|
|
||||||
avgResolutionTimeHours: 0,
|
|
||||||
total: 0,
|
|
||||||
topMonitor: null,
|
|
||||||
totalManualResolutions: 0,
|
|
||||||
totalAutomaticResolutions: 0,
|
|
||||||
latestIncidents: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isLoadingSummary, setIsLoadingSummary] = useState(false);
|
|
||||||
const [summaryError, setSummaryError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSummary = async () => {
|
|
||||||
setIsLoadingSummary(true);
|
|
||||||
setSummaryError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const summaryData = await fetchIncidentSummary({
|
|
||||||
limit: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (summaryData) {
|
|
||||||
setSummary(summaryData);
|
|
||||||
} else {
|
|
||||||
setSummaryError(new Error("Failed to fetch summary"));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching incident summary:", error);
|
|
||||||
setSummaryError(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingSummary(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSummary();
|
|
||||||
}, [fetchIncidentSummary, updateTrigger]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
spacing={3}
|
|
||||||
>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={4}
|
|
||||||
>
|
|
||||||
<ActiveIncidentsPanel
|
|
||||||
totalCount={summary.totalActive || 0}
|
|
||||||
isLoading={isLoadingSummary}
|
|
||||||
error={summaryError}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={4}
|
|
||||||
>
|
|
||||||
<LatestIncidentsPanel
|
|
||||||
incidents={summary.latestIncidents || []}
|
|
||||||
isLoading={isLoadingSummary}
|
|
||||||
error={summaryError}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={4}
|
|
||||||
>
|
|
||||||
<StatisticsPanel
|
|
||||||
summary={summary}
|
|
||||||
isLoading={isLoadingSummary}
|
|
||||||
error={summaryError}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
IncidentsSummaryPanel.propTypes = {
|
|
||||||
updateTrigger: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
export default IncidentsSummaryPanel;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Skeleton } from "@mui/material";
|
|
||||||
import SummaryCard from "../SummaryCard/index.jsx";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple skeleton loader for summary panels
|
|
||||||
* Reusable for ActiveIncidentsPanel, LatestIncidentsPanel, and StatisticsPanel
|
|
||||||
*/
|
|
||||||
const PanelSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<SummaryCard sx={{ minHeight: "25vh" }}>
|
|
||||||
<Skeleton
|
|
||||||
variant="rounded"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
/>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PanelSkeleton;
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
|
||||||
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
|
|
||||||
import Monitors from "@/assets/icons/monitors.svg?react";
|
|
||||||
import AverageResponseIcon from "@/assets/icons/status-pages.svg?react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const IncidentItem = ({ incident }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const isActive = incident.status === true;
|
|
||||||
const calculateDuration = () => {
|
|
||||||
if (!incident.startTime) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
const startTime = new Date(incident.startTime);
|
|
||||||
const endTime = isActive
|
|
||||||
? new Date()
|
|
||||||
: incident.endTime
|
|
||||||
? new Date(incident.endTime)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!endTime) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const durationMs = endTime.getTime() - startTime.getTime();
|
|
||||||
|
|
||||||
if (durationMs < 0) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getHumanReadableDuration(durationMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
const duration = calculateDuration();
|
|
||||||
const iconWrapperStyle = {
|
|
||||||
px: theme.spacing(2),
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
|
||||||
"& svg path": {
|
|
||||||
stroke: "currentColor",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
width: "100%",
|
|
||||||
py: theme.spacing(0.5),
|
|
||||||
"&:hover": { opacity: 0.8 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction="column"
|
|
||||||
gap={theme.spacing(1.5)}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(1)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
...iconWrapperStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Monitors />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
alignItems="baseline"
|
|
||||||
>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{t("incidentsPage.incidentItemMonitor")}:
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
fontWeight={600}
|
|
||||||
>
|
|
||||||
{incident.monitorName || t("incidentsPage.unknownMonitor")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
width="100%"
|
|
||||||
marginTop={theme.spacing(1)}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(3)}
|
|
||||||
>
|
|
||||||
<Box sx={{ ...iconWrapperStyle }}>
|
|
||||||
<AverageResponseIcon />
|
|
||||||
</Box>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{t("incidentsPage.incidentItemStatus")}:
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<StatusLabel
|
|
||||||
status={isActive ? "down" : "up"}
|
|
||||||
text={isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")}
|
|
||||||
customStyles={{
|
|
||||||
textTransform: "capitalize",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
fontWeight={500}
|
|
||||||
>
|
|
||||||
{duration}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
IncidentItem.propTypes = {
|
|
||||||
incident: PropTypes.shape({
|
|
||||||
_id: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
monitorId: PropTypes.string,
|
|
||||||
monitorName: PropTypes.string,
|
|
||||||
status: PropTypes.bool,
|
|
||||||
startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
|
||||||
endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
|
||||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
|
||||||
}),
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IncidentItem;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box, Stack, Typography, Divider } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
|
||||||
import IncidentItem from "./IncidentItem.jsx";
|
|
||||||
import SummaryCard from "../SummaryCard/index.jsx";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LatestIncidentsPanel Component
|
|
||||||
*
|
|
||||||
* Displays a quick overview of recent incidents (both active and resolved).
|
|
||||||
* Shows only essential information: monitor name, status, duration, and resolution type.
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {Array} props.incidents - Array of recent incidents
|
|
||||||
* @param {boolean} props.isLoading - Loading state
|
|
||||||
* @param {Object} props.error - Error object if any
|
|
||||||
*/
|
|
||||||
const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <PanelSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<SummaryCard>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: "100%",
|
|
||||||
minHeight: 200,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
color="error"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsLatestPanelError")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SummaryCard title={t("incidentsPage.incidentsLatestPanelTitle")}>
|
|
||||||
{!incidents || incidents.length === 0 ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsLatestPanelEmpty")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Stack gap={theme.spacing(4)}>
|
|
||||||
{incidents.map((incident, index) => (
|
|
||||||
<Box key={incident._id}>
|
|
||||||
<IncidentItem incident={incident} />
|
|
||||||
{index < incidents.length - 1 && <Divider sx={{ mt: theme.spacing(2) }} />}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LatestIncidentsPanel.propTypes = {
|
|
||||||
incidents: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
_id: PropTypes.string,
|
|
||||||
monitorId: PropTypes.string,
|
|
||||||
monitorName: PropTypes.string,
|
|
||||||
status: PropTypes.bool,
|
|
||||||
duration: PropTypes.string,
|
|
||||||
resolutionType: PropTypes.oneOf(["automatic", "manual"]),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
error: PropTypes.object,
|
|
||||||
onIncidentClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LatestIncidentsPanel;
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Components
|
|
||||||
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
|
||||||
import Select from "@/Components/v1/Inputs/Select/index.jsx";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
//Utils
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const OptionsHeader = ({
|
|
||||||
shouldRender,
|
|
||||||
selectedMonitor = 0,
|
|
||||||
setSelectedMonitor,
|
|
||||||
monitors,
|
|
||||||
filter = "all",
|
|
||||||
setFilter,
|
|
||||||
dateRange = "all",
|
|
||||||
setDateRange,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
|
|
||||||
|
|
||||||
const filterOptions = [
|
|
||||||
{ _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") },
|
|
||||||
{ _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") },
|
|
||||||
{ _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") },
|
|
||||||
{ _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") },
|
|
||||||
];
|
|
||||||
|
|
||||||
const stackStyles = {
|
|
||||||
direction: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(6),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!shouldRender) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<Stack {...stackStyles}>
|
|
||||||
<Typography
|
|
||||||
display="inline-block"
|
|
||||||
component="h1"
|
|
||||||
color={theme.palette.primary.contrastTextSecondary}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeader")}
|
|
||||||
</Typography>
|
|
||||||
<Select
|
|
||||||
id="incidents-select-monitor"
|
|
||||||
placeholder={t("incidentsOptionsPlaceholderAllServers")}
|
|
||||||
value={selectedMonitor}
|
|
||||||
onChange={(e) => setSelectedMonitor(e.target.value)}
|
|
||||||
items={monitorNames}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
color: theme.palette.primary.contrastTextSecondary,
|
|
||||||
}}
|
|
||||||
maxWidth={250}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack {...stackStyles}>
|
|
||||||
<Typography
|
|
||||||
display="inline-block"
|
|
||||||
component="h1"
|
|
||||||
color={theme.palette.primary.contrastTextSecondary}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderFilterBy")}
|
|
||||||
</Typography>
|
|
||||||
<Select
|
|
||||||
id="incidents-select-filter"
|
|
||||||
value={filter}
|
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
|
||||||
items={filterOptions}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
color: theme.palette.primary.contrastTextSecondary,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack {...stackStyles}>
|
|
||||||
<Typography
|
|
||||||
display="inline-block"
|
|
||||||
component="h1"
|
|
||||||
color={theme.palette.primary.contrastTextSecondary}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderShow")}
|
|
||||||
</Typography>
|
|
||||||
<ButtonGroup>
|
|
||||||
<Button
|
|
||||||
variant="group"
|
|
||||||
filled={(dateRange === "hour").toString()}
|
|
||||||
onClick={() => setDateRange("hour")}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderLastHour")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="group"
|
|
||||||
filled={(dateRange === "day").toString()}
|
|
||||||
onClick={() => setDateRange("day")}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderLastDay")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="group"
|
|
||||||
filled={(dateRange === "week").toString()}
|
|
||||||
onClick={() => setDateRange("week")}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderLastWeek")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="group"
|
|
||||||
filled={(dateRange === "all").toString()}
|
|
||||||
onClick={() => setDateRange("all")}
|
|
||||||
>
|
|
||||||
{t("incidentsOptionsHeaderFilterAll")}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
OptionsHeader.propTypes = {
|
|
||||||
shouldRender: PropTypes.bool,
|
|
||||||
selectedMonitor: PropTypes.string,
|
|
||||||
setSelectedMonitor: PropTypes.func,
|
|
||||||
monitors: PropTypes.object,
|
|
||||||
filter: PropTypes.string,
|
|
||||||
setFilter: PropTypes.func,
|
|
||||||
dateRange: PropTypes.string,
|
|
||||||
setDateRange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptionsHeader;
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Divider } from "@mui/material";
|
|
||||||
import Clock from "@/assets/icons/maintenance.svg?react";
|
|
||||||
import Incidents from "@/assets/icons/incidents.svg?react";
|
|
||||||
import ResolutionItem from "@/assets/icons/interval-check.svg?react";
|
|
||||||
import NotificationIcon from "@/assets/icons/notifications.svg?react";
|
|
||||||
import SummaryCard from "../SummaryCard/index.jsx";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* StatisticsPanel Component
|
|
||||||
*
|
|
||||||
* Displays key metrics and statistics about incidents.
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {Object} props.statistics - Statistics data object
|
|
||||||
* @param {number} props.statistics.total - Total incidents count
|
|
||||||
* @param {number} props.statistics.totalTrend - Trend percentage for total (optional)
|
|
||||||
* @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m")
|
|
||||||
* @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional)
|
|
||||||
* @param {Object} props.statistics.topMonitor - Top monitor with most incidents
|
|
||||||
* @param {string} props.statistics.topMonitor.name - Monitor name
|
|
||||||
* @param {number} props.statistics.topMonitor.count - Incident count
|
|
||||||
* @param {number} props.statistics.topMonitor.percentage - Percentage of total
|
|
||||||
* @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m")
|
|
||||||
* @param {number} props.statistics.availability - Availability percentage
|
|
||||||
* @param {boolean} props.isLoading - Loading state
|
|
||||||
* @param {Object} props.error - Error object if any
|
|
||||||
*/
|
|
||||||
const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const totalResolutions =
|
|
||||||
(summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0);
|
|
||||||
const automaticPercentage =
|
|
||||||
totalResolutions > 0
|
|
||||||
? (summary?.totalAutomaticResolutions / totalResolutions) * 100
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const iconWrapperStyle = {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
mx: theme.spacing(3),
|
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
|
||||||
"& svg path": {
|
|
||||||
stroke: "currentColor",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (isLoading) {
|
|
||||||
return <PanelSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<SummaryCard>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: "100%",
|
|
||||||
minHeight: 200,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
color="error"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
{t("incidentsPage.incidentsStatisticsPanelError")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SummaryCard title={t("incidentsPage.incidentsStatisticsPanelTitle")}>
|
|
||||||
<Stack gap={theme.spacing(4)}>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(3)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
...iconWrapperStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NotificationIcon />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.totalIncidents")} : {summary.total || 0}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Box sx={iconWrapperStyle}>
|
|
||||||
<Incidents />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.mostAffectedMonitor")} :{" "}
|
|
||||||
{summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Box sx={iconWrapperStyle}>
|
|
||||||
<Clock />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.avgResolutionTime")} :{" "}
|
|
||||||
{summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Box padding={theme.spacing(2)}>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
mb={theme.spacing(1.5)}
|
|
||||||
>
|
|
||||||
<Box sx={iconWrapperStyle}>
|
|
||||||
<ResolutionItem />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.resolutions")}: {totalResolutions}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: "hidden",
|
|
||||||
bgcolor: theme.palette.accent.main,
|
|
||||||
width: "100%",
|
|
||||||
marginTop: theme.spacing(4),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: `${automaticPercentage}%`,
|
|
||||||
|
|
||||||
bgcolor: theme.palette.warningSecondary.lowContrast,
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
paddingTop: theme.spacing(4),
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{summary.totalAutomaticResolutions > 0 && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
fontWeight={500}
|
|
||||||
color={theme.palette.warningSecondary.contrastText}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{summary.totalManualResolutions > 0 && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
fontWeight={500}
|
|
||||||
color={theme.palette.accent.main}
|
|
||||||
>
|
|
||||||
{t("incidentsPage.manual")} ({summary.totalManualResolutions})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</SummaryCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
StatisticsPanel.propTypes = {
|
|
||||||
summary: PropTypes.object,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
error: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatisticsPanel;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Paper } from "@mui/material";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box, Stack, Typography, Divider } from "@mui/material";
|
|
||||||
const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
padding: theme.spacing(4),
|
|
||||||
borderRadius: 3,
|
|
||||||
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,
|
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
|
||||||
fontSize: theme.typography.body1.fontSize,
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title && (
|
|
||||||
<Box>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
sx={{
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
sx={{
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: theme.typography.h2.fontSize,
|
|
||||||
|
|
||||||
letterSpacing: theme.spacing(0.5),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Divider />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Stack
|
|
||||||
mt={theme.spacing(4)}
|
|
||||||
paddingTop={theme.spacing(5)}
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
sx={{ height: "100%" }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
SummaryCard.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
isHighPriority: PropTypes.bool,
|
|
||||||
sx: PropTypes.object,
|
|
||||||
title: PropTypes.string,
|
|
||||||
};
|
|
||||||
export default SummaryCard;
|
|
||||||
Loaded 100 of 485 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user