Compare commits
198 Commits
dashboard-
...
v0.52.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1308f0f15f | ||
|
|
6c634b99d0 | ||
|
|
9856335840 | ||
|
|
e85b405396 | ||
|
|
e2e965bc7f | ||
|
|
7811fdd17a | ||
|
|
0dca1237b9 | ||
|
|
f3d73f6d44 | ||
|
|
187927403a | ||
|
|
0157b47424 | ||
|
|
156905afc7 | ||
|
|
a4878f6430 | ||
|
|
4489df6f39 | ||
|
|
06c075466b | ||
|
|
62be3e7c13 | ||
|
|
bb84960442 | ||
|
|
52199361d5 | ||
|
|
f031845300 | ||
|
|
6f73bb6eca | ||
|
|
fe398bcc49 | ||
|
|
6781c29082 | ||
|
|
eb146491f2 | ||
|
|
ae325ec1ca | ||
|
|
fd6f0574f5 | ||
|
|
b819a90c80 | ||
|
|
a6848f6abd | ||
|
|
abe65975c9 | ||
|
|
5cedd57aa2 | ||
|
|
80a7b9d16d | ||
|
|
9f7b2542ec | ||
|
|
4a4c9f26a2 | ||
|
|
c957c0f757 | ||
|
|
3ff0aa4b4b | ||
|
|
063c9adba6 | ||
|
|
5c3ce146fa | ||
|
|
481bb6e8b8 | ||
|
|
61e6316736 | ||
|
|
f9d1494657 | ||
|
|
0021b4d784 | ||
|
|
a5d5800871 | ||
|
|
16dc90bbd1 | ||
|
|
fff61379fe | ||
|
|
08a415032c | ||
|
|
3783ffdd4c | ||
|
|
a8e4359d95 | ||
|
|
d9e94a4067 | ||
|
|
ae19eaa76a | ||
|
|
fff9954da2 | ||
|
|
220edd139a | ||
|
|
59121bd932 | ||
|
|
aef935a817 | ||
|
|
f300518d61 | ||
|
|
18b608a1d8 | ||
|
|
738d62c9cf | ||
|
|
38e694cd36 | ||
|
|
1281330c52 | ||
|
|
7b7cca7db7 | ||
|
|
3134e8c1cf | ||
|
|
d00024b64a | ||
|
|
4360cd0397 | ||
|
|
a688b6c60e | ||
|
|
522e73b48e | ||
|
|
ba7e6fcf23 | ||
|
|
eefccafa5b | ||
|
|
05bd6d52f1 | ||
|
|
d60daef171 | ||
|
|
d50530f58c | ||
|
|
6957bd71ca | ||
|
|
ef8b50c19e | ||
|
|
1585065fff | ||
|
|
99c68ddbcd | ||
|
|
b08e859426 | ||
|
|
89fd3e4f55 | ||
|
|
a2492b0135 | ||
|
|
eb8ca5a7ca | ||
|
|
80133240ca | ||
|
|
7d7d112f40 | ||
|
|
add2d19614 | ||
|
|
adfe20e88a | ||
|
|
d3b83f5a41 | ||
|
|
77eba9a558 | ||
|
|
43e73e06fe | ||
|
|
840d8b2e49 | ||
|
|
df751c7f38 | ||
|
|
cd07c743b6 | ||
|
|
46e6c34e51 | ||
|
|
42f7905b3b | ||
|
|
a6e68c6519 | ||
|
|
c7e3e6dc4e | ||
|
|
9194ab08b6 | ||
|
|
3ecb2e35ef | ||
|
|
9844dcdfb7 | ||
|
|
ddf5569ce9 | ||
|
|
83455e614e | ||
|
|
831de18464 | ||
|
|
3b2a811f7b | ||
|
|
2c7a5126fd | ||
|
|
87f1597d4e | ||
|
|
916663b4d5 | ||
|
|
b0e355eb64 | ||
|
|
69a39531f0 | ||
|
|
9c9ed741b2 | ||
|
|
e6eaaa660a | ||
|
|
79eef5bb91 | ||
|
|
4d64f1dede | ||
|
|
bf177882e6 | ||
|
|
f6b29999c9 | ||
|
|
75815897b0 | ||
|
|
c9309eecaa | ||
|
|
4264fc0f3a | ||
|
|
ef854910db | ||
|
|
6b8b2ae761 | ||
|
|
a48340a2ea | ||
|
|
e542d2ee09 | ||
|
|
08431131a9 | ||
|
|
1b0ec8ac43 | ||
|
|
2e0ddc7c7f | ||
|
|
858a0cb0de | ||
|
|
216ad36234 | ||
|
|
6628abd435 | ||
|
|
7c81270ed9 | ||
|
|
81c3e6fa65 | ||
|
|
d215ce09b0 | ||
|
|
161a69fbe9 | ||
|
|
3ee51770fd | ||
|
|
932b7ddc69 | ||
|
|
6e466df89d | ||
|
|
326dec21fd | ||
|
|
b0b69c83db | ||
|
|
02106277a6 | ||
|
|
b34509215e | ||
|
|
fd603b8fdf | ||
|
|
c5d23336a7 | ||
|
|
53c6288025 | ||
|
|
4f2c314f39 | ||
|
|
1ad61615c6 | ||
|
|
7ddfadfb18 | ||
|
|
a7e02af8b0 | ||
|
|
da3f6fd7fd | ||
|
|
a453471b51 | ||
|
|
13df87ed69 | ||
|
|
f23ceea54e | ||
|
|
46b4c8a004 | ||
|
|
580198ca7a | ||
|
|
2fb5b16840 | ||
|
|
de571aa69a | ||
|
|
daa5a05677 | ||
|
|
4f69996b9d | ||
|
|
6c402d9e46 | ||
|
|
c6e9eeeee6 | ||
|
|
97b66741a7 | ||
|
|
6b234da969 | ||
|
|
51032f6caa | ||
|
|
41f91db622 | ||
|
|
52e0303997 | ||
|
|
5df25e83d1 | ||
|
|
873280abea | ||
|
|
8ccdc71eaf | ||
|
|
d5f156a6e9 | ||
|
|
cc7559ddee | ||
|
|
415057c260 | ||
|
|
89b67b8880 | ||
|
|
878cb7c0a6 | ||
|
|
0375fc47a7 | ||
|
|
a7a160df76 | ||
|
|
8cd60b5c60 | ||
|
|
8ff392bc96 | ||
|
|
b59d9c7b90 | ||
|
|
afcee9cd02 | ||
|
|
9dbef080c6 | ||
|
|
82a079e687 | ||
|
|
6c192f1242 | ||
|
|
adfeaaa1f0 | ||
|
|
6ee9705599 | ||
|
|
67965c8e4d | ||
|
|
38b1de5ccc | ||
|
|
64e06ab3f9 | ||
|
|
537641000d | ||
|
|
4916cf5083 | ||
|
|
f3c2fb0246 | ||
|
|
a4e98e565d | ||
|
|
faa1728b8c | ||
|
|
b69e97d7b0 | ||
|
|
c0195e9dc9 | ||
|
|
b69545a771 | ||
|
|
9a6db272c1 | ||
|
|
45d6430ab3 | ||
|
|
cf7bf32ac2 | ||
|
|
1695b4f06d | ||
|
|
a65d5095a0 | ||
|
|
0fade428ef | ||
|
|
3b4b9e43b3 | ||
|
|
c104b758ba | ||
|
|
2a4e97f8da | ||
|
|
f1b5da9916 | ||
|
|
b57a24a177 | ||
|
|
a6e005e3a2 | ||
|
|
4d375e7cc3 |
1
.github/workflows/push.yaml
vendored
@@ -158,6 +158,7 @@ jobs:
|
|||||||
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||||
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|||||||
3
.github/workflows/staging-deployment.yaml
vendored
@@ -30,6 +30,7 @@ jobs:
|
|||||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
|
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||||
run: |
|
run: |
|
||||||
read -r -d '' COMMAND <<EOF || true
|
read -r -d '' COMMAND <<EOF || true
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
@@ -51,4 +52,4 @@ jobs:
|
|||||||
make build-frontend-amd64
|
make build-frontend-amd64
|
||||||
make run-testing
|
make run-testing
|
||||||
EOF
|
EOF
|
||||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
|||||||
3
.github/workflows/testing-deployment.yaml
vendored
@@ -30,6 +30,7 @@ jobs:
|
|||||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
|
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||||
run: |
|
run: |
|
||||||
read -r -d '' COMMAND <<EOF || true
|
read -r -d '' COMMAND <<EOF || true
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
@@ -52,4 +53,4 @@ jobs:
|
|||||||
make build-frontend-amd64
|
make build-frontend-amd64
|
||||||
make run-testing
|
make run-testing
|
||||||
EOF
|
EOF
|
||||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-
|
|||||||
```bash
|
```bash
|
||||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||||
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
|
||||||
```
|
```
|
||||||
|
|
||||||
**5.1.3 To stop the load generation:**
|
**5.1.3 To stop the load generation:**
|
||||||
|
|||||||
1
Makefile
@@ -188,3 +188,4 @@ test:
|
|||||||
go test ./pkg/query-service/tests/integration/...
|
go test ./pkg/query-service/tests/integration/...
|
||||||
go test ./pkg/query-service/rules/...
|
go test ./pkg/query-service/rules/...
|
||||||
go test ./pkg/query-service/collectorsimulator/...
|
go test ./pkg/query-service/collectorsimulator/...
|
||||||
|
go test ./pkg/query-service/postprocess/...
|
||||||
|
|||||||
@@ -198,14 +198,14 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
|||||||
|
|
||||||
#### Frontend
|
#### Frontend
|
||||||
|
|
||||||
- [Palash Gupta](https://github.com/palashgdev)
|
|
||||||
- [Yunus M](https://github.com/YounixM)
|
- [Yunus M](https://github.com/YounixM)
|
||||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
- [Vikrant Gupta](https://github.com/vikrantgupta25)
|
||||||
|
- [Sagar Rajput](https://github.com/SagarRajput-7)
|
||||||
|
|
||||||
#### DevOps
|
#### DevOps
|
||||||
|
|
||||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||||
- [Dhawal Sanghvi](https://github.com/dhawal1248)
|
- [Vibhu Pandey](https://github.com/grandwizard28)
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.46.0
|
image: signoz/query-service:0.49.1
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.46.0
|
image: signoz/frontend:0.48.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.88.24
|
image: signoz/signoz-otel-collector:0.102.2
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -211,6 +211,7 @@ services:
|
|||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||||
@@ -237,7 +238,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.24
|
image: signoz/signoz-schema-migrator:0.102.2
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ receivers:
|
|||||||
# endpoint: 0.0.0.0:6832
|
# endpoint: 0.0.0.0:6832
|
||||||
hostmetrics:
|
hostmetrics:
|
||||||
collection_interval: 30s
|
collection_interval: 30s
|
||||||
|
root_path: /hostfs
|
||||||
scrapers:
|
scrapers:
|
||||||
cpu: {}
|
cpu: {}
|
||||||
load: {}
|
load: {}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -81,7 +81,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
otel-collector:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.88.24
|
image: signoz/signoz-otel-collector:0.102.2
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -93,6 +93,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -204,7 +204,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -216,7 +216,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -230,7 +230,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -244,6 +244,7 @@ services:
|
|||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.49.1}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -243,6 +243,7 @@ services:
|
|||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ receivers:
|
|||||||
# endpoint: 0.0.0.0:6832
|
# endpoint: 0.0.0.0:6832
|
||||||
hostmetrics:
|
hostmetrics:
|
||||||
collection_interval: 30s
|
collection_interval: 30s
|
||||||
|
root_path: /hostfs
|
||||||
scrapers:
|
scrapers:
|
||||||
cpu: {}
|
cpu: {}
|
||||||
load: {}
|
load: {}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ trap bye EXIT
|
|||||||
|
|
||||||
URL="https://api.segment.io/v1/track"
|
URL="https://api.segment.io/v1/track"
|
||||||
HEADER_1="Content-Type: application/json"
|
HEADER_1="Content-Type: application/json"
|
||||||
HEADER_2="Authorization: Basic NEdtb2E0aXhKQVVIeDJCcEp4c2p3QTFiRWZud0VlUno6"
|
HEADER_2="Authorization: Basic OWtScko3b1BDR1BFSkxGNlFqTVBMdDVibGpGaFJRQnI="
|
||||||
|
|
||||||
send_event() {
|
send_event() {
|
||||||
error=""
|
error=""
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.DataConnector
|
||||||
SkipConfig *basemodel.SkipConfig
|
SkipConfig *basemodel.SkipConfig
|
||||||
PreferDelta bool
|
|
||||||
PreferSpanMetrics bool
|
PreferSpanMetrics bool
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
MaxOpenConns int
|
MaxOpenConns int
|
||||||
@@ -53,7 +52,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||||
Reader: opts.DataConnector,
|
Reader: opts.DataConnector,
|
||||||
SkipConfig: opts.SkipConfig,
|
SkipConfig: opts.SkipConfig,
|
||||||
PerferDelta: opts.PreferDelta,
|
|
||||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||||
MaxIdleConns: opts.MaxIdleConns,
|
MaxIdleConns: opts.MaxIdleConns,
|
||||||
MaxOpenConns: opts.MaxOpenConns,
|
MaxOpenConns: opts.MaxOpenConns,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||||
@@ -29,6 +31,10 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
// Get the dashboard UUID from the request
|
// Get the dashboard UUID from the request
|
||||||
uuid := mux.Vars(r)["uuid"]
|
uuid := mux.Vars(r)["uuid"]
|
||||||
|
if strings.HasPrefix(uuid,"integration") {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/ee/query-service/constants"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
featureSet, err := ah.FF().GetFeatureFlags()
|
featureSet, err := ah.FF().GetFeatureFlags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constants.FetchFeatures == "true" {
|
||||||
|
zap.L().Debug("fetching license")
|
||||||
|
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("failed to fetch license", zap.Error(err))
|
||||||
|
} else if license == nil {
|
||||||
|
zap.L().Debug("no active license found")
|
||||||
|
} else {
|
||||||
|
licenseKey := license.Key
|
||||||
|
|
||||||
|
zap.L().Debug("fetching zeus features")
|
||||||
|
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
|
||||||
|
if err == nil {
|
||||||
|
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
|
||||||
|
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
|
||||||
|
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
|
||||||
|
} else {
|
||||||
|
zap.L().Error("failed to fetch zeus features", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ah.opts.PreferSpanMetrics {
|
if ah.opts.PreferSpanMetrics {
|
||||||
for idx := range featureSet {
|
for idx := range featureSet {
|
||||||
feature := &featureSet[idx]
|
feature := &featureSet[idx]
|
||||||
@@ -20,5 +51,96 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ah.Respond(w, featureSet)
|
ah.Respond(w, featureSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
|
||||||
|
// and returns the FeatureSet.
|
||||||
|
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
|
||||||
|
// Check if the URL is empty
|
||||||
|
if url == "" {
|
||||||
|
return nil, fmt.Errorf("url is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the licenseKey is empty
|
||||||
|
if licenseKey == "" {
|
||||||
|
return nil, fmt.Errorf("licenseKey is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating an HTTP client with a timeout for better control
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
// Creating a new GET request
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting the custom header
|
||||||
|
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
|
||||||
|
|
||||||
|
// Making the GET request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make GET request: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check for non-OK status code
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%w: %d %s", errors.New("received non-OK HTTP status code"), resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading and decoding the response body
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeusResponse ZeusFeaturesResponse
|
||||||
|
if err := json.Unmarshal(body, &zeusResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", errors.New("failed to decode response body"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if zeusResponse.Status != "success" {
|
||||||
|
return nil, fmt.Errorf("%w: %s", errors.New("failed to fetch zeus features"), zeusResponse.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zeusResponse.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZeusFeaturesResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data basemodel.FeatureSet `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
|
||||||
|
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
|
||||||
|
// Create a map to store the merged features
|
||||||
|
featureMap := make(map[string]basemodel.Feature)
|
||||||
|
|
||||||
|
// Add all features from the otherFeatures set to the map
|
||||||
|
for _, feature := range internalFeatures {
|
||||||
|
featureMap[feature.Name] = feature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all features from the zeusFeatures set to the map
|
||||||
|
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
|
||||||
|
for _, feature := range zeusFeatures {
|
||||||
|
featureMap[feature.Name] = feature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the map back to a FeatureSet slice
|
||||||
|
var mergedFeatures basemodel.FeatureSet
|
||||||
|
for _, feature := range featureMap {
|
||||||
|
mergedFeatures = append(mergedFeatures, feature)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedFeatures
|
||||||
|
}
|
||||||
|
|||||||
88
ee/query-service/app/api/featureFlags_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeFeatureSets(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
zeusFeatures basemodel.FeatureSet
|
||||||
|
internalFeatures basemodel.FeatureSet
|
||||||
|
expected basemodel.FeatureSet
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty zeusFeatures and internalFeatures",
|
||||||
|
zeusFeatures: basemodel.FeatureSet{},
|
||||||
|
internalFeatures: basemodel.FeatureSet{},
|
||||||
|
expected: basemodel.FeatureSet{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-empty zeusFeatures and empty internalFeatures",
|
||||||
|
zeusFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
},
|
||||||
|
internalFeatures: basemodel.FeatureSet{},
|
||||||
|
expected: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty zeusFeatures and non-empty internalFeatures",
|
||||||
|
zeusFeatures: basemodel.FeatureSet{},
|
||||||
|
internalFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
},
|
||||||
|
expected: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
|
||||||
|
zeusFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature3", Active: false},
|
||||||
|
},
|
||||||
|
internalFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature2", Active: true},
|
||||||
|
{Name: "Feature4", Active: false},
|
||||||
|
},
|
||||||
|
expected: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: true},
|
||||||
|
{Name: "Feature3", Active: false},
|
||||||
|
{Name: "Feature4", Active: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
|
||||||
|
zeusFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
},
|
||||||
|
internalFeatures: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: false},
|
||||||
|
{Name: "Feature3", Active: true},
|
||||||
|
},
|
||||||
|
expected: basemodel.FeatureSet{
|
||||||
|
{Name: "Feature1", Active: true},
|
||||||
|
{Name: "Feature2", Active: false},
|
||||||
|
{Name: "Feature3", Active: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
actual := MergeFeatureSets(test.zeusFeatures, test.internalFeatures)
|
||||||
|
assert.ElementsMatch(t, test.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
// GetMetricResultEE runs the query and returns list of time series
|
// GetMetricResultEE runs the query and returns list of time series
|
||||||
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
|
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
|
||||||
|
|
||||||
defer utils.Elapsed("GetMetricResult")()
|
defer utils.Elapsed("GetMetricResult", nil)()
|
||||||
zap.L().Info("Executing metric result query: ", zap.String("query", query))
|
zap.L().Info("Executing metric result query: ", zap.String("query", query))
|
||||||
|
|
||||||
var hash string
|
var hash string
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -28,6 +29,8 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/migrate"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
|
||||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||||
@@ -41,6 +44,7 @@ import (
|
|||||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/app/preferences"
|
||||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||||
@@ -64,7 +68,6 @@ type ServerOptions struct {
|
|||||||
// alert specific params
|
// alert specific params
|
||||||
DisableRules bool
|
DisableRules bool
|
||||||
RuleRepoURL string
|
RuleRepoURL string
|
||||||
PreferDelta bool
|
|
||||||
PreferSpanMetrics bool
|
PreferSpanMetrics bool
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
MaxOpenConns int
|
MaxOpenConns int
|
||||||
@@ -111,6 +114,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||||
|
|
||||||
|
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -119,33 +126,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
localDB.SetMaxOpenConns(10)
|
localDB.SetMaxOpenConns(10)
|
||||||
|
|
||||||
gatewayFeature := basemodel.Feature{
|
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||||
Name: "GATEWAY",
|
|
||||||
Active: false,
|
|
||||||
Usage: 0,
|
|
||||||
UsageLimit: -1,
|
|
||||||
Route: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
//Activate this feature if the url is not empty
|
|
||||||
var gatewayProxy *httputil.ReverseProxy
|
|
||||||
if serverOptions.GatewayUrl == "" {
|
|
||||||
gatewayFeature.Active = false
|
|
||||||
gatewayProxy, err = gateway.NewNoopProxy()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
zap.L().Info("Enabling gateway feature flag ...")
|
|
||||||
gatewayFeature.Active = true
|
|
||||||
gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiate license manager
|
// initiate license manager
|
||||||
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
|
lm, err := licensepkg.StartManager("sqlite", localDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -194,6 +181,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// initiate opamp
|
// initiate opamp
|
||||||
_, err = opAmpModel.InitDB(localDB)
|
_, err = opAmpModel.InitDB(localDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -256,7 +250,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
SkipConfig: skipConfig,
|
SkipConfig: skipConfig,
|
||||||
PreferDelta: serverOptions.PreferDelta,
|
|
||||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||||
MaxIdleConns: serverOptions.MaxIdleConns,
|
MaxIdleConns: serverOptions.MaxIdleConns,
|
||||||
MaxOpenConns: serverOptions.MaxOpenConns,
|
MaxOpenConns: serverOptions.MaxOpenConns,
|
||||||
@@ -325,7 +318,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
// ip here for alert manager
|
// ip here for alert manager
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
|
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
|
||||||
})
|
})
|
||||||
|
|
||||||
handler := c.Handler(r)
|
handler := c.Handler(r)
|
||||||
@@ -342,7 +335,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
|
|
||||||
// add auth middleware
|
// add auth middleware
|
||||||
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
||||||
return auth.GetUserFromRequest(r, apiHandler)
|
user, err := auth.GetUserFromRequest(r, apiHandler)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.User.OrgId == "" {
|
||||||
|
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||||
|
|
||||||
@@ -360,7 +363,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
|
||||||
})
|
})
|
||||||
|
|
||||||
handler := c.Handler(r)
|
handler := c.Handler(r)
|
||||||
@@ -416,6 +419,15 @@ func (lrw *loggingResponseWriter) Flush() {
|
|||||||
lrw.ResponseWriter.(http.Flusher).Flush()
|
lrw.ResponseWriter.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support websockets
|
||||||
|
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
h, ok := lrw.ResponseWriter.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("hijack not supported")
|
||||||
|
}
|
||||||
|
return h.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
|
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
|
||||||
pathToExtractBodyFromV3 := "/api/v3/query_range"
|
pathToExtractBodyFromV3 := "/api/v3/query_range"
|
||||||
pathToExtractBodyFromV4 := "/api/v4/query_range"
|
pathToExtractBodyFromV4 := "/api/v4/query_range"
|
||||||
@@ -734,6 +746,7 @@ func makeRulesManager(
|
|||||||
DisableRules: disableRules,
|
DisableRules: disableRules,
|
||||||
FeatureFlags: fm,
|
FeatureFlags: fm,
|
||||||
Reader: ch,
|
Reader: ch,
|
||||||
|
EvalDelay: baseconst.GetEvalDelay(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// create Manager
|
// create Manager
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
|||||||
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
||||||
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
|
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
|
||||||
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
|
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
|
||||||
|
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
|
||||||
|
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
|
||||||
|
|
||||||
func GetOrDefaultEnv(key string, fallback string) string {
|
func GetOrDefaultEnv(key string, fallback string) string {
|
||||||
v := os.Getenv(key)
|
v := os.Getenv(key)
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ import (
|
|||||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
||||||
// get auth domain from email domain
|
// get auth domain from email domain
|
||||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||||
|
|
||||||
if apierr != nil {
|
if apierr != nil {
|
||||||
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||||||
return nil, model.InternalErrorStr("failed to get domain from email")
|
return nil, model.InternalErrorStr("failed to get domain from email")
|
||||||
}
|
}
|
||||||
|
if domain == nil {
|
||||||
|
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
|
||||||
|
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
|
||||||
|
}
|
||||||
|
|
||||||
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
||||||
return nil, nil
|
return &httputil.ReverseProxy{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
|
|||||||
for _, l := range licenses {
|
for _, l := range licenses {
|
||||||
l.ParsePlan()
|
l.ParsePlan()
|
||||||
|
|
||||||
if l.Key == lm.activeLicense.Key {
|
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
|
||||||
l.IsCurrent = true
|
l.IsCurrent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func main() {
|
|||||||
|
|
||||||
var cacheConfigPath, fluxInterval string
|
var cacheConfigPath, fluxInterval string
|
||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
var preferDelta bool
|
|
||||||
var preferSpanMetrics bool
|
var preferSpanMetrics bool
|
||||||
|
|
||||||
var maxIdleConns int
|
var maxIdleConns int
|
||||||
@@ -100,14 +99,13 @@ func main() {
|
|||||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||||
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
||||||
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
||||||
flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over cumulative metrics)")
|
|
||||||
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
|
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
|
||||||
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
|
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
|
||||||
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
|
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
|
||||||
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
|
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
|
||||||
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
||||||
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
|
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
|
||||||
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(cache config to use)")
|
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
|
||||||
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
||||||
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
|
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
|
||||||
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
|
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
|
||||||
@@ -125,7 +123,6 @@ func main() {
|
|||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
HTTPHostPort: baseconst.HTTPHostPort,
|
||||||
PromConfigPath: promConfigPath,
|
PromConfigPath: promConfigPath,
|
||||||
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||||
PreferDelta: preferDelta,
|
|
||||||
PreferSpanMetrics: preferSpanMetrics,
|
PreferSpanMetrics: preferSpanMetrics,
|
||||||
PrivateHostPort: baseconst.PrivateHostPort,
|
PrivateHostPort: baseconst.PrivateHostPort,
|
||||||
DisableRules: disableRules,
|
DisableRules: disableRules,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const Enterprise = "ENTERPRISE_PLAN"
|
|||||||
const DisableUpsell = "DISABLE_UPSELL"
|
const DisableUpsell = "DISABLE_UPSELL"
|
||||||
const Onboarding = "ONBOARDING"
|
const Onboarding = "ONBOARDING"
|
||||||
const ChatSupport = "CHAT_SUPPORT"
|
const ChatSupport = "CHAT_SUPPORT"
|
||||||
|
const Gateway = "GATEWAY"
|
||||||
|
|
||||||
var BasicPlan = basemodel.FeatureSet{
|
var BasicPlan = basemodel.FeatureSet{
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
@@ -111,6 +112,13 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ProPlan = basemodel.FeatureSet{
|
var ProPlan = basemodel.FeatureSet{
|
||||||
@@ -205,6 +213,13 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnterprisePlan = basemodel.FeatureSet{
|
var EnterprisePlan = basemodel.FeatureSet{
|
||||||
@@ -313,4 +328,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const config: Config.InitialOptions = {
|
|||||||
modulePathIgnorePatterns: ['dist'],
|
modulePathIgnorePatterns: ['dist'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
|
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
extensionsToTreatAsEsm: ['.ts'],
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
"lucide-react": "0.379.0",
|
"lucide-react": "0.379.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
|
"posthog-js": "1.142.1",
|
||||||
"rc-tween-one": "3.0.6",
|
"rc-tween-one": "3.0.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-addons-update": "15.6.3",
|
"react-addons-update": "15.6.3",
|
||||||
@@ -109,6 +110,8 @@
|
|||||||
"react-syntax-highlighter": "15.5.0",
|
"react-syntax-highlighter": "15.5.0",
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
"react-virtuoso": "4.0.3",
|
"react-virtuoso": "4.0.3",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
|
"overlayscrollbars": "^2.8.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
|
|||||||
1
frontend/public/Logos/azure-aks.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="a" x1="2.94" y1="3.74" x2="8.67" y2="3.74" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="b" x1="9.13" y1="3.79" x2="14.85" y2="3.79" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="c" x1=".01" y1="9.12" x2="5.73" y2="9.12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="d" x1="6.18" y1="9.08" x2="11.9" y2="9.08" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="e" x1="12.35" y1="9.13" x2="18.08" y2="9.13" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="f" x1="2.87" y1="14.56" x2="8.6" y2="14.56" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient><linearGradient id="g" x1="9.05" y1="14.6" x2="14.78" y2="14.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b77af4"/><stop offset="1" stop-color="#773adc"/></linearGradient></defs><path fill="url(#a)" d="M5.8 1.22l-2.86.53v3.9l2.86.61 2.87-1.15V2.2L5.8 1.22z"/><path d="M5.91 6.2l2.62-1.06A.2.2 0 008.65 5V2.36a.21.21 0 00-.13-.18l-2.65-.9h-.12l-2.6.48a.2.2 0 00-.15.18v3.53a.19.19 0 00.15.19l2.63.55a.32.32 0 00.13-.01z" fill="none"/><path d="M2.94 1.75v3.9l2.89.61v-5zm1.22 3.6l-.81-.16v-3l.81-.13zm1.26.23l-.93-.15V2l.93-.16z" fill="#341a6e"/><path fill="url(#b)" d="M11.99 1.27l-2.86.53v3.9l2.86.61 2.86-1.16v-2.9l-2.86-.98z"/><path d="M9.13 1.8v3.9l2.87.61v-5zm1.21 3.6l-.81-.16v-3l.81-.13zm1.26.23l-.93-.15V2.05l.93-.17z" fill="#341a6e"/><path fill="url(#c)" d="M2.87 6.6l-2.86.53v3.9l2.86.61 2.87-1.15V7.58L2.87 6.6z"/><path d="M0 7.13V11l2.89.61v-5zm1.21 3.61l-.81-.17v-3l.81-.14zm1.27.26l-.93-.15V7.38l.93-.16z" fill="#341a6e"/><path fill="url(#d)" d="M9.04 6.56l-2.86.53v3.9l2.86.62 2.86-1.16V7.54l-2.86-.98z"/><path d="M6.18 7.09V11l2.88.61v-5zm1.21 3.61l-.81-.17v-3l.81-.14zm1.26.22l-.93-.15V7.34l.93-.16z" fill="#341a6e"/><path fill="url(#e)" d="M15.21 6.61l-2.86.53v3.9l2.86.61 2.87-1.15V7.59l-2.87-.98z"/><path d="M12.35 7.14V11l2.89.61v-5zm1.22 3.61l-.81-.17v-3l.81-.14zm1.26.22l-.93-.15V7.39l.93-.16z" fill="#341a6e"/><path fill="url(#f)" d="M5.73 12.04l-2.86.52v3.9l2.86.62 2.87-1.16v-2.9l-2.87-.98z"/><path d="M5.84 17l2.61-1a.18.18 0 00.12-.18v-2.6a.2.2 0 00-.13-.22l-2.64-.9a.17.17 0 00-.12 0l-2.6.47a.19.19 0 00-.16.19v3.54a.19.19 0 00.15.19L5.7 17a.23.23 0 00.14 0z" fill="none"/><path d="M2.87 12.56v3.9l2.89.62V12zm1.22 3.61L3.28 16v-3l.81-.14zm1.26.23l-.93-.15v-3.44l.93-.16z" fill="#341a6e"/><path fill="url(#g)" d="M11.91 12.08l-2.86.53v3.9l2.86.61 2.87-1.15v-2.91l-2.87-.98z"/><path d="M9.05 12.61v3.9l2.89.61v-5zm1.22 3.61l-.81-.17v-3l.81-.14zm1.26.22l-.93-.15v-3.43l.93-.16z" fill="#341a6e"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
1
frontend/public/Logos/azure-app-service.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="b" x1="4.4" y1="11.48" x2="4.37" y2="7.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient><linearGradient id="c" x1="10.13" y1="15.45" x2="10.13" y2="11.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient><linearGradient id="d" x1="14.18" y1="11.15" x2="14.18" y2="7.38" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ccc"/><stop offset="1" stop-color="#fcfcfc"/></linearGradient><radialGradient id="a" cx="13428.81" cy="3518.86" r="56.67" gradientTransform="matrix(.15 0 0 .15 -2005.33 -518.83)" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#5ea0ef"/><stop offset="1" stop-color="#0078d4"/></radialGradient></defs><path d="M14.21 15.72A8.5 8.5 0 013.79 2.28l.09-.06a8.5 8.5 0 0110.33 13.5" fill="url(#a)"/><path d="M6.69 7.23a13 13 0 018.91-3.58 8.47 8.47 0 00-1.49-1.44 14.34 14.34 0 00-4.69 1.1 12.54 12.54 0 00-4.08 2.82 2.76 2.76 0 011.35 1.1zM2.48 10.65a17.86 17.86 0 00-.83 2.62 7.82 7.82 0 00.62.92c.18.23.35.44.55.65a17.94 17.94 0 011.08-3.47 2.76 2.76 0 01-1.42-.72z" fill="#fff" opacity=".6"/><path d="M3.46 6.11a12 12 0 01-.69-2.94 8.15 8.15 0 00-1.1 1.45A12.69 12.69 0 002.24 7a2.69 2.69 0 011.22-.89z" fill="#f2f2f2" opacity=".55"/><circle cx="4.38" cy="8.68" r="2.73" fill="url(#b)"/><path d="M8.36 13.67a1.77 1.77 0 01.54-1.27 11.88 11.88 0 01-2.53-1.86 2.74 2.74 0 01-1.49.83 13.1 13.1 0 001.45 1.28 12.12 12.12 0 002.05 1.25 1.79 1.79 0 01-.02-.23zM14.66 13.88a12 12 0 01-2.76-.32.41.41 0 010 .11 1.75 1.75 0 01-.51 1.24 13.69 13.69 0 003.42.24A8.21 8.21 0 0016 13.81a11.5 11.5 0 01-1.34.07z" fill="#f2f2f2" opacity=".55"/><circle cx="10.13" cy="13.67" r="1.78" fill="url(#c)"/><path d="M12.32 8.93a1.83 1.83 0 01.61-1 25.5 25.5 0 01-4.46-4.14 16.91 16.91 0 01-2-2.92 7.64 7.64 0 00-1.09.42 18.14 18.14 0 002.15 3.18 26.44 26.44 0 004.79 4.46z" fill="#f2f2f2" opacity=".7"/><circle cx="14.18" cy="9.27" r="1.89" fill="url(#d)"/><path d="M17.35 10.54l-.35-.17-.3-.16h-.06l-.26-.21h-.07L16 9.8a1.76 1.76 0 01-.64.92c.12.08.25.15.38.22l.08.05.35.19.86.45a8.63 8.63 0 00.29-1.11z" fill="#f2f2f2" opacity=".55"/><circle cx="4.38" cy="8.68" r="2.73" fill="url(#b)"/><circle cx="10.13" cy="13.67" r="1.78" fill="url(#c)"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
2
frontend/public/Logos/azure-blob-storage.svg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
1
frontend/public/Logos/azure-container-apps.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><linearGradient id="b27f1ad0-7d11-4247-9da3-91bce6211f32" x1="8.798" y1="8.703" x2="14.683" y2="8.703" gradientUnits="userSpaceOnUse"><stop offset="0.001" stop-color="#773adc" /><stop offset="1" stop-color="#552f99" /></linearGradient><linearGradient id="b2f92112-4ca9-4b17-a019-c9f26c1a4a8f" x1="5.764" y1="3.777" x2="5.764" y2="13.78" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a67af4" /><stop offset="0.999" stop-color="#773adc" /></linearGradient></defs><g id="b8a0486a-5501-4d92-b540-a766c4b3b548"><g><g><g><path d="M16.932,11.578a8.448,8.448,0,0,1-7.95,5.59,8.15,8.15,0,0,1-2.33-.33,2.133,2.133,0,0,0,.18-.83c.01,0,.03.01.04.01a7.422,7.422,0,0,0,2.11.3,7.646,7.646,0,0,0,6.85-4.28l.01-.01Z" fill="#32bedd" /><path d="M3.582,14.068a2.025,2.025,0,0,0-.64.56,8.6,8.6,0,0,1-1.67-2.44l1.04.23v.26a.6.6,0,0,0,.47.59l.14.03a6.136,6.136,0,0,0,.62.73Z" fill="#32bedd" /><path d="M12.352.958a2.28,2.28,0,0,0-.27.81c-.02-.01-.05-.02-.07-.03a7.479,7.479,0,0,0-3.03-.63,7.643,7.643,0,0,0-5.9,2.8l-.29.06a.6.6,0,0,0-.48.58v.46l-1.02.19A8.454,8.454,0,0,1,8.982.268,8.6,8.6,0,0,1,12.352.958Z" fill="#32bedd" /><path d="M16.872,5.7l-1.09-.38a6.6,6.6,0,0,0-.72-1.16c-.02-.03-.04-.05-.05-.07a2.083,2.083,0,0,0,.72-.45A7.81,7.81,0,0,1,16.872,5.7Z" fill="#32bedd" /><path d="M10.072,11.908l2.54.56L8.672,14.1c-.02,0-.03.01-.05.01a.154.154,0,0,1-.15-.15V3.448a.154.154,0,0,1,.15-.15.09.09,0,0,1,.05.01l4.46,1.56-3.05.57a.565.565,0,0,0-.44.54v5.4A.537.537,0,0,0,10.072,11.908Z" fill="#fff" /><g><g id="e918f286-5032-4942-ad29-ea17e6f1cc90"><path d="M1.1,5.668l1.21-.23v6.55l-1.23-.27-.99-.22a.111.111,0,0,1-.09-.12v-5.4a.12.12,0,0,1,.09-.12Z" fill="#a67af4" /></g><g><g id="a47a99dd-4d47-4c70-8c42-c5ac274ce496"><g><path d="M10.072,11.908l2.54.56L8.672,14.1c-.02,0-.03.01-.05.01a.154.154,0,0,1-.15-.15V3.448a.154.154,0,0,1,.15-.15.09.09,0,0,1,.05.01l4.46,1.56-3.05.57a.565.565,0,0,0-.44.54v5.4A.537.537,0,0,0,10.072,11.908Z" fill="url(#b27f1ad0-7d11-4247-9da3-91bce6211f32)" /><path d="M8.586,3.3,2.878,4.378a.177.177,0,0,0-.14.175V12.68a.177.177,0,0,0,.137.174L8.581,14.1a.176.176,0,0,0,.21-.174V3.478A.175.175,0,0,0,8.619,3.3Z" fill="url(#b2f92112-4ca9-4b17-a019-c9f26c1a4a8f)" /></g></g><polygon points="5.948 4.921 5.948 12.483 7.934 12.814 7.934 4.564 5.948 4.921" fill="#b796f9" opacity="0.5" /><polygon points="3.509 5.329 3.509 11.954 5.238 12.317 5.238 5.031 3.509 5.329" fill="#b796f9" opacity="0.5" /></g></g></g><path d="M16,2.048a1.755,1.755,0,1,1-1.76-1.76A1.756,1.756,0,0,1,16,2.048Z" fill="#32bedd" /><circle cx="4.65" cy="15.973" r="1.759" fill="#32bedd" /></g><path d="M18,6.689v3.844a.222.222,0,0,1-.133.2l-.766.316-3.07,1.268-.011,0a.126.126,0,0,1-.038,0,.1.1,0,0,1-.1-.1V5.234a.1.1,0,0,1,.054-.088l0,0,.019,0a.031.031,0,0,1,.019,0,.055.055,0,0,1,.034.008l.011,0,.012,0L17.05,6.2l.8.282A.213.213,0,0,1,18,6.689Z" fill="#773adc" /><path d="M13.959,5.14l-3.8.715a.118.118,0,0,0-.093.117v5.409a.118.118,0,0,0,.091.116l3.8.831a.115.115,0,0,0,.137-.09.109.109,0,0,0,0-.026V5.256a.117.117,0,0,0-.115-.118A.082.082,0,0,0,13.959,5.14Z" fill="#a67af4" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
1
frontend/public/Logos/azure-functions.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="a" x1="-175.993" y1="-343.723" x2="-175.993" y2="-359.232" gradientTransform="matrix(1.156 0 0 1.029 212.573 370.548)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fea11b"/><stop offset=".284" stop-color="#fea51a"/><stop offset=".547" stop-color="#feb018"/><stop offset=".8" stop-color="#ffc314"/><stop offset="1" stop-color="#ffd70f"/></linearGradient></defs><path d="M5.54 13.105l-.586.588a.267.267 0 01-.377 0L.223 9.353a.533.533 0 010-.755l.588-.59 4.732 4.718a.267.267 0 010 .378z" fill="#50e6ff"/><path d="M4.863 4.305l.59.588a.267.267 0 010 .378L.806 9.932l-.59-.589a.533.533 0 01-.001-.754l4.273-4.285a.267.267 0 01.376 0z" fill="#1490df"/><path d="M17.19 8.012l.588.59a.533.533 0 01-.001.754l-4.354 4.34a.267.267 0 01-.377 0l-.586-.587a.267.267 0 010-.377l4.732-4.718z" fill="#50e6ff"/><path d="M17.782 9.34l-.59.589-4.648-4.662a.267.267 0 010-.377l.59-.588a.267.267 0 01.378 0l4.273 4.286a.533.533 0 010 .753z" fill="#1490df"/><path d="M8.459 9.9H4.87a.193.193 0 01-.2-.181.166.166 0 01.018-.075L8.991 1.13a.206.206 0 01.186-.106h4.245a.193.193 0 01.2.181.165.165 0 01-.035.1L8.534 7.966h4.928a.193.193 0 01.2.181.176.176 0 01-.052.122l-8.189 8.519c-.077.046-.624.5-.356-.189z" fill="url(#a)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/Logos/azure-sql-database-metrics.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><radialGradient id="b" cx="9.36" cy="10.57" r="7.07" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2f2f2"/><stop offset=".58" stop-color="#eee"/><stop offset="1" stop-color="#e6e6e6"/></radialGradient><linearGradient id="a" x1="2.59" y1="10.16" x2="15.41" y2="10.16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005ba1"/><stop offset=".07" stop-color="#0060a9"/><stop offset=".36" stop-color="#0071c8"/><stop offset=".52" stop-color="#0078d4"/><stop offset=".64" stop-color="#0074cd"/><stop offset=".82" stop-color="#006abb"/><stop offset="1" stop-color="#005ba1"/></linearGradient></defs><path d="M9 5.14c-3.54 0-6.41-1-6.41-2.32v12.36c0 1.27 2.82 2.3 6.32 2.32H9c3.54 0 6.41-1 6.41-2.32V2.82c0 1.29-2.87 2.32-6.41 2.32z" fill="url(#a)"/><path d="M15.41 2.82c0 1.29-2.87 2.32-6.41 2.32s-6.41-1-6.41-2.32S5.46.5 9 .5s6.41 1 6.41 2.32" fill="#e8e8e8"/><path d="M13.92 2.63c0 .82-2.21 1.48-4.92 1.48s-4.92-.66-4.92-1.48S6.29 1.16 9 1.16s4.92.66 4.92 1.47" fill="#50e6ff"/><path d="M9 3a11.55 11.55 0 00-3.89.57A11.42 11.42 0 009 4.11a11.15 11.15 0 003.89-.58A11.84 11.84 0 009 3z" fill="#198ab3"/><path d="M12.9 11.4V8H12v4.13h2.46v-.73zM5.76 9.73a1.83 1.83 0 01-.51-.31.44.44 0 01-.12-.32.34.34 0 01.15-.3.68.68 0 01.42-.12 1.62 1.62 0 011 .29v-.86a2.58 2.58 0 00-1-.16 1.64 1.64 0 00-1.09.34 1.08 1.08 0 00-.42.89c0 .51.32.91 1 1.21a2.88 2.88 0 01.62.36.42.42 0 01.15.32.38.38 0 01-.16.31.81.81 0 01-.45.11 1.66 1.66 0 01-1.09-.42V12a2.17 2.17 0 001.07.24 1.88 1.88 0 001.18-.33 1.08 1.08 0 00.33-.91 1.05 1.05 0 00-.25-.7 2.42 2.42 0 00-.83-.57zM11 11.32a2.34 2.34 0 00.33-1.26A2.32 2.32 0 0011 9a1.81 1.81 0 00-.7-.75 2 2 0 00-1-.26 2.11 2.11 0 00-1.08.27 1.86 1.86 0 00-.73.74 2.46 2.46 0 00-.26 1.14 2.26 2.26 0 00.24 1 1.76 1.76 0 00.69.74 2.06 2.06 0 001 .3l.86 1h1.21L10 12.08a1.79 1.79 0 001-.76zm-1-.25a.94.94 0 01-.76.35.92.92 0 01-.76-.36 1.52 1.52 0 01-.29-1 1.53 1.53 0 01.29-1 1 1 0 01.78-.37.87.87 0 01.75.37 1.62 1.62 0 01.27 1 1.46 1.46 0 01-.28 1.01z" fill="url(#b)"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/public/Logos/azure-vm.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><defs><linearGradient id="a" x1="8.88" y1="12.21" x2="8.88" y2=".21" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0078d4"/><stop offset=".82" stop-color="#5ea0ef"/></linearGradient><linearGradient id="b" x1="8.88" y1="16.84" x2="8.88" y2="12.21" gradientUnits="userSpaceOnUse"><stop offset=".15" stop-color="#ccc"/><stop offset="1" stop-color="#707070"/></linearGradient></defs><rect x="-.12" y=".21" width="18" height="12" rx=".6" fill="url(#a)"/><path fill="#50e6ff" d="M11.88 4.46v3.49l-3 1.76v-3.5l3-1.75z"/><path fill="#c3f1ff" d="M11.88 4.46l-3 1.76-3-1.76 3-1.75 3 1.75z"/><path fill="#9cebff" d="M8.88 6.22v3.49l-3-1.76V4.46l3 1.76z"/><path fill="#c3f1ff" d="M5.88 7.95l3-1.74v3.5l-3-1.76z"/><path fill="#9cebff" d="M11.88 7.95l-3-1.74v3.5l3-1.76z"/><path d="M12.49 15.84c-1.78-.28-1.85-1.56-1.85-3.63H7.11c0 2.07-.06 3.35-1.84 3.63a1 1 0 00-.89 1h9a1 1 0 00-.89-1z" fill="url(#b)"/></svg>
|
||||||
|
After Width: | Height: | Size: 973 B |
BIN
frontend/public/fonts/GeistMonoVF.woff2
Normal file
8
frontend/public/locales/en-GB/onboarding.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"invite_user": "Invite your teammates",
|
||||||
|
"invite": "Invite",
|
||||||
|
"skip": "Skip",
|
||||||
|
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||||
|
"select_use_case": "Select a use-case to get started",
|
||||||
|
"get_started": "Get Started"
|
||||||
|
}
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
"share": "Share",
|
"share": "Share",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"logged_in": "Logged In"
|
"logged_in": "Logged In",
|
||||||
|
"pending_data_placeholder": "Just a bit of patience, just a little bit’s enough ⎯ we’re getting your {{dataSource}}!"
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/public/locales/en/onboarding.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"invite_user": "Invite your teammates",
|
||||||
|
"invite": "Invite",
|
||||||
|
"skip": "Skip",
|
||||||
|
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||||
|
"select_use_case": "Select a use-case to get started",
|
||||||
|
"get_started": "Get Started"
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
||||||
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
||||||
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
|
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
|
||||||
|
"GET_STARTED_AZURE_MONITORING": "SigNoz | Get Started | AZURE",
|
||||||
"TRACE": "SigNoz | Trace",
|
"TRACE": "SigNoz | Trace",
|
||||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||||
|
|||||||
@@ -76,9 +76,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
isUserFetching: false,
|
isUserFetching: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
history.push(ROUTES.LOGIN);
|
history.push(ROUTES.LOGIN, { from: pathname });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@@ -17,6 +18,7 @@ import { NotificationProvider } from 'hooks/useNotifications';
|
|||||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { identity, pick, pickBy } from 'lodash-es';
|
import { identity, pick, pickBy } from 'lodash-es';
|
||||||
|
import posthog from 'posthog-js';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
@@ -38,7 +40,7 @@ import defaultRoutes, {
|
|||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const themeConfig = useThemeConfig();
|
const themeConfig = useThemeConfig();
|
||||||
const { data } = useLicense();
|
const { data: licenseData } = useLicense();
|
||||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||||
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -47,7 +49,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const { trackPageView, trackEvent } = useAnalytics();
|
const { trackPageView } = useAnalytics();
|
||||||
|
|
||||||
const { hostname, pathname } = window.location;
|
const { hostname, pathname } = window.location;
|
||||||
|
|
||||||
@@ -64,6 +66,14 @@ function App(): JSX.Element {
|
|||||||
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
|
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
|
||||||
false;
|
false;
|
||||||
|
|
||||||
|
const isPremiumSupportEnabled =
|
||||||
|
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
|
||||||
|
false;
|
||||||
|
|
||||||
|
const showAddCreditCardModal =
|
||||||
|
!isPremiumSupportEnabled &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -80,7 +90,7 @@ function App(): JSX.Element {
|
|||||||
setRoutes(newRoutes);
|
setRoutes(newRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoggedInState && isChatSupportEnabled) {
|
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.Intercom('boot', {
|
window.Intercom('boot', {
|
||||||
@@ -92,10 +102,10 @@ function App(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isOnBasicPlan =
|
const isOnBasicPlan =
|
||||||
data?.payload?.licenses?.some(
|
licenseData?.payload?.licenses?.some(
|
||||||
(license) =>
|
(license) =>
|
||||||
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
||||||
) || data?.payload?.licenses === null;
|
) || licenseData?.payload?.licenses === null;
|
||||||
|
|
||||||
const enableAnalytics = (user: User): void => {
|
const enableAnalytics = (user: User): void => {
|
||||||
const orgName =
|
const orgName =
|
||||||
@@ -112,9 +122,7 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
||||||
|
|
||||||
const domain = extractDomain(email);
|
const domain = extractDomain(email);
|
||||||
|
|
||||||
const hostNameParts = hostname.split('.');
|
const hostNameParts = hostname.split('.');
|
||||||
|
|
||||||
const groupTraits = {
|
const groupTraits = {
|
||||||
@@ -127,10 +135,30 @@ function App(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.analytics.identify(email, sanitizedIdentifyPayload);
|
window.analytics.identify(email, sanitizedIdentifyPayload);
|
||||||
|
|
||||||
window.analytics.group(domain, groupTraits);
|
window.analytics.group(domain, groupTraits);
|
||||||
|
|
||||||
window.clarity('identify', email, name);
|
window.clarity('identify', email, name);
|
||||||
|
|
||||||
|
posthog?.identify(email, {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
posthog?.group('company', domain, {
|
||||||
|
name: orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,10 +172,6 @@ function App(): JSX.Element {
|
|||||||
!isIdentifiedUser
|
!isIdentifiedUser
|
||||||
) {
|
) {
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||||
|
|
||||||
if (isCloudUserVal) {
|
|
||||||
enableAnalytics(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -184,7 +208,7 @@ function App(): JSX.Element {
|
|||||||
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
||||||
);
|
);
|
||||||
if (!isThemeAnalyticsSent) {
|
if (!isThemeAnalyticsSent) {
|
||||||
trackEvent('Theme Analytics', {
|
logEvent('Theme Analytics', {
|
||||||
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
@@ -195,6 +219,11 @@ function App(): JSX.Element {
|
|||||||
console.error('Failed to parse local storage theme analytics event');
|
console.error('Failed to parse local storage theme analytics event');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCloudUserVal && user && user.email) {
|
||||||
|
enableAnalytics(user);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
|||||||
// making the error status code as standard Error Status Code
|
// making the error status code as standard Error Status Code
|
||||||
const statusCode = response.status as ErrorStatusCode;
|
const statusCode = response.status as ErrorStatusCode;
|
||||||
|
|
||||||
if (statusCode >= 400 && statusCode < 500) {
|
|
||||||
const { data } = response as AxiosResponse;
|
const { data } = response as AxiosResponse;
|
||||||
|
|
||||||
|
if (statusCode >= 400 && statusCode < 500) {
|
||||||
if (statusCode === 404) {
|
if (statusCode === 404) {
|
||||||
return {
|
return {
|
||||||
statusCode,
|
statusCode,
|
||||||
@@ -34,12 +34,11 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
|||||||
body: JSON.stringify((response.data as any).data),
|
body: JSON.stringify((response.data as any).data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode,
|
statusCode,
|
||||||
payload: null,
|
payload: null,
|
||||||
error: 'Something went wrong',
|
error: 'Something went wrong',
|
||||||
message: null,
|
message: data?.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (request) {
|
if (request) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const apiV1 = '/api/v1/';
|
|||||||
export const apiV2 = '/api/v2/';
|
export const apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
export const apiAlertManager = '/api/alertmanager';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|
||||||
export default apiV1;
|
export default apiV1;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'api';
|
import { ApiBaseInstance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@@ -21,6 +21,7 @@ const logEvent = async (
|
|||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ const interceptorRejected = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const interceptorRejectedBase = async (
|
||||||
|
value: AxiosResponse<any>,
|
||||||
|
): Promise<AxiosResponse<any>> => Promise.reject(value);
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
});
|
});
|
||||||
@@ -140,6 +144,18 @@ ApiV4Instance.interceptors.response.use(
|
|||||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// axios Base
|
||||||
|
export const ApiBaseInstance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiBaseInstance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejectedBase,
|
||||||
|
);
|
||||||
|
ApiBaseInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
// gateway Api V1
|
// gateway Api V1
|
||||||
export const GatewayApiV1Instance = axios.create({
|
export const GatewayApiV1Instance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
|
interface GetTopLevelOperationsProps {
|
||||||
const response = await axios.post(`/service/top_level_operations`);
|
service?: string;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTopLevelOperations = async (
|
||||||
|
props: GetTopLevelOperationsProps,
|
||||||
|
): Promise<ServiceDataProps> => {
|
||||||
|
const response = await axios.post(`/service/top_level_operations`, {
|
||||||
|
start: !isNil(props.start) ? `${props.start}` : undefined,
|
||||||
|
end: !isNil(props.end) ? `${props.end}` : undefined,
|
||||||
|
service: props.service,
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { Dayjs } from 'dayjs';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
import { Recurrence } from './getAllDowntimeSchedules';
|
import { Recurrence } from './getAllDowntimeSchedules';
|
||||||
@@ -11,8 +12,8 @@ export interface DowntimeSchedulePayload {
|
|||||||
alertIds: string[];
|
alertIds: string[];
|
||||||
schedule: {
|
schedule: {
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
startTime?: string;
|
startTime?: string | Dayjs;
|
||||||
endTime?: string;
|
endTime?: string | Dayjs;
|
||||||
recurrence?: Recurrence;
|
recurrence?: Recurrence;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu';
|
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
|
||||||
import { useQuery, UseQueryResult } from 'react-query';
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
|
||||||
export type Recurrence = {
|
export type Recurrence = {
|
||||||
@@ -28,6 +28,7 @@ export interface DowntimeSchedules {
|
|||||||
createdBy: string | null;
|
createdBy: string | null;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
updatedBy: string | null;
|
updatedBy: string | null;
|
||||||
|
kind: string | null;
|
||||||
}
|
}
|
||||||
export type PayloadProps = { data: DowntimeSchedules[] };
|
export type PayloadProps = { data: DowntimeSchedules[] };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { Button, Modal, Typography } from 'antd';
|
||||||
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { CreditCard, X } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
|
import { License } from 'types/api/licenses/def';
|
||||||
|
|
||||||
|
export default function ChatSupportGateway(): JSX.Element {
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
|
|
||||||
|
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeValidLicense =
|
||||||
|
licenseData?.payload?.licenses?.find(
|
||||||
|
(license) => license.isCurrent === true,
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
setActiveLicense(activeValidLicense);
|
||||||
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
|
const handleBillingOnSuccess = (
|
||||||
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
|
): void => {
|
||||||
|
if (data?.payload?.redirectURL) {
|
||||||
|
const newTab = document.createElement('a');
|
||||||
|
newTab.href = data.payload.redirectURL;
|
||||||
|
newTab.target = '_blank';
|
||||||
|
newTab.rel = 'noopener noreferrer';
|
||||||
|
newTab.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBillingOnError = (): void => {
|
||||||
|
notifications.error({
|
||||||
|
message: SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
|
||||||
|
updateCreditCardApi,
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
handleBillingOnSuccess(data);
|
||||||
|
},
|
||||||
|
onError: handleBillingOnError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddCreditCard = (): void => {
|
||||||
|
logEvent('Add Credit card modal: Clicked', {
|
||||||
|
source: `intercom icon`,
|
||||||
|
page: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCreditCard({
|
||||||
|
licenseKey: activeLicense?.key || '',
|
||||||
|
successURL: window.location.href,
|
||||||
|
cancelURL: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="chat-support-gateway">
|
||||||
|
<Button
|
||||||
|
className="chat-support-gateway-btn"
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('Disabled Chat Support: Clicked', {
|
||||||
|
source: `intercom icon`,
|
||||||
|
page: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsAddCreditCardModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 28 32"
|
||||||
|
className="chat-support-gateway-btn-icon"
|
||||||
|
>
|
||||||
|
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Credit Card Modal */}
|
||||||
|
<Modal
|
||||||
|
className="add-credit-card-modal"
|
||||||
|
title={<span className="title">Add Credit Card for Chat Support</span>}
|
||||||
|
open={isAddCreditCardModalOpen}
|
||||||
|
closable
|
||||||
|
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
|
||||||
|
destroyOnClose
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={(): void => setIsAddCreditCardModalOpen(false)}
|
||||||
|
className="cancel-btn"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
icon={<CreditCard size={16} />}
|
||||||
|
size="middle"
|
||||||
|
loading={isLoadingBilling}
|
||||||
|
disabled={isLoadingBilling}
|
||||||
|
onClick={handleAddCreditCard}
|
||||||
|
className="add-credit-card-btn"
|
||||||
|
>
|
||||||
|
Add Credit Card
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Typography.Text className="add-credit-card-text">
|
||||||
|
You're currently on <span className="highlight-text">Trial plan</span>
|
||||||
|
. Add a credit card to access SigNoz chat support to your workspace.
|
||||||
|
</Typography.Text>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -287,7 +287,7 @@ function CustomTimePicker({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
arrow={false}
|
arrow={false}
|
||||||
trigger="hover"
|
trigger="click"
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ import './DropDown.styles.scss';
|
|||||||
import { EllipsisOutlined } from '@ant-design/icons';
|
import { EllipsisOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown, MenuProps } from 'antd';
|
import { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
function DropDown({
|
||||||
|
element,
|
||||||
|
onDropDownItemClick,
|
||||||
|
}: {
|
||||||
|
element: JSX.Element[];
|
||||||
|
onDropDownItemClick?: MenuProps['onClick'];
|
||||||
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const items: MenuProps['items'] = element.map(
|
const items: MenuProps['items'] = element.map(
|
||||||
@@ -14,12 +21,25 @@ function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isDdOpen, setDdOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={{ items }}>
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items,
|
||||||
|
onMouseEnter: (): void => setDdOpen(true),
|
||||||
|
onMouseLeave: (): void => setDdOpen(false),
|
||||||
|
onClick: (item): void => onDropDownItemClick?.(item),
|
||||||
|
}}
|
||||||
|
open={isDdOpen}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
|
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
|
||||||
onClick={(e): void => e.preventDefault()}
|
onClick={(e): void => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDdOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<EllipsisOutlined className="dropdown-icon" />
|
<EllipsisOutlined className="dropdown-icon" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -27,4 +47,8 @@ function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DropDown.defaultProps = {
|
||||||
|
onDropDownItemClick: (): void => {},
|
||||||
|
};
|
||||||
|
|
||||||
export default DropDown;
|
export default DropDown;
|
||||||
|
|||||||
184
frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import './LaunchChatSupport.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||||
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
|
import { CreditCard, HelpCircle, X } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
|
import { License } from 'types/api/licenses/def';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
|
export interface LaunchChatSupportProps {
|
||||||
|
eventName: string;
|
||||||
|
attributes: Record<string, unknown>;
|
||||||
|
message?: string;
|
||||||
|
buttonText?: string;
|
||||||
|
className?: string;
|
||||||
|
onHoverText?: string;
|
||||||
|
intercomMessageDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
function LaunchChatSupport({
|
||||||
|
attributes,
|
||||||
|
eventName,
|
||||||
|
message = '',
|
||||||
|
buttonText = '',
|
||||||
|
className = '',
|
||||||
|
onHoverText = '',
|
||||||
|
intercomMessageDisabled = false,
|
||||||
|
}: LaunchChatSupportProps): JSX.Element | null {
|
||||||
|
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
|
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPremiumChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
|
|
||||||
|
const showAddCreditCardModal =
|
||||||
|
!isPremiumChatSupportEnabled &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeValidLicense =
|
||||||
|
licenseData?.payload?.licenses?.find(
|
||||||
|
(license) => license.isCurrent === true,
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
setActiveLicense(activeValidLicense);
|
||||||
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
|
const handleFacingIssuesClick = (): void => {
|
||||||
|
if (showAddCreditCardModal) {
|
||||||
|
setIsAddCreditCardModalOpen(true);
|
||||||
|
} else {
|
||||||
|
logEvent(eventName, attributes);
|
||||||
|
if (window.Intercom && !intercomMessageDisabled) {
|
||||||
|
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBillingOnSuccess = (
|
||||||
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
|
): void => {
|
||||||
|
if (data?.payload?.redirectURL) {
|
||||||
|
const newTab = document.createElement('a');
|
||||||
|
newTab.href = data.payload.redirectURL;
|
||||||
|
newTab.target = '_blank';
|
||||||
|
newTab.rel = 'noopener noreferrer';
|
||||||
|
newTab.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBillingOnError = (): void => {
|
||||||
|
notifications.error({
|
||||||
|
message: SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
|
||||||
|
updateCreditCardApi,
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
handleBillingOnSuccess(data);
|
||||||
|
},
|
||||||
|
onError: handleBillingOnError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddCreditCard = (): void => {
|
||||||
|
logEvent('Add Credit card modal: Clicked', {
|
||||||
|
source: `facing issues button`,
|
||||||
|
page: '',
|
||||||
|
...attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCreditCard({
|
||||||
|
licenseKey: activeLicense?.key || '',
|
||||||
|
successURL: window.location.href,
|
||||||
|
cancelURL: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
|
||||||
|
<div className="facing-issue-button">
|
||||||
|
<Tooltip
|
||||||
|
title={onHoverText}
|
||||||
|
autoAdjustOverflow
|
||||||
|
style={{ padding: 8 }}
|
||||||
|
overlayClassName="tooltip-overlay"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={cx('periscope-btn', 'facing-issue-button', className)}
|
||||||
|
onClick={handleFacingIssuesClick}
|
||||||
|
icon={<HelpCircle size={14} />}
|
||||||
|
>
|
||||||
|
{buttonText || 'Facing issues?'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Add Credit Card Modal */}
|
||||||
|
<Modal
|
||||||
|
className="add-credit-card-modal"
|
||||||
|
title={<span className="title">Add Credit Card for Chat Support</span>}
|
||||||
|
open={isAddCreditCardModalOpen}
|
||||||
|
closable
|
||||||
|
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
|
||||||
|
destroyOnClose
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="cancel"
|
||||||
|
onClick={(): void => setIsAddCreditCardModalOpen(false)}
|
||||||
|
className="cancel-btn"
|
||||||
|
icon={<X size={16} />}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
icon={<CreditCard size={16} />}
|
||||||
|
size="middle"
|
||||||
|
loading={isLoadingBilling}
|
||||||
|
disabled={isLoadingBilling}
|
||||||
|
onClick={handleAddCreditCard}
|
||||||
|
className="add-credit-card-btn"
|
||||||
|
>
|
||||||
|
Add Credit Card
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Typography.Text className="add-credit-card-text">
|
||||||
|
You're currently on <span className="highlight-text">Trial plan</span>
|
||||||
|
. Add a credit card to access SigNoz chat support to your workspace.
|
||||||
|
</Typography.Text>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchChatSupport.defaultProps = {
|
||||||
|
message: '',
|
||||||
|
buttonText: '',
|
||||||
|
className: '',
|
||||||
|
onHoverText: '',
|
||||||
|
intercomMessageDisabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LaunchChatSupport;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DrawerProps } from 'antd';
|
import { DrawerProps } from 'antd';
|
||||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||||
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
import { VIEWS } from './constants';
|
import { VIEWS } from './constants';
|
||||||
@@ -9,6 +10,7 @@ export type LogDetailProps = {
|
|||||||
log: ILog | null;
|
log: ILog | null;
|
||||||
selectedTab: VIEWS;
|
selectedTab: VIEWS;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
|
listViewPanelSelectedFields?: IField[] | null;
|
||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||||
Pick<DrawerProps, 'onClose'>;
|
Pick<DrawerProps, 'onClose'>;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
.log-body {
|
.log-body {
|
||||||
font-family: 'SF Mono';
|
font-family: 'SF Mono';
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
|
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
|||||||
import { RadioChangeEvent } from 'antd/lib';
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
import Overview from 'container/LogDetailedView/Overview';
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||||
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import {
|
import {
|
||||||
@@ -21,9 +24,10 @@ import {
|
|||||||
TextSelect,
|
TextSelect,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { VIEW_TYPES, VIEWS } from './constants';
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
@@ -36,6 +40,7 @@ function LogDetail({
|
|||||||
onClickActionItem,
|
onClickActionItem,
|
||||||
selectedTab,
|
selectedTab,
|
||||||
isListViewPanel = false,
|
isListViewPanel = false,
|
||||||
|
listViewPanelSelectedFields,
|
||||||
}: LogDetailProps): JSX.Element {
|
}: LogDetailProps): JSX.Element {
|
||||||
const [, copyToClipboard] = useCopyToClipboard();
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||||
@@ -45,6 +50,19 @@ function LogDetail({
|
|||||||
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
|
const { initialDataSource, stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const listQuery = useMemo(() => {
|
||||||
|
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||||
|
|
||||||
|
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||||
|
}, [stagedQuery]);
|
||||||
|
|
||||||
|
const { options } = useOptionsMenu({
|
||||||
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
|
dataSource: initialDataSource || DataSource.LOGS,
|
||||||
|
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||||
|
});
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
@@ -192,6 +210,8 @@ function LogDetail({
|
|||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
onClickActionItem={onClickActionItem}
|
onClickActionItem={onClickActionItem}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
|
selectedOptions={options}
|
||||||
|
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import './AddToQueryHOC.styles.scss';
|
|||||||
|
|
||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
@@ -10,9 +10,10 @@ function AddToQueryHOC({
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
children,
|
children,
|
||||||
}: AddToQueryHOCProps): JSX.Element {
|
}: AddToQueryHOCProps): JSX.Element {
|
||||||
const handleQueryAdd = useCallback(() => {
|
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
|
||||||
|
event.stopPropagation();
|
||||||
onAddToQuery(fieldKey, fieldValue, OPERATORS.IN);
|
onAddToQuery(fieldKey, fieldValue, OPERATORS.IN);
|
||||||
}, [fieldKey, fieldValue, onAddToQuery]);
|
};
|
||||||
|
|
||||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||||
fieldKey,
|
fieldKey,
|
||||||
|
|||||||
@@ -28,12 +28,17 @@ export const SEVERITY_TEXT_TYPE = {
|
|||||||
FATAL2: 'FATAL2',
|
FATAL2: 'FATAL2',
|
||||||
FATAL3: 'FATAL3',
|
FATAL3: 'FATAL3',
|
||||||
FATAL4: 'FATAL4',
|
FATAL4: 'FATAL4',
|
||||||
|
UNKNOWN: 'UNKNOWN',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const LogType = {
|
export const LogType = {
|
||||||
|
TRACE: 'TRACE',
|
||||||
|
DEBUG: 'DEBUG',
|
||||||
INFO: 'INFO',
|
INFO: 'INFO',
|
||||||
WARNING: 'WARNING',
|
WARN: 'WARN',
|
||||||
ERROR: 'ERROR',
|
ERROR: 'ERROR',
|
||||||
|
FATAL: 'FATAL',
|
||||||
|
UNKNOWN: 'UNKNOWN',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function LogStateIndicator({
|
function LogStateIndicator({
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
|
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
|
||||||
|
|
||||||
describe('getLogIndicatorType', () => {
|
describe('getLogIndicatorType', () => {
|
||||||
it('should return severity type for valid log with severityText', () => {
|
it('severity_number should be given priority over severity_text', () => {
|
||||||
const log = {
|
const log = {
|
||||||
date: '2024-02-29T12:34:46Z',
|
date: '2024-02-29T12:34:46Z',
|
||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
@@ -20,11 +21,57 @@ describe('getLogIndicatorType', () => {
|
|||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
severity_text: 'INFO',
|
severity_text: 'INFO',
|
||||||
|
severity_number: 2,
|
||||||
};
|
};
|
||||||
expect(getLogIndicatorType(log)).toBe('INFO');
|
// severity_number should get priority over severity_text
|
||||||
|
expect(getLogIndicatorType(log)).toBe('TRACE');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return log level if severityText is missing', () => {
|
it('severity_text should be used when severity_number is absent ', () => {
|
||||||
|
const log = {
|
||||||
|
date: '2024-02-29T12:34:46Z',
|
||||||
|
timestamp: 1646115296,
|
||||||
|
id: '123456',
|
||||||
|
traceId: '987654',
|
||||||
|
spanId: '54321',
|
||||||
|
traceFlags: 0,
|
||||||
|
severityText: 'INFO',
|
||||||
|
severityNumber: 2,
|
||||||
|
body: 'Sample log Message',
|
||||||
|
resources_string: {},
|
||||||
|
attributesString: {},
|
||||||
|
attributes_string: {},
|
||||||
|
attributesInt: {},
|
||||||
|
attributesFloat: {},
|
||||||
|
severity_text: 'FATAL',
|
||||||
|
severity_number: 0,
|
||||||
|
};
|
||||||
|
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case insensitive severity_text should be valid', () => {
|
||||||
|
const log = {
|
||||||
|
date: '2024-02-29T12:34:46Z',
|
||||||
|
timestamp: 1646115296,
|
||||||
|
id: '123456',
|
||||||
|
traceId: '987654',
|
||||||
|
spanId: '54321',
|
||||||
|
traceFlags: 0,
|
||||||
|
severityText: 'INFO',
|
||||||
|
severityNumber: 2,
|
||||||
|
body: 'Sample log Message',
|
||||||
|
resources_string: {},
|
||||||
|
attributesString: {},
|
||||||
|
attributes_string: {},
|
||||||
|
attributesInt: {},
|
||||||
|
attributesFloat: {},
|
||||||
|
severity_text: 'fatAl',
|
||||||
|
severity_number: 0,
|
||||||
|
};
|
||||||
|
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return log level if severityText and severityNumber is missing', () => {
|
||||||
const log: ILog = {
|
const log: ILog = {
|
||||||
date: '2024-02-29T12:34:58Z',
|
date: '2024-02-29T12:34:58Z',
|
||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
@@ -36,13 +83,16 @@ describe('getLogIndicatorType', () => {
|
|||||||
body: 'Sample log',
|
body: 'Sample log',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
attributes_string: {},
|
attributes_string: {
|
||||||
|
log_level: 'INFO' as never,
|
||||||
|
},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
severity_text: 'FATAL',
|
severity_text: 'some_random',
|
||||||
severityText: '',
|
severityText: '',
|
||||||
|
severity_number: 0,
|
||||||
};
|
};
|
||||||
expect(getLogIndicatorType(log)).toBe('FATAL');
|
expect(getLogIndicatorType(log)).toBe('INFO');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +105,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanId: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
|
severityNumber: 2,
|
||||||
severity_number: 2,
|
severity_number: 2,
|
||||||
body: 'Sample log message',
|
body: 'Sample log message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
@@ -64,7 +115,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
severity_text: 'WARN',
|
severity_text: 'WARN',
|
||||||
};
|
};
|
||||||
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
|
expect(getLogIndicatorTypeForTable(log)).toBe('TRACE');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return log level if severityText is missing', () => {
|
it('should return log level if severityText is missing', () => {
|
||||||
@@ -75,7 +126,8 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanId: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityNumber: 2,
|
severityNumber: 0,
|
||||||
|
severity_number: 0,
|
||||||
body: 'Sample log message',
|
body: 'Sample log message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
@@ -87,3 +139,47 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
|
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('logIndicatorBySeverityNumber', () => {
|
||||||
|
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
||||||
|
const logLevelExpectations = [
|
||||||
|
{ minSevNumber: 1, maxSevNumber: 4, expectedIndicatorType: 'TRACE' },
|
||||||
|
{ minSevNumber: 5, maxSevNumber: 8, expectedIndicatorType: 'DEBUG' },
|
||||||
|
{ minSevNumber: 9, maxSevNumber: 12, expectedIndicatorType: 'INFO' },
|
||||||
|
{ minSevNumber: 13, maxSevNumber: 16, expectedIndicatorType: 'WARN' },
|
||||||
|
{ minSevNumber: 17, maxSevNumber: 20, expectedIndicatorType: 'ERROR' },
|
||||||
|
{ minSevNumber: 21, maxSevNumber: 24, expectedIndicatorType: 'FATAL' },
|
||||||
|
];
|
||||||
|
logLevelExpectations.forEach((e) => {
|
||||||
|
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
|
||||||
|
const sevText = (Math.random() + 1).toString(36).substring(2);
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
date: '2024-02-29T12:34:46Z',
|
||||||
|
timestamp: 1646115296,
|
||||||
|
id: '123456',
|
||||||
|
traceId: '987654',
|
||||||
|
spanId: '54321',
|
||||||
|
traceFlags: 0,
|
||||||
|
severityText: sevText,
|
||||||
|
severityNumber: sevNum,
|
||||||
|
body: 'Sample log Message',
|
||||||
|
resources_string: {},
|
||||||
|
attributesString: {},
|
||||||
|
attributes_string: {},
|
||||||
|
attributesInt: {},
|
||||||
|
attributesFloat: {},
|
||||||
|
severity_text: sevText,
|
||||||
|
severity_number: sevNum,
|
||||||
|
};
|
||||||
|
|
||||||
|
it(`getLogIndicatorType should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
|
||||||
|
expect(getLogIndicatorType(log)).toBe(e.expectedIndicatorType);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`getLogIndicatorTypeForTable should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
|
||||||
|
expect(getLogIndicatorTypeForTable(log)).toBe(e.expectedIndicatorType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,56 +2,112 @@ import { ILog } from 'types/api/logs/log';
|
|||||||
|
|
||||||
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
|
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
|
||||||
|
|
||||||
const getSeverityType = (severityText: string): string => {
|
const getLogTypeBySeverityText = (severityText: string): string => {
|
||||||
switch (severityText) {
|
switch (severityText) {
|
||||||
case SEVERITY_TEXT_TYPE.TRACE:
|
case SEVERITY_TEXT_TYPE.TRACE:
|
||||||
case SEVERITY_TEXT_TYPE.TRACE2:
|
case SEVERITY_TEXT_TYPE.TRACE2:
|
||||||
case SEVERITY_TEXT_TYPE.TRACE3:
|
case SEVERITY_TEXT_TYPE.TRACE3:
|
||||||
case SEVERITY_TEXT_TYPE.TRACE4:
|
case SEVERITY_TEXT_TYPE.TRACE4:
|
||||||
return SEVERITY_TEXT_TYPE.TRACE;
|
return LogType.TRACE;
|
||||||
case SEVERITY_TEXT_TYPE.DEBUG:
|
case SEVERITY_TEXT_TYPE.DEBUG:
|
||||||
case SEVERITY_TEXT_TYPE.DEBUG2:
|
case SEVERITY_TEXT_TYPE.DEBUG2:
|
||||||
case SEVERITY_TEXT_TYPE.DEBUG3:
|
case SEVERITY_TEXT_TYPE.DEBUG3:
|
||||||
case SEVERITY_TEXT_TYPE.DEBUG4:
|
case SEVERITY_TEXT_TYPE.DEBUG4:
|
||||||
return SEVERITY_TEXT_TYPE.DEBUG;
|
return LogType.DEBUG;
|
||||||
case SEVERITY_TEXT_TYPE.INFO:
|
case SEVERITY_TEXT_TYPE.INFO:
|
||||||
case SEVERITY_TEXT_TYPE.INFO2:
|
case SEVERITY_TEXT_TYPE.INFO2:
|
||||||
case SEVERITY_TEXT_TYPE.INFO3:
|
case SEVERITY_TEXT_TYPE.INFO3:
|
||||||
case SEVERITY_TEXT_TYPE.INFO4:
|
case SEVERITY_TEXT_TYPE.INFO4:
|
||||||
return SEVERITY_TEXT_TYPE.INFO;
|
return LogType.INFO;
|
||||||
case SEVERITY_TEXT_TYPE.WARN:
|
case SEVERITY_TEXT_TYPE.WARN:
|
||||||
case SEVERITY_TEXT_TYPE.WARN2:
|
case SEVERITY_TEXT_TYPE.WARN2:
|
||||||
case SEVERITY_TEXT_TYPE.WARN3:
|
case SEVERITY_TEXT_TYPE.WARN3:
|
||||||
case SEVERITY_TEXT_TYPE.WARN4:
|
case SEVERITY_TEXT_TYPE.WARN4:
|
||||||
case SEVERITY_TEXT_TYPE.WARNING:
|
case SEVERITY_TEXT_TYPE.WARNING:
|
||||||
return SEVERITY_TEXT_TYPE.WARN;
|
return LogType.WARN;
|
||||||
case SEVERITY_TEXT_TYPE.ERROR:
|
case SEVERITY_TEXT_TYPE.ERROR:
|
||||||
case SEVERITY_TEXT_TYPE.ERROR2:
|
case SEVERITY_TEXT_TYPE.ERROR2:
|
||||||
case SEVERITY_TEXT_TYPE.ERROR3:
|
case SEVERITY_TEXT_TYPE.ERROR3:
|
||||||
case SEVERITY_TEXT_TYPE.ERROR4:
|
case SEVERITY_TEXT_TYPE.ERROR4:
|
||||||
return SEVERITY_TEXT_TYPE.ERROR;
|
return LogType.ERROR;
|
||||||
case SEVERITY_TEXT_TYPE.FATAL:
|
case SEVERITY_TEXT_TYPE.FATAL:
|
||||||
case SEVERITY_TEXT_TYPE.FATAL2:
|
case SEVERITY_TEXT_TYPE.FATAL2:
|
||||||
case SEVERITY_TEXT_TYPE.FATAL3:
|
case SEVERITY_TEXT_TYPE.FATAL3:
|
||||||
case SEVERITY_TEXT_TYPE.FATAL4:
|
case SEVERITY_TEXT_TYPE.FATAL4:
|
||||||
return SEVERITY_TEXT_TYPE.FATAL;
|
return LogType.FATAL;
|
||||||
default:
|
default:
|
||||||
return SEVERITY_TEXT_TYPE.INFO;
|
return LogType.UNKNOWN;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogIndicatorType = (logData: ILog): string => {
|
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
||||||
if (logData.severity_text) {
|
const getLogTypeBySeverityNumber = (severityNumber: number): string => {
|
||||||
return getSeverityType(logData.severity_text);
|
if (severityNumber < 1) {
|
||||||
|
return LogType.UNKNOWN;
|
||||||
}
|
}
|
||||||
return logData.attributes_string?.log_level || LogType.INFO;
|
if (severityNumber < 5) {
|
||||||
|
return LogType.TRACE;
|
||||||
|
}
|
||||||
|
if (severityNumber < 9) {
|
||||||
|
return LogType.DEBUG;
|
||||||
|
}
|
||||||
|
if (severityNumber < 13) {
|
||||||
|
return LogType.INFO;
|
||||||
|
}
|
||||||
|
if (severityNumber < 17) {
|
||||||
|
return LogType.WARN;
|
||||||
|
}
|
||||||
|
if (severityNumber < 21) {
|
||||||
|
return LogType.ERROR;
|
||||||
|
}
|
||||||
|
if (severityNumber < 25) {
|
||||||
|
return LogType.FATAL;
|
||||||
|
}
|
||||||
|
return LogType.UNKNOWN;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogType = (
|
||||||
|
severityText: string,
|
||||||
|
severityNumber: number,
|
||||||
|
defaultType: string,
|
||||||
|
): string => {
|
||||||
|
// give priority to the severityNumber
|
||||||
|
if (severityNumber) {
|
||||||
|
const logType = getLogTypeBySeverityNumber(severityNumber);
|
||||||
|
if (logType !== LogType.UNKNOWN) {
|
||||||
|
return logType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// is severityNumber is not present then rely on the severityText
|
||||||
|
if (severityText) {
|
||||||
|
const logType = getLogTypeBySeverityText(severityText);
|
||||||
|
if (logType !== LogType.UNKNOWN) {
|
||||||
|
return logType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLogIndicatorType = (logData: ILog): string => {
|
||||||
|
const defaultType = logData.attributes_string?.log_level || LogType.INFO;
|
||||||
|
// convert the severity_text to upper case for the comparison to support case insensitive values
|
||||||
|
return getLogType(
|
||||||
|
logData?.severity_text?.toUpperCase(),
|
||||||
|
logData?.severity_number || 0,
|
||||||
|
defaultType,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogIndicatorTypeForTable = (
|
export const getLogIndicatorTypeForTable = (
|
||||||
log: Record<string, unknown>,
|
log: Record<string, unknown>,
|
||||||
): string => {
|
): string => {
|
||||||
if (log.severity_text) {
|
const defaultType = (log.log_level as string) || LogType.INFO;
|
||||||
return getSeverityType(log.severity_text as string);
|
// convert the severity_text to upper case for the comparison to support case insensitive values
|
||||||
}
|
return getLogType(
|
||||||
return (log.log_level as string) || LogType.INFO;
|
(log?.severity_text as string)?.toUpperCase(),
|
||||||
|
(log?.severity_number as number) || 0,
|
||||||
|
defaultType,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ function RawLogView({
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||||
|
|
||||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
|
||||||
|
|
||||||
const logType = getLogIndicatorType(data);
|
const logType = getLogIndicatorType(data);
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
@@ -88,17 +86,16 @@ function RawLogView({
|
|||||||
attributesText += ' | ';
|
attributesText += ' | ';
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = useMemo(
|
const text = useMemo(() => {
|
||||||
() =>
|
const date =
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
? `${dayjs(data.timestamp).format(
|
? dayjs(data.timestamp)
|
||||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
: dayjs(data.timestamp / 1e6);
|
||||||
)} | ${attributesText} ${severityText} ${data.body}`
|
|
||||||
: `${dayjs(data.timestamp / 1e6).format(
|
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
|
||||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
data.body
|
||||||
)} | ${attributesText} ${severityText} ${data.body}`,
|
}`;
|
||||||
[data.timestamp, data.body, severityText, attributesText],
|
}, [data.timestamp, data.body, attributesText]);
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickExpand = useCallback(() => {
|
const handleClickExpand = useCallback(() => {
|
||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ExpandIconWrapper = styled(Col)`
|
|||||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: 'SF Mono', monospace;
|
font-family: 'SF Mono', monospace;
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: 'Geist Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||||
|
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import { CSSProperties, ReactElement, useMemo } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactElement;
|
||||||
|
isVirtuoso?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options?: PartialOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function OverlayScrollbar({
|
||||||
|
children,
|
||||||
|
isVirtuoso,
|
||||||
|
style,
|
||||||
|
options: customOptions,
|
||||||
|
}: Props): any {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
scrollbars: {
|
||||||
|
autoHide: 'scroll',
|
||||||
|
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
|
||||||
|
},
|
||||||
|
...(customOptions || {}),
|
||||||
|
} as PartialOptions),
|
||||||
|
[customOptions, isDarkMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isVirtuoso) {
|
||||||
|
return (
|
||||||
|
<VirtuosoOverlayScrollbar style={style} options={options}>
|
||||||
|
{children}
|
||||||
|
</VirtuosoOverlayScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TypicalOverlayScrollbar style={style} options={options}>
|
||||||
|
{children}
|
||||||
|
</TypicalOverlayScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayScrollbar.defaultProps = {
|
||||||
|
isVirtuoso: false,
|
||||||
|
style: {},
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverlayScrollbar;
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
import './DynamicColumnTable.syles.scss';
|
import './DynamicColumnTable.syles.scss';
|
||||||
|
|
||||||
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||||
|
import { ColumnGroupType, ColumnType } from 'antd/es/table';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
import { SlidersHorizontal } from 'lucide-react';
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
@@ -22,6 +24,7 @@ function DynamicColumnTable({
|
|||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
facingIssueBtn,
|
facingIssueBtn,
|
||||||
|
shouldSendAlertsLogEvent,
|
||||||
...restProps
|
...restProps
|
||||||
}: DynamicColumnTableProps): JSX.Element {
|
}: DynamicColumnTableProps): JSX.Element {
|
||||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||||
@@ -47,11 +50,18 @@ function DynamicColumnTable({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [columns, dynamicColumns]);
|
}, [columns, dynamicColumns]);
|
||||||
|
|
||||||
const onToggleHandler = (index: number) => (
|
const onToggleHandler = (
|
||||||
checked: boolean,
|
index: number,
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
column: ColumnGroupType<any> | ColumnType<any>,
|
||||||
): void => {
|
) => (checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (shouldSendAlertsLogEvent) {
|
||||||
|
logEvent('Alert: Column toggled', {
|
||||||
|
column: column?.title,
|
||||||
|
action: checked ? 'Enable' : 'Disable',
|
||||||
|
});
|
||||||
|
}
|
||||||
setVisibleColumns({
|
setVisibleColumns({
|
||||||
tablesource,
|
tablesource,
|
||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
@@ -75,7 +85,7 @@ function DynamicColumnTable({
|
|||||||
<div>{column.title?.toString()}</div>
|
<div>{column.title?.toString()}</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||||
onChange={onToggleHandler(index)}
|
onChange={onToggleHandler(index, column)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -86,7 +96,7 @@ function DynamicColumnTable({
|
|||||||
return (
|
return (
|
||||||
<div className="DynamicColumnTable">
|
<div className="DynamicColumnTable">
|
||||||
<Flex justify="flex-end" align="center" gap={8}>
|
<Flex justify="flex-end" align="center" gap={8}>
|
||||||
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
|
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
|
||||||
{dynamicColumns && (
|
{dynamicColumns && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
import { dragColumnParams } from 'hooks/useDragColumns/configs';
|
||||||
|
import { set } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -20,6 +21,7 @@ import { ResizeTableProps } from './types';
|
|||||||
function ResizeTable({
|
function ResizeTable({
|
||||||
columns,
|
columns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
|
pagination,
|
||||||
...restProps
|
...restProps
|
||||||
}: ResizeTableProps): JSX.Element {
|
}: ResizeTableProps): JSX.Element {
|
||||||
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
||||||
@@ -58,15 +60,22 @@ function ResizeTable({
|
|||||||
[columnsData, onDragColumn, handleResize],
|
[columnsData, onDragColumn, handleResize],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableParams = useMemo(
|
const tableParams = useMemo(() => {
|
||||||
() => ({
|
const props = {
|
||||||
...restProps,
|
...restProps,
|
||||||
components: { header: { cell: ResizableHeader } },
|
components: { header: { cell: ResizableHeader } },
|
||||||
columns: mergedColumns,
|
columns: mergedColumns,
|
||||||
}),
|
};
|
||||||
[mergedColumns, restProps],
|
|
||||||
|
set(
|
||||||
|
props,
|
||||||
|
'pagination',
|
||||||
|
pagination ? { ...pagination, hideOnSinglePage: true } : false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}, [mergedColumns, pagination, restProps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (columns) {
|
if (columns) {
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
||||||
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
|
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
|
|
||||||
import { TableDataSource } from './contants';
|
import { TableDataSource } from './contants';
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ export interface DynamicColumnTableProps extends TableProps<any> {
|
|||||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||||
dynamicColumns: TableProps<any>['columns'];
|
dynamicColumns: TableProps<any>['columns'];
|
||||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||||
facingIssueBtn?: FacingIssueBtnProps;
|
facingIssueBtn?: LaunchChatSupportProps;
|
||||||
|
shouldSendAlertsLogEvent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetVisibleColumnsFunction = (
|
export type GetVisibleColumnsFunction = (
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Button } from 'antd';
|
|||||||
import { Tag } from 'antd/lib';
|
import { Tag } from 'antd/lib';
|
||||||
import Input from 'components/Input';
|
import Input from 'components/Input';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
import { TweenOneGroup } from 'rc-tween-one';
|
|
||||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
||||||
@@ -46,41 +45,19 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
|||||||
func(value);
|
func(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const forMap = (tag: string): React.ReactElement => (
|
|
||||||
<span key={tag} style={{ display: 'inline-block' }}>
|
|
||||||
<Tag
|
|
||||||
closable
|
|
||||||
onClose={(e): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleClose(tag);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagChild = tags.map(forMap);
|
|
||||||
|
|
||||||
const renderTagsAnimated = (): React.ReactElement => (
|
|
||||||
<TweenOneGroup
|
|
||||||
appear={false}
|
|
||||||
className="tags"
|
|
||||||
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
|
|
||||||
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
|
|
||||||
onEnd={(e): void => {
|
|
||||||
if (e.type === 'appear' || e.type === 'enter') {
|
|
||||||
(e.target as any).style = 'display: inline-block';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tagChild}
|
|
||||||
</TweenOneGroup>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tags-container">
|
<div className="tags-container">
|
||||||
{renderTagsAnimated()}
|
{tags.map<React.ReactNode>((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={tag}
|
||||||
|
closable
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
onClose={(): void => handleClose(tag)}
|
||||||
|
>
|
||||||
|
<span>{tag}</span>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
|
||||||
{inputVisible && (
|
{inputVisible && (
|
||||||
<div className="add-tag-container">
|
<div className="add-tag-container">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Tooltip } from 'antd';
|
|||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import { style } from './constant';
|
import { style } from './constant';
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ function TextToolTip({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip getTooltipContainer={popupContainer} overlay={overlay}>
|
<Tooltip overlay={overlay}>
|
||||||
{useFilledIcon ? (
|
{useFilledIcon ? (
|
||||||
<QuestionCircleFilled style={iconStyle} />
|
<QuestionCircleFilled style={iconStyle} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import './typicalOverlayScrollbar.scss';
|
||||||
|
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import { CSSProperties, ReactElement } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactElement;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options?: PartialOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TypicalOverlayScrollbar({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
options,
|
||||||
|
}: Props): ReturnType<typeof OverlayScrollbarsComponent> {
|
||||||
|
return (
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
defer
|
||||||
|
options={options}
|
||||||
|
style={style}
|
||||||
|
className="overlay-scrollbar"
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypicalOverlayScrollbar.defaultProps = { style: {}, options: {} };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.overlay-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import './Uplot.styles.scss';
|
import './Uplot.styles.scss';
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { LineChart } from 'lucide-react';
|
import { LineChart } from 'lucide-react';
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
|
||||||
import UPlot from 'uplot';
|
import UPlot from 'uplot';
|
||||||
|
|
||||||
import { dataMatch, optionsUpdateState } from './utils';
|
import { dataMatch, optionsUpdateState } from './utils';
|
||||||
@@ -139,7 +139,7 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<div className="uplot-graph-container" ref={targetRef}>
|
<div className="uplot-graph-container" ref={targetRef}>
|
||||||
{data && data[0] && data[0]?.length === 0 ? (
|
{data && data[0] && data[0]?.length === 0 ? (
|
||||||
<div className="not-found">
|
<div className="not-found">
|
||||||
@@ -147,7 +147,7 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ function ValueGraph({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||||
<ExclamationCircleFilled className="value-graph-icon" />
|
<ExclamationCircleFilled
|
||||||
|
className="value-graph-icon"
|
||||||
|
data-testid="conflicting-thresholds"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import './virtuosoOverlayScrollbar.scss';
|
||||||
|
|
||||||
|
import useInitializeOverlayScrollbar from 'hooks/useInitializeOverlayScrollbar/useInitializeOverlayScrollbar';
|
||||||
|
import { PartialOptions } from 'overlayscrollbars';
|
||||||
|
import React, { CSSProperties, ReactElement } from 'react';
|
||||||
|
|
||||||
|
interface VirtuosoOverlayScrollbarProps {
|
||||||
|
children: ReactElement;
|
||||||
|
style?: CSSProperties;
|
||||||
|
options: PartialOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VirtuosoOverlayScrollbar({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
options,
|
||||||
|
}: VirtuosoOverlayScrollbarProps): JSX.Element {
|
||||||
|
const { rootRef, setScroller } = useInitializeOverlayScrollbar(options);
|
||||||
|
|
||||||
|
const enhancedChild = React.cloneElement(children, {
|
||||||
|
scrollerRef: setScroller,
|
||||||
|
'data-overlayscrollbars-initialize': true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-overlayscrollbars-initialize
|
||||||
|
ref={rootRef}
|
||||||
|
className="overlay-scroll-wrapper"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{enhancedChild}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtuosoOverlayScrollbar.defaultProps = { style: {} };
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.overlay-scroll-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import './FacingIssueBtn.style.scss';
|
|
||||||
|
|
||||||
import { Button, Tooltip } from 'antd';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import cx from 'classnames';
|
|
||||||
import { FeatureKeys } from 'constants/features';
|
|
||||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
|
||||||
import { defaultTo } from 'lodash-es';
|
|
||||||
import { HelpCircle } from 'lucide-react';
|
|
||||||
import { isCloudUser } from 'utils/app';
|
|
||||||
|
|
||||||
export interface FacingIssueBtnProps {
|
|
||||||
eventName: string;
|
|
||||||
attributes: Record<string, unknown>;
|
|
||||||
message?: string;
|
|
||||||
buttonText?: string;
|
|
||||||
className?: string;
|
|
||||||
onHoverText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FacingIssueBtn({
|
|
||||||
attributes,
|
|
||||||
eventName,
|
|
||||||
message = '',
|
|
||||||
buttonText = '',
|
|
||||||
className = '',
|
|
||||||
onHoverText = '',
|
|
||||||
}: FacingIssueBtnProps): JSX.Element | null {
|
|
||||||
const handleFacingIssuesClick = (): void => {
|
|
||||||
logEvent(eventName, attributes);
|
|
||||||
|
|
||||||
if (window.Intercom) {
|
|
||||||
window.Intercom('showNewMessage', defaultTo(message, ''));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
|
|
||||||
const isCloudUserVal = isCloudUser();
|
|
||||||
|
|
||||||
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
|
|
||||||
<div className="facing-issue-button">
|
|
||||||
<Tooltip
|
|
||||||
title={onHoverText}
|
|
||||||
autoAdjustOverflow
|
|
||||||
style={{ padding: 8 }}
|
|
||||||
overlayClassName="tooltip-overlay"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={cx('periscope-btn', 'facing-issue-button', className)}
|
|
||||||
onClick={handleFacingIssuesClick}
|
|
||||||
icon={<HelpCircle size={14} />}
|
|
||||||
>
|
|
||||||
{buttonText || 'Facing issues?'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
FacingIssueBtn.defaultProps = {
|
|
||||||
message: '',
|
|
||||||
buttonText: '',
|
|
||||||
className: '',
|
|
||||||
onHoverText: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FacingIssueBtn;
|
|
||||||
@@ -19,6 +19,6 @@ export enum FeatureKeys {
|
|||||||
OSS = 'OSS',
|
OSS = 'OSS',
|
||||||
ONBOARDING = 'ONBOARDING',
|
ONBOARDING = 'ONBOARDING',
|
||||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||||
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
|
|
||||||
GATEWAY = 'GATEWAY',
|
GATEWAY = 'GATEWAY',
|
||||||
|
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,5 @@ export const getComponentForPanelType = (
|
|||||||
export const AVAILABLE_EXPORT_PANEL_TYPES = [
|
export const AVAILABLE_EXPORT_PANEL_TYPES = [
|
||||||
PANEL_TYPES.TIME_SERIES,
|
PANEL_TYPES.TIME_SERIES,
|
||||||
PANEL_TYPES.TABLE,
|
PANEL_TYPES.TABLE,
|
||||||
|
PANEL_TYPES.LIST,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
|||||||
value: QueryFunctionsTypes.ABSOLUTE,
|
value: QueryFunctionsTypes.ABSOLUTE,
|
||||||
label: 'Absolute',
|
label: 'Absolute',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: QueryFunctionsTypes.RUNNING_DIFF,
|
||||||
|
label: 'Running Diff',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: QueryFunctionsTypes.LOG_2,
|
value: QueryFunctionsTypes.LOG_2,
|
||||||
label: 'Log2',
|
label: 'Log2',
|
||||||
@@ -103,6 +107,9 @@ export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
|||||||
absolute: {
|
absolute: {
|
||||||
showInput: false,
|
showInput: false,
|
||||||
},
|
},
|
||||||
|
runningDiff: {
|
||||||
|
showInput: false,
|
||||||
|
},
|
||||||
log2: {
|
log2: {
|
||||||
showInput: false,
|
showInput: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const ROUTES = {
|
|||||||
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
||||||
'/get-started/infrastructure-monitoring',
|
'/get-started/infrastructure-monitoring',
|
||||||
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
|
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
|
||||||
|
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
|
||||||
USAGE_EXPLORER: '/usage-explorer',
|
USAGE_EXPLORER: '/usage-explorer',
|
||||||
APPLICATION: '/services',
|
APPLICATION: '/services',
|
||||||
ALL_DASHBOARD: '/dashboard',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import AlertChannels from 'container/AllAlertChannels';
|
||||||
|
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||||
|
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
jest.mock('hooks/useFetch', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
payload: allAlertChannels,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const successNotification = jest.fn();
|
||||||
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useNotifications: jest.fn(() => ({
|
||||||
|
notifications: {
|
||||||
|
success: successNotification,
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Alert Channels Settings List page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<AlertChannels />);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
describe('Should display the Alert Channels page properly', () => {
|
||||||
|
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
|
||||||
|
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if "New Alert Channel" Button is visble ', () => {
|
||||||
|
expect(screen.getByText('button_new_channel')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
|
||||||
|
const helpIcon = screen.getByLabelText('question-circle');
|
||||||
|
|
||||||
|
fireEvent.mouseOver(helpIcon);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const tooltip = screen.getByText('tooltip_notification_channels');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Should check if the channels table is properly displayed', () => {
|
||||||
|
it('Should check if the table columns are properly displayed', () => {
|
||||||
|
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('column_channel_action')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the data in the table is displayed properly', () => {
|
||||||
|
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('column_channel_edit')[0]).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('Delete')[0]).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if clicking on Delete displays Success Toast "Channel Deleted Successfully"', async () => {
|
||||||
|
const deleteButton = screen.getAllByRole('button', { name: 'Delete' })[0];
|
||||||
|
expect(deleteButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(successNotification).toBeCalledWith({
|
||||||
|
message: 'Success',
|
||||||
|
description: 'channel_delete_success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import AlertChannels from 'container/AllAlertChannels';
|
||||||
|
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
|
||||||
|
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
jest.mock('hooks/useFetch', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
payload: allAlertChannels,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const successNotification = jest.fn();
|
||||||
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useNotifications: jest.fn(() => ({
|
||||||
|
notifications: {
|
||||||
|
success: successNotification,
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useComponentPermission', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => [false]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Alert Channels Settings List page (Normal User)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<AlertChannels />);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
describe('Should display the Alert Channels page properly', () => {
|
||||||
|
it('Should check if "The alerts will be sent to all the configured channels." is visible ', () => {
|
||||||
|
expect(screen.getByText('sending_channels_note')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if "New Alert Channel" Button is visble and disabled', () => {
|
||||||
|
const newAlertButton = screen.getByRole('button', {
|
||||||
|
name: 'plus button_new_channel',
|
||||||
|
});
|
||||||
|
expect(newAlertButton).toBeInTheDocument();
|
||||||
|
expect(newAlertButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
it('Should check if the help icon is visible and displays "tooltip_notification_channels ', async () => {
|
||||||
|
const helpIcon = screen.getByLabelText('question-circle');
|
||||||
|
|
||||||
|
fireEvent.mouseOver(helpIcon);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const tooltip = screen.getByText('tooltip_notification_channels');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Should check if the channels table is properly displayed', () => {
|
||||||
|
it('Should check if the table columns are properly displayed', () => {
|
||||||
|
expect(screen.getByText('column_channel_name')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('column_channel_type')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('column_channel_action')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the data in the table is displayed properly', () => {
|
||||||
|
expect(screen.getByText('Dummy-Channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('slack')[0]).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('column_channel_edit')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
|
||||||
|
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||||
|
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||||
|
import {
|
||||||
|
opsGenieDescriptionDefaultValue,
|
||||||
|
opsGenieMessageDefaultValue,
|
||||||
|
opsGeniePriorityDefaultValue,
|
||||||
|
pagerDutyAdditionalDetailsDefaultValue,
|
||||||
|
pagerDutyDescriptionDefaultVaule,
|
||||||
|
pagerDutySeverityTextDefaultValue,
|
||||||
|
slackDescriptionDefaultValue,
|
||||||
|
slackTitleDefaultValue,
|
||||||
|
} from 'mocks-server/__mockdata__/alerts';
|
||||||
|
import { server } from 'mocks-server/server';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import { testLabelInputAndHelpValue } from './testUtils';
|
||||||
|
|
||||||
|
const successNotification = jest.fn();
|
||||||
|
const errorNotification = jest.fn();
|
||||||
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useNotifications: jest.fn(() => ({
|
||||||
|
notifications: {
|
||||||
|
success: successNotification,
|
||||||
|
error: errorNotification,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useFeatureFlag', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
active: true,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Create Alert Channel', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Slack} />);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
it('Should check if the title is "New Notification Channels"', () => {
|
||||||
|
expect(screen.getByText('page_title_create')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_name',
|
||||||
|
testId: 'channel-name-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_send_resolved',
|
||||||
|
testId: 'field-send-resolved-checkbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_type',
|
||||||
|
testId: 'channel-type-select',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Default Channel type (Slack) fields
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||||
|
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_recipient',
|
||||||
|
testId: 'slack-channel-textbox',
|
||||||
|
helpText: 'slack_channel_help',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_title',
|
||||||
|
testId: 'title-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Title contains template', () => {
|
||||||
|
const titleTextArea = screen.getByTestId('title-textarea');
|
||||||
|
|
||||||
|
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Description label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_description',
|
||||||
|
testId: 'description-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Description contains template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||||
|
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if saving the form without filling the name displays "Something went wrong"', async () => {
|
||||||
|
const saveButton = screen.getByRole('button', {
|
||||||
|
name: 'button_save_channel',
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(errorNotification).toHaveBeenCalledWith({
|
||||||
|
description: 'Something went wrong',
|
||||||
|
message: 'Error',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.post('http://localhost/api/v1/testChannel', (req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
status: 'success',
|
||||||
|
data: 'test alert sent',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const testButton = screen.getByRole('button', {
|
||||||
|
name: 'button_test_channel',
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(testButton);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(successNotification).toHaveBeenCalledWith({
|
||||||
|
message: 'Success',
|
||||||
|
description: 'channel_test_done',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Should check if clicking on Test button shows "Something went wrong" error message if testing fails', async () => {
|
||||||
|
const testButton = screen.getByRole('button', {
|
||||||
|
name: 'button_test_channel',
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(testButton);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(errorNotification).toHaveBeenCalledWith({
|
||||||
|
message: 'Error',
|
||||||
|
description: 'channel_test_failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||||
|
describe('Webhook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
|
||||||
|
expect(screen.getByText('Webhook')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_username',
|
||||||
|
testId: 'webhook-username-textbox',
|
||||||
|
helpText: 'help_webhook_username',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Password label and textbox, and help text are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'Password (optional)',
|
||||||
|
testId: 'webhook-password-textbox',
|
||||||
|
helpText: 'help_webhook_password',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('PagerDuty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
|
||||||
|
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_routing_key',
|
||||||
|
testId: 'pager-routing-key-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_description',
|
||||||
|
testId: 'pager-description-textarea',
|
||||||
|
helpText: 'help_pager_description',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if the description contains default template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId(
|
||||||
|
'pager-description-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(
|
||||||
|
pagerDutyDescriptionDefaultVaule,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_severity',
|
||||||
|
testId: 'pager-severity-textbox',
|
||||||
|
helpText: 'help_pager_severity',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Severity contains the default template', () => {
|
||||||
|
const severityTextbox = screen.getByTestId('pager-severity-textbox');
|
||||||
|
|
||||||
|
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_details',
|
||||||
|
testId: 'pager-additional-details-textarea',
|
||||||
|
helpText: 'help_pager_details',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Additional Information contains the default template', () => {
|
||||||
|
const detailsTextArea = screen.getByTestId(
|
||||||
|
'pager-additional-details-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_group',
|
||||||
|
testId: 'pager-group-textarea',
|
||||||
|
helpText: 'help_pager_group',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_class',
|
||||||
|
testId: 'pager-class-textarea',
|
||||||
|
helpText: 'help_pager_class',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_client',
|
||||||
|
testId: 'pager-client-textarea',
|
||||||
|
helpText: 'help_pager_client',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
|
||||||
|
const clientTextArea = screen.getByTestId('pager-client-textarea');
|
||||||
|
|
||||||
|
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
|
||||||
|
});
|
||||||
|
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_client_url',
|
||||||
|
testId: 'pager-client-url-textarea',
|
||||||
|
helpText: 'help_pager_client_url',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
|
||||||
|
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
|
||||||
|
|
||||||
|
expect(clientUrlTextArea).toHaveValue(
|
||||||
|
'https://enter-signoz-host-n-port-here/alerts',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Opsgenie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
|
||||||
|
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if API key label, required, and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_api_key',
|
||||||
|
testId: 'opsgenie-api-key-textbox',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_message',
|
||||||
|
testId: 'opsgenie-message-textarea',
|
||||||
|
helpText: 'help_opsgenie_message',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message contains the default template ', () => {
|
||||||
|
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
|
||||||
|
|
||||||
|
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_description',
|
||||||
|
testId: 'opsgenie-description-textarea',
|
||||||
|
helpText: 'help_opsgenie_description',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId(
|
||||||
|
'opsgenie-description-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(
|
||||||
|
opsGenieDescriptionDefaultValue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_priority',
|
||||||
|
testId: 'opsgenie-priority-textarea',
|
||||||
|
helpText: 'help_opsgenie_priority',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message contains the default template', () => {
|
||||||
|
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
|
||||||
|
|
||||||
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Opsgenie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Email"', () => {
|
||||||
|
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_email_to',
|
||||||
|
testId: 'email-to-textbox',
|
||||||
|
helpText: 'help_email_to',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Microsoft Teams', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
|
||||||
|
expect(screen.getByText('msteams')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_title',
|
||||||
|
testId: 'title-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title contains template', () => {
|
||||||
|
const titleTextArea = screen.getByTestId('title-textarea');
|
||||||
|
|
||||||
|
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Description label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_description',
|
||||||
|
testId: 'description-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description contains template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
|
||||||
|
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
|
||||||
|
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||||
|
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||||
|
import {
|
||||||
|
opsGenieDescriptionDefaultValue,
|
||||||
|
opsGenieMessageDefaultValue,
|
||||||
|
opsGeniePriorityDefaultValue,
|
||||||
|
pagerDutyAdditionalDetailsDefaultValue,
|
||||||
|
pagerDutyDescriptionDefaultVaule,
|
||||||
|
pagerDutySeverityTextDefaultValue,
|
||||||
|
slackDescriptionDefaultValue,
|
||||||
|
slackTitleDefaultValue,
|
||||||
|
} from 'mocks-server/__mockdata__/alerts';
|
||||||
|
import { render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import { testLabelInputAndHelpValue } from './testUtils';
|
||||||
|
|
||||||
|
describe('Create Alert Channel (Normal User)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
describe('Should check if the new alert channel is properly displayed with the cascading fields of slack channel ', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Slack} />);
|
||||||
|
});
|
||||||
|
it('Should check if the title is "New Notification Channels"', () => {
|
||||||
|
expect(screen.getByText('page_title_create')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_name',
|
||||||
|
testId: 'channel-name-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Send resolved alerts label and checkbox are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_send_resolved',
|
||||||
|
testId: 'field-send-resolved-checkbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_type',
|
||||||
|
testId: 'channel-type-select',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Default Channel type (Slack) fields
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||||
|
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_recipient',
|
||||||
|
testId: 'slack-channel-textbox',
|
||||||
|
helpText: 'slack_channel_help',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_title',
|
||||||
|
testId: 'title-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Title contains template', () => {
|
||||||
|
const titleTextArea = screen.getByTestId('title-textarea');
|
||||||
|
|
||||||
|
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Description label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_description',
|
||||||
|
testId: 'description-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Description contains template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||||
|
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||||
|
describe('Webhook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Webhook} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Webhook"', () => {
|
||||||
|
expect(screen.getByText('Webhook')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Webhook User Name label, input, and help text are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_username',
|
||||||
|
testId: 'webhook-username-textbox',
|
||||||
|
helpText: 'help_webhook_username',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Password label and textbox, and help text are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'Password (optional)',
|
||||||
|
testId: 'webhook-password-textbox',
|
||||||
|
helpText: 'help_webhook_password',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('PagerDuty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Pagerduty} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Pagerduty"', () => {
|
||||||
|
expect(screen.getByText('Pagerduty')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if Routing key label, required, and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_routing_key',
|
||||||
|
testId: 'pager-routing-key-textbox',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in pagerduty), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_description',
|
||||||
|
testId: 'pager-description-textarea',
|
||||||
|
helpText: 'help_pager_description',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if the description contains default template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId(
|
||||||
|
'pager-description-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(
|
||||||
|
pagerDutyDescriptionDefaultVaule,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_severity',
|
||||||
|
testId: 'pager-severity-textbox',
|
||||||
|
helpText: 'help_pager_severity',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Severity contains the default template', () => {
|
||||||
|
const severityTextbox = screen.getByTestId('pager-severity-textbox');
|
||||||
|
|
||||||
|
expect(severityTextbox).toHaveValue(pagerDutySeverityTextDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Additional Information label, text area, and help text (help_pager_details) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_details',
|
||||||
|
testId: 'pager-additional-details-textarea',
|
||||||
|
helpText: 'help_pager_details',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Additional Information contains the default template', () => {
|
||||||
|
const detailsTextArea = screen.getByTestId(
|
||||||
|
'pager-additional-details-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detailsTextArea).toHaveValue(pagerDutyAdditionalDetailsDefaultValue);
|
||||||
|
});
|
||||||
|
it('Should check if Group label, text area, and info (help_pager_group) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_group',
|
||||||
|
testId: 'pager-group-textarea',
|
||||||
|
helpText: 'help_pager_group',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Class label, text area, and info (help_pager_class) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_class',
|
||||||
|
testId: 'pager-class-textarea',
|
||||||
|
helpText: 'help_pager_class',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client label, text area, and info (Shows up as event source in Pagerduty) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_client',
|
||||||
|
testId: 'pager-client-textarea',
|
||||||
|
helpText: 'help_pager_client',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client input contains the default value "SigNoz Alert Manager"', () => {
|
||||||
|
const clientTextArea = screen.getByTestId('pager-client-textarea');
|
||||||
|
|
||||||
|
expect(clientTextArea).toHaveValue('SigNoz Alert Manager');
|
||||||
|
});
|
||||||
|
it('Should check if Client URL label, text area, and info (Shows up as event source link in Pagerduty) are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_pager_client_url',
|
||||||
|
testId: 'pager-client-url-textarea',
|
||||||
|
helpText: 'help_pager_client_url',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Client URL contains the default value "https://enter-signoz-host-n-port-here/alerts"', () => {
|
||||||
|
const clientUrlTextArea = screen.getByTestId('pager-client-url-textarea');
|
||||||
|
|
||||||
|
expect(clientUrlTextArea).toHaveValue(
|
||||||
|
'https://enter-signoz-host-n-port-here/alerts',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Opsgenie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Opsgenie"', () => {
|
||||||
|
expect(screen.getByText('Opsgenie')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if API key label, required, and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_api_key',
|
||||||
|
testId: 'opsgenie-api-key-textbox',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message label, required, info (Shows up as message in opsgenie), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_message',
|
||||||
|
testId: 'opsgenie-message-textarea',
|
||||||
|
helpText: 'help_opsgenie_message',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message contains the default template ', () => {
|
||||||
|
const messageTextArea = screen.getByTestId('opsgenie-message-textarea');
|
||||||
|
|
||||||
|
expect(messageTextArea).toHaveValue(opsGenieMessageDefaultValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_description',
|
||||||
|
testId: 'opsgenie-description-textarea',
|
||||||
|
helpText: 'help_opsgenie_description',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description label, required, info (Shows up as description in opsgenie), and text area are displayed properly `{{ if gt (len .Alerts.Firing) 0 -}}', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId(
|
||||||
|
'opsgenie-description-textarea',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(
|
||||||
|
opsGenieDescriptionDefaultValue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Priority label, required, info (help_opsgenie_priority), and text area are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_opsgenie_priority',
|
||||||
|
testId: 'opsgenie-priority-textarea',
|
||||||
|
helpText: 'help_opsgenie_priority',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Message contains the default template', () => {
|
||||||
|
const priorityTextArea = screen.getByTestId('opsgenie-priority-textarea');
|
||||||
|
|
||||||
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Opsgenie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Email"', () => {
|
||||||
|
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if API key label, required, info(help_email_to), and textbox are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_email_to',
|
||||||
|
testId: 'email-to-textbox',
|
||||||
|
helpText: 'help_email_to',
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Microsoft Teams', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Microsoft Teams (Supported in Paid Plans Only)"', () => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the upgrade plan message is shown', () => {
|
||||||
|
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/This feature is available for paid plans only./),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
const link = screen.getByRole('link', { name: 'Click here' });
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
expect(link).toHaveAttribute('href', SIGNOZ_UPGRADE_PLAN_URL);
|
||||||
|
expect(screen.getByText(/to Upgrade/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'button_test_channel' }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'button_return' }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Should check if save and test buttons are disabled', () => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||||
|
).toBeDisabled();
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'button_test_channel' }),
|
||||||
|
).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import EditAlertChannels from 'container/EditAlertChannels';
|
||||||
|
import {
|
||||||
|
editAlertChannelInitialValue,
|
||||||
|
editSlackDescriptionDefaultValue,
|
||||||
|
slackTitleDefaultValue,
|
||||||
|
} from 'mocks-server/__mockdata__/alerts';
|
||||||
|
import { render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import { testLabelInputAndHelpValue } from './testUtils';
|
||||||
|
|
||||||
|
const successNotification = jest.fn();
|
||||||
|
const errorNotification = jest.fn();
|
||||||
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useNotifications: jest.fn(() => ({
|
||||||
|
notifications: {
|
||||||
|
success: successNotification,
|
||||||
|
error: errorNotification,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useFeatureFlag', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
active: true,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Should check if the edit alert channel is properly displayed ', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
it('Should check if the title is "Edit Notification Channels"', () => {
|
||||||
|
expect(screen.getByText('page_title_edit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the name label and textbox are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_name',
|
||||||
|
testId: 'channel-name-textbox',
|
||||||
|
value: 'Dummy-Channel',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should check if Send resolved alerts label and checkbox are displayed properly and the checkbox is checked ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_send_resolved',
|
||||||
|
testId: 'field-send-resolved-checkbox',
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('field-send-resolved-checkbox')).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if channel type label and dropdown are displayed properly', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_channel_type',
|
||||||
|
testId: 'channel-type-select',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the selected item in the type dropdown has text "Slack"', () => {
|
||||||
|
expect(screen.getByText('Slack')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_webhook_url',
|
||||||
|
testId: 'webhook-url-textbox',
|
||||||
|
value:
|
||||||
|
'https://discord.com/api/webhooks/dummy_webhook_id/dummy_webhook_token/slack',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Recepient label, input, and help text are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_recipient',
|
||||||
|
testId: 'slack-channel-textbox',
|
||||||
|
helpText: 'slack_channel_help',
|
||||||
|
value: '#dummy_channel',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_title',
|
||||||
|
testId: 'title-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Title contains template', () => {
|
||||||
|
const titleTextArea = screen.getByTestId('title-textarea');
|
||||||
|
|
||||||
|
expect(titleTextArea).toHaveTextContent(slackTitleDefaultValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description label and text area are displayed properly ', () => {
|
||||||
|
testLabelInputAndHelpValue({
|
||||||
|
labelText: 'field_slack_description',
|
||||||
|
testId: 'description-textarea',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if Description contains template', () => {
|
||||||
|
const descriptionTextArea = screen.getByTestId('description-textarea');
|
||||||
|
|
||||||
|
expect(descriptionTextArea).toHaveTextContent(
|
||||||
|
editSlackDescriptionDefaultValue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||||
|
expect(screen.getByText('button_save_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_test_channel')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('button_return')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { screen } from 'tests/test-utils';
|
||||||
|
|
||||||
|
export const testLabelInputAndHelpValue = ({
|
||||||
|
labelText,
|
||||||
|
testId,
|
||||||
|
helpText,
|
||||||
|
required = false,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
labelText: string;
|
||||||
|
testId: string;
|
||||||
|
helpText?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string;
|
||||||
|
}): void => {
|
||||||
|
const label = screen.getByText(labelText);
|
||||||
|
expect(label).toBeInTheDocument();
|
||||||
|
|
||||||
|
const input = screen.getByTestId(testId);
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (helpText !== undefined) {
|
||||||
|
expect(screen.getByText(helpText)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
if (required) {
|
||||||
|
expect(input).toBeRequired();
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
expect(input).toHaveValue(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import getAll from 'api/channels/getAll';
|
import getAll from 'api/channels/getAll';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback } from 'react';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -31,6 +33,14 @@ function AlertChannels(): JSX.Element {
|
|||||||
|
|
||||||
const { loading, payload, error, errorMessage } = useFetch(getAll);
|
const { loading, payload, error, errorMessage } = useFetch(getAll);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUndefined(payload)) {
|
||||||
|
logEvent('Alert Channel: Channel list page visited', {
|
||||||
|
number: payload?.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [payload]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Typography>{errorMessage}</Typography>;
|
return <Typography>{errorMessage}</Typography>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
|||||||
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import getAll from 'api/errors/getAll';
|
import getAll from 'api/errors/getAll';
|
||||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
@@ -23,7 +24,8 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -410,6 +412,26 @@ function AllErrors(): JSX.Element {
|
|||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!logEventCalledRef.current &&
|
||||||
|
!isUndefined(errorCountResponse.data?.payload)
|
||||||
|
) {
|
||||||
|
const selectedEnvironments = queries.find(
|
||||||
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
|
)?.tagValue;
|
||||||
|
|
||||||
|
logEvent('Exception: List page visited', {
|
||||||
|
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||||
|
selectedEnvironments,
|
||||||
|
resourceAttributeUsed: !!queries?.length,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [errorCountResponse.data?.payload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
overflow: auto;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
@@ -25,6 +24,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-support-gateway {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
.chat-support-gateway-btn {
|
||||||
|
max-width: 48px;
|
||||||
|
width: 48px;
|
||||||
|
max-height: 48px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: #f25733;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white !important;
|
||||||
|
border-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-support-gateway-btn-icon {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-credit-card-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-text {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
padding-right: 4px;
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-credit-card-modal {
|
||||||
|
.ant-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
background: var(--bg-slate-500, #161922);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-credit-card-btn {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.isDarkMode {
|
.isDarkMode {
|
||||||
.app-layout {
|
.app-layout {
|
||||||
.app-content {
|
.app-content {
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
import './AppLayout.styles.scss';
|
import './AppLayout.styles.scss';
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
import useLicense from 'hooks/useLicense';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@@ -27,7 +31,6 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
@@ -38,7 +41,6 @@ import { sideBarCollapse } from 'store/actions';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import {
|
import {
|
||||||
UPDATE_CONFIGS,
|
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
@@ -50,6 +52,7 @@ import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
|||||||
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
||||||
import { getRouteKey } from './utils';
|
import { getRouteKey } from './utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function AppLayout(props: AppLayoutProps): JSX.Element {
|
function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||||
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
@@ -59,18 +62,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const { data: licenseData, isFetching } = useLicense();
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
|
|
||||||
|
const isPremiumChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
|
|
||||||
|
const showAddCreditCardModal =
|
||||||
|
!isPremiumChatSupportEnabled &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { t } = useTranslation(['titles']);
|
const { t } = useTranslation(['titles']);
|
||||||
|
|
||||||
const [
|
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||||
getUserVersionResponse,
|
|
||||||
getUserLatestVersionResponse,
|
|
||||||
getDynamicConfigsResponse,
|
|
||||||
] = useQueries([
|
|
||||||
{
|
{
|
||||||
queryFn: getUserVersion,
|
queryFn: getUserVersion,
|
||||||
queryKey: ['getUserVersion', user?.accessJwt],
|
queryKey: ['getUserVersion', user?.accessJwt],
|
||||||
@@ -81,10 +89,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
||||||
enabled: isLoggedIn,
|
enabled: isLoggedIn,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
queryFn: getDynamicConfigs,
|
|
||||||
queryKey: ['getDynamicConfigs', user?.accessJwt],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,15 +99,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
||||||
getUserVersionResponse.refetch();
|
getUserVersionResponse.refetch();
|
||||||
}
|
}
|
||||||
if (getDynamicConfigsResponse.status === 'idle') {
|
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
||||||
getDynamicConfigsResponse.refetch();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getUserLatestVersionResponse,
|
|
||||||
getUserVersionResponse,
|
|
||||||
isLoggedIn,
|
|
||||||
getDynamicConfigsResponse,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
@@ -111,9 +107,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const latestCurrentCounter = useRef(0);
|
const latestCurrentCounter = useRef(0);
|
||||||
const latestVersionCounter = useRef(0);
|
const latestVersionCounter = useRef(0);
|
||||||
const latestConfigCounter = useRef(0);
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const onCollapse = useCallback(() => {
|
const onCollapse = useCallback(() => {
|
||||||
setCollapsed((collapsed) => !collapsed);
|
setCollapsed((collapsed) => !collapsed);
|
||||||
@@ -189,23 +182,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
getDynamicConfigsResponse.isFetched &&
|
|
||||||
getDynamicConfigsResponse.isSuccess &&
|
|
||||||
getDynamicConfigsResponse.data &&
|
|
||||||
getDynamicConfigsResponse.data.payload &&
|
|
||||||
latestConfigCounter.current === 0
|
|
||||||
) {
|
|
||||||
latestConfigCounter.current = 1;
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_CONFIGS,
|
|
||||||
payload: {
|
|
||||||
configs: getDynamicConfigsResponse.data.payload,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
dispatch,
|
dispatch,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -220,9 +196,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserLatestVersionResponse.isFetched,
|
getUserLatestVersionResponse.isFetched,
|
||||||
getUserVersionResponse.isFetched,
|
getUserVersionResponse.isFetched,
|
||||||
getUserLatestVersionResponse.isSuccess,
|
getUserLatestVersionResponse.isSuccess,
|
||||||
getDynamicConfigsResponse.data,
|
|
||||||
getDynamicConfigsResponse.isFetched,
|
|
||||||
getDynamicConfigsResponse.isSuccess,
|
|
||||||
notifications,
|
notifications,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -236,7 +209,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||||
pathname === ROUTES.GET_STARTED_AWS_MONITORING;
|
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
|
||||||
|
pathname === ROUTES.GET_STARTED_AZURE_MONITORING;
|
||||||
|
|
||||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||||
|
|
||||||
@@ -341,9 +315,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
|
<div
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
className={cx('app-content', collapsed ? 'collapsed' : '')}
|
||||||
<LayoutContent>
|
data-overlayscrollbars-initialize
|
||||||
|
>
|
||||||
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
|
<LayoutContent data-overlayscrollbars-initialize>
|
||||||
|
<OverlayScrollbar>
|
||||||
<ChildrenContainer
|
<ChildrenContainer
|
||||||
style={{
|
style={{
|
||||||
margin:
|
margin:
|
||||||
@@ -359,10 +337,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
{children}
|
{children}
|
||||||
</ChildrenContainer>
|
</ChildrenContainer>
|
||||||
|
</OverlayScrollbar>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{showAddCreditCardModal && <ChatSupportGateway />}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const Layout = styled(LayoutComponent)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0.1rem;
|
width: 0.1rem;
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
|
|||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
import useLicense from 'hooks/useLicense';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@@ -137,8 +137,6 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
Partial<UsageResponsePayloadProps>
|
Partial<UsageResponsePayloadProps>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
||||||
|
|
||||||
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
@@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
const handleBilling = useCallback(async () => {
|
const handleBilling = useCallback(async () => {
|
||||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||||
trackEvent('Billing : Upgrade Plan', {
|
logEvent('Billing : Upgrade Plan', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
});
|
});
|
||||||
@@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
cancelURL: window.location.href,
|
cancelURL: window.location.href,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
trackEvent('Billing : Manage Billing', {
|
logEvent('Billing : Manage Billing', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import testOpsGenie from 'api/channels/testOpsgenie';
|
|||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
import testSlackApi from 'api/channels/testSlack';
|
import testSlackApi from 'api/channels/testSlack';
|
||||||
import testWebhookApi from 'api/channels/testWebhook';
|
import testWebhookApi from 'api/channels/testWebhook';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import FormAlertChannels from 'container/FormAlertChannels';
|
import FormAlertChannels from 'container/FormAlertChannels';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +44,10 @@ function CreateAlertChannels({
|
|||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Alert Channel: Create channel page visited', {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [selectedConfig, setSelectedConfig] = useState<
|
const [selectedConfig, setSelectedConfig] = useState<
|
||||||
Partial<
|
Partial<
|
||||||
SlackChannel &
|
SlackChannel &
|
||||||
@@ -139,19 +144,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareSlackRequest, t, notifications]);
|
}, [prepareSlackRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareWebhookRequest = useCallback(() => {
|
const prepareWebhookRequest = useCallback(() => {
|
||||||
@@ -200,19 +211,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareWebhookRequest, t, notifications]);
|
}, [prepareWebhookRequest, t, notifications]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(() => {
|
const preparePagerRequest = useCallback(() => {
|
||||||
@@ -245,8 +262,8 @@ function CreateAlertChannels({
|
|||||||
setSavingState(true);
|
setSavingState(true);
|
||||||
const request = preparePagerRequest();
|
const request = preparePagerRequest();
|
||||||
|
|
||||||
if (request) {
|
|
||||||
try {
|
try {
|
||||||
|
if (request) {
|
||||||
const response = await createPagerApi(request);
|
const response = await createPagerApi(request);
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
@@ -255,20 +272,31 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
}
|
} catch (error) {
|
||||||
|
notifications.error({
|
||||||
|
message: 'Error',
|
||||||
|
description: t('channel_creation_failed'),
|
||||||
|
});
|
||||||
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [t, notifications, preparePagerRequest]);
|
}, [t, notifications, preparePagerRequest]);
|
||||||
|
|
||||||
const prepareOpsgenieRequest = useCallback(
|
const prepareOpsgenieRequest = useCallback(
|
||||||
@@ -295,19 +323,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareOpsgenieRequest, t, notifications]);
|
}, [prepareOpsgenieRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
@@ -332,19 +366,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareEmailRequest, t, notifications]);
|
}, [prepareEmailRequest, t, notifications]);
|
||||||
|
|
||||||
const prepareMsTeamsRequest = useCallback(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
@@ -370,19 +410,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_creation_done'),
|
description: t('channel_creation_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_creation_failed'),
|
description: response.error || t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_creation_failed'),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_creation_failed'),
|
description: t('channel_creation_failed'),
|
||||||
});
|
});
|
||||||
}
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
}
|
||||||
}, [prepareMsTeamsRequest, t, notifications]);
|
}, [prepareMsTeamsRequest, t, notifications]);
|
||||||
|
|
||||||
const onSaveHandler = useCallback(
|
const onSaveHandler = useCallback(
|
||||||
@@ -400,7 +446,15 @@ function CreateAlertChannels({
|
|||||||
const functionToCall = functionMapper[value as keyof typeof functionMapper];
|
const functionToCall = functionMapper[value as keyof typeof functionMapper];
|
||||||
|
|
||||||
if (functionToCall) {
|
if (functionToCall) {
|
||||||
functionToCall();
|
const result = await functionToCall();
|
||||||
|
logEvent('Alert Channel: Save channel', {
|
||||||
|
type: value,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'true',
|
||||||
|
status: result?.status,
|
||||||
|
statusMessage: result?.statusMessage,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@@ -409,6 +463,7 @@ function CreateAlertChannels({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
onSlackHandler,
|
onSlackHandler,
|
||||||
onWebhookHandler,
|
onWebhookHandler,
|
||||||
@@ -472,14 +527,25 @@ function CreateAlertChannels({
|
|||||||
description: t('channel_test_failed'),
|
description: t('channel_test_failed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logEvent('Alert Channel: Test notification', {
|
||||||
|
type: channelType,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'true',
|
||||||
|
status:
|
||||||
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('channel_test_unexpected'),
|
description: t('channel_test_unexpected'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTestingState(false);
|
setTestingState(false);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
prepareWebhookRequest,
|
prepareWebhookRequest,
|
||||||
t,
|
t,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Row, Typography } from 'antd';
|
import { Row, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -34,6 +36,13 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logEvent('Alert: Sample alert link clicked', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[option],
|
||||||
|
link: url,
|
||||||
|
page: 'New alert data source selection page',
|
||||||
|
});
|
||||||
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
const renderOptions = useMemo(
|
const renderOptions = useMemo(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
@@ -68,6 +69,8 @@ function CreateRules(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (alertType) {
|
if (alertType) {
|
||||||
onSelectType(alertType);
|
onSelectType(alertType);
|
||||||
|
} else {
|
||||||
|
logEvent('Alert: New alert data source selection page visited', {});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [alertType]);
|
}, [alertType]);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import testOpsgenie from 'api/channels/testOpsgenie';
|
|||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
import testSlackApi from 'api/channels/testSlack';
|
import testSlackApi from 'api/channels/testSlack';
|
||||||
import testWebhookApi from 'api/channels/testWebhook';
|
import testWebhookApi from 'api/channels/testWebhook';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
ChannelType,
|
ChannelType,
|
||||||
@@ -89,7 +90,7 @@ function EditAlertChannels({
|
|||||||
description: t('webhook_url_required'),
|
description: t('webhook_url_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editSlackApi(prepareSlackRequest());
|
const response = await editSlackApi(prepareSlackRequest());
|
||||||
@@ -101,13 +102,17 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareSlackRequest, t, notifications, selectedConfig]);
|
}, [prepareSlackRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareWebhookRequest = useCallback(() => {
|
const prepareWebhookRequest = useCallback(() => {
|
||||||
@@ -136,13 +141,13 @@ function EditAlertChannels({
|
|||||||
if (selectedConfig?.api_url === '') {
|
if (selectedConfig?.api_url === '') {
|
||||||
showError(t('webhook_url_required'));
|
showError(t('webhook_url_required'));
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username && (!password || password === '')) {
|
if (username && (!password || password === '')) {
|
||||||
showError(t('username_no_password'));
|
showError(t('username_no_password'));
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('username_no_password') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editWebhookApi(prepareWebhookRequest());
|
const response = await editWebhookApi(prepareWebhookRequest());
|
||||||
@@ -154,10 +159,15 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
showError(response.error || t('channel_edit_failed'));
|
|
||||||
}
|
}
|
||||||
|
showError(response.error || t('channel_edit_failed'));
|
||||||
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
@@ -181,13 +191,18 @@ function EditAlertChannels({
|
|||||||
description: t('channel_edit_done'),
|
description: t('channel_edit_done'),
|
||||||
});
|
});
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareEmailRequest, t, notifications]);
|
}, [prepareEmailRequest, t, notifications]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(
|
const preparePagerRequest = useCallback(
|
||||||
@@ -218,7 +233,7 @@ function EditAlertChannels({
|
|||||||
description: validationError,
|
description: validationError,
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: validationError };
|
||||||
}
|
}
|
||||||
const response = await editPagerApi(preparePagerRequest());
|
const response = await editPagerApi(preparePagerRequest());
|
||||||
|
|
||||||
@@ -229,13 +244,18 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [preparePagerRequest, notifications, selectedConfig, t]);
|
}, [preparePagerRequest, notifications, selectedConfig, t]);
|
||||||
|
|
||||||
const prepareOpsgenieRequest = useCallback(
|
const prepareOpsgenieRequest = useCallback(
|
||||||
@@ -259,7 +279,7 @@ function EditAlertChannels({
|
|||||||
description: t('api_key_required'),
|
description: t('api_key_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('api_key_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editOpsgenie(prepareOpsgenieRequest());
|
const response = await editOpsgenie(prepareOpsgenieRequest());
|
||||||
@@ -271,13 +291,18 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
|
}, [prepareOpsgenieRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const prepareMsTeamsRequest = useCallback(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
@@ -301,7 +326,7 @@ function EditAlertChannels({
|
|||||||
description: t('webhook_url_required'),
|
description: t('webhook_url_required'),
|
||||||
});
|
});
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
return;
|
return { status: 'failed', statusMessage: t('webhook_url_required') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await editMsTeamsApi(prepareMsTeamsRequest());
|
const response = await editMsTeamsApi(prepareMsTeamsRequest());
|
||||||
@@ -313,31 +338,46 @@ function EditAlertChannels({
|
|||||||
});
|
});
|
||||||
|
|
||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
} else {
|
return { status: 'success', statusMessage: t('channel_edit_done') };
|
||||||
|
}
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: response.error || t('channel_edit_failed'),
|
description: response.error || t('channel_edit_failed'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
|
return {
|
||||||
|
status: 'failed',
|
||||||
|
statusMessage: response.error || t('channel_edit_failed'),
|
||||||
|
};
|
||||||
}, [prepareMsTeamsRequest, t, notifications, selectedConfig]);
|
}, [prepareMsTeamsRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const onSaveHandler = useCallback(
|
const onSaveHandler = useCallback(
|
||||||
(value: ChannelType) => {
|
async (value: ChannelType) => {
|
||||||
|
let result;
|
||||||
if (value === ChannelType.Slack) {
|
if (value === ChannelType.Slack) {
|
||||||
onSlackEditHandler();
|
result = await onSlackEditHandler();
|
||||||
} else if (value === ChannelType.Webhook) {
|
} else if (value === ChannelType.Webhook) {
|
||||||
onWebhookEditHandler();
|
result = await onWebhookEditHandler();
|
||||||
} else if (value === ChannelType.Pagerduty) {
|
} else if (value === ChannelType.Pagerduty) {
|
||||||
onPagerEditHandler();
|
result = await onPagerEditHandler();
|
||||||
} else if (value === ChannelType.MsTeams) {
|
} else if (value === ChannelType.MsTeams) {
|
||||||
onMsTeamsEditHandler();
|
result = await onMsTeamsEditHandler();
|
||||||
} else if (value === ChannelType.Opsgenie) {
|
} else if (value === ChannelType.Opsgenie) {
|
||||||
onOpsgenieEditHandler();
|
result = await onOpsgenieEditHandler();
|
||||||
} else if (value === ChannelType.Email) {
|
} else if (value === ChannelType.Email) {
|
||||||
onEmailEditHandler();
|
result = await onEmailEditHandler();
|
||||||
}
|
}
|
||||||
|
logEvent('Alert Channel: Save channel', {
|
||||||
|
type: value,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'false',
|
||||||
|
status: result?.status,
|
||||||
|
statusMessage: result?.statusMessage,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
onSlackEditHandler,
|
onSlackEditHandler,
|
||||||
onWebhookEditHandler,
|
onWebhookEditHandler,
|
||||||
@@ -399,6 +439,14 @@ function EditAlertChannels({
|
|||||||
description: t('channel_test_failed'),
|
description: t('channel_test_failed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
logEvent('Alert Channel: Test notification', {
|
||||||
|
type: channelType,
|
||||||
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
name: selectedConfig?.name,
|
||||||
|
new: 'false',
|
||||||
|
status:
|
||||||
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@@ -407,6 +455,7 @@ function EditAlertChannels({
|
|||||||
}
|
}
|
||||||
setTestingState(false);
|
setTestingState(false);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[
|
[
|
||||||
t,
|
t,
|
||||||
prepareWebhookRequest,
|
prepareWebhookRequest,
|
||||||
|
|||||||
@@ -1,8 +1,34 @@
|
|||||||
import './EmptyLogsSearch.styles.scss';
|
import './EmptyLogsSearch.styles.scss';
|
||||||
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export default function EmptyLogsSearch({
|
||||||
|
dataSource,
|
||||||
|
panelType,
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
panelType: PanelTypeKeys;
|
||||||
|
}): JSX.Element {
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current) {
|
||||||
|
if (dataSource === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: No results', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (dataSource === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: No results', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
export default function EmptyLogsSearch(): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="empty-logs-search-container">
|
<div className="empty-logs-search-container">
|
||||||
<div className="empty-logs-search-container-content">
|
<div className="empty-logs-search-container-content">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
import { Button, Divider, Space, Typography } from 'antd';
|
import { Button, Divider, Space, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
@@ -9,8 +10,9 @@ import dayjs from 'dayjs';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { isUndefined } from 'lodash-es';
|
||||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -111,9 +113,29 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const onClickTraceHandler = (): void => {
|
const onClickTraceHandler = (): void => {
|
||||||
|
logEvent('Exception: Navigate to trace detail page', {
|
||||||
|
groupId: errorDetail?.groupID,
|
||||||
|
spanId: errorDetail.spanID,
|
||||||
|
traceId: errorDetail.traceID,
|
||||||
|
exceptionId: errorDetail?.errorId,
|
||||||
|
});
|
||||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logEventCalledRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||||
|
logEvent('Exception: Detail page visited', {
|
||||||
|
groupId: errorDetail?.groupID,
|
||||||
|
spanId: errorDetail.spanID,
|
||||||
|
traceId: errorDetail.traceID,
|
||||||
|
exceptionId: errorDetail?.errorId,
|
||||||
|
});
|
||||||
|
logEventCalledRef.current = true;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography>{errorDetail.exceptionType}</Typography>
|
<Typography>{errorDetail.exceptionType}</Typography>
|
||||||
|
|||||||
@@ -91,8 +91,7 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
||||||
&.ant-btn-round {
|
&.ant-btn-round {
|
||||||
padding-inline-start: 10px;
|
padding: 8px 12px 8px 10px;
|
||||||
padding-inline-end: 8px;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
@@ -93,7 +94,23 @@ function ExplorerOptions({
|
|||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
panelType,
|
||||||
|
isStagedQueryUpdated,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const handleSaveViewModalToggle = (): void => {
|
const handleSaveViewModalToggle = (): void => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Save view clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Save view clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsSaveModalOpen(!isSaveModalOpen);
|
setIsSaveModalOpen(!isSaveModalOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,11 +121,21 @@ function ExplorerOptions({
|
|||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Create alert', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Create alert', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
JSON.stringify(query),
|
JSON.stringify(query),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [history, query]);
|
}, [history, query]);
|
||||||
|
|
||||||
const onCancel = (value: boolean) => (): void => {
|
const onCancel = (value: boolean) => (): void => {
|
||||||
@@ -116,6 +143,15 @@ function ExplorerOptions({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAddToDashboard = (): void => {
|
const onAddToDashboard = (): void => {
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Add to dashboard clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Add to dashboard clicked', {
|
||||||
|
panelType,
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsExport(true);
|
setIsExport(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,13 +163,6 @@ function ExplorerOptions({
|
|||||||
refetch: refetchAllView,
|
refetch: refetchAllView,
|
||||||
} = useGetAllViews(sourcepage);
|
} = useGetAllViews(sourcepage);
|
||||||
|
|
||||||
const {
|
|
||||||
currentQuery,
|
|
||||||
panelType,
|
|
||||||
isStagedQueryUpdated,
|
|
||||||
redirectWithQueryBuilderData,
|
|
||||||
} = useQueryBuilder();
|
|
||||||
|
|
||||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||||
|
|
||||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||||
@@ -224,6 +253,17 @@ function ExplorerOptions({
|
|||||||
onMenuItemSelectHandler({
|
onMenuItemSelectHandler({
|
||||||
key: option.key,
|
key: option.key,
|
||||||
});
|
});
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Select view', {
|
||||||
|
panelType,
|
||||||
|
viewName: option?.value,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Select view', {
|
||||||
|
panelType,
|
||||||
|
viewName: option?.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.blur();
|
ref.current.blur();
|
||||||
}
|
}
|
||||||
@@ -259,6 +299,17 @@ function ExplorerOptions({
|
|||||||
viewName: newViewName,
|
viewName: newViewName,
|
||||||
setNewViewName,
|
setNewViewName,
|
||||||
});
|
});
|
||||||
|
if (sourcepage === DataSource.TRACES) {
|
||||||
|
logEvent('Traces Explorer: Save view successful', {
|
||||||
|
panelType,
|
||||||
|
viewName: newViewName,
|
||||||
|
});
|
||||||
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
|
logEvent('Logs Explorer: Save view successful', {
|
||||||
|
panelType,
|
||||||
|
viewName: newViewName,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Remove this and move this to scss file
|
// TODO: Remove this and move this to scss file
|
||||||
@@ -499,7 +550,7 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
export interface ExplorerOptionsProps {
|
export interface ExplorerOptionsProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null) => void;
|
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
||||||
query: Query | null;
|
query: Query | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
|
|||||||