Compare commits
84 Commits
v0.42.0-qu
...
v0.46.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52c8584e63 | ||
|
|
9a908c3f76 | ||
|
|
4887a1d8dd | ||
|
|
f2b0387a1b | ||
|
|
cbb9fd51f8 | ||
|
|
10e44ce440 | ||
|
|
6827d66ae9 | ||
|
|
611ec3e08d | ||
|
|
4ab350e721 | ||
|
|
631c12259f | ||
|
|
de497bf5b6 | ||
|
|
12be6ce020 | ||
|
|
2dbe598b2c | ||
|
|
cf64da2631 | ||
|
|
9ff0e34038 | ||
|
|
d313f44556 | ||
|
|
5a778dcb18 | ||
|
|
7e31b4ca01 | ||
|
|
3efd9801a1 | ||
|
|
0cbaa17d9f | ||
|
|
30bfad527f | ||
|
|
9f1c45bc32 | ||
|
|
51becf7cfb | ||
|
|
7460e650af | ||
|
|
a544723bb8 | ||
|
|
eb6f038db5 | ||
|
|
47dcd994f0 | ||
|
|
211fe4fdd5 | ||
|
|
e2992b42c1 | ||
|
|
3957d91a9b | ||
|
|
967aa16f21 | ||
|
|
08b1a87cb5 | ||
|
|
03ddcdd20e | ||
|
|
1aec7f3ca6 | ||
|
|
241edcb88a | ||
|
|
27d12871af | ||
|
|
e78e1d4b63 | ||
|
|
64bf580323 | ||
|
|
152aa4b518 | ||
|
|
b3d5831574 | ||
|
|
b85b9f42ed | ||
|
|
5c1c09c790 | ||
|
|
33960b05fd | ||
|
|
191d9b0648 | ||
|
|
7d81bc3417 | ||
|
|
506916661d | ||
|
|
5326f2d23b | ||
|
|
dfaa344dce | ||
|
|
882b540a0b | ||
|
|
1c4b579c3d | ||
|
|
706f25cc5d | ||
|
|
e6e0a59f5f | ||
|
|
b2c170c752 | ||
|
|
ee421af95c | ||
|
|
453be9074d | ||
|
|
3272444e13 | ||
|
|
71b3e6d522 | ||
|
|
6cf7cc9f4f | ||
|
|
5ec2f17d09 | ||
|
|
a45fb8ec0c | ||
|
|
bd148bbd5a | ||
|
|
1306e99ca8 | ||
|
|
1a8f063b4b | ||
|
|
c7668b9a78 | ||
|
|
5e3dba2587 | ||
|
|
374f30e0cd | ||
|
|
38d2833931 | ||
|
|
731eacbbca | ||
|
|
a63bb139bf | ||
|
|
a140bef0e6 | ||
|
|
48e5436167 | ||
|
|
0fc664a387 | ||
|
|
5817d50652 | ||
|
|
bb318cf52a | ||
|
|
ec0185da61 | ||
|
|
fc2bdb610f | ||
|
|
a9464de62d | ||
|
|
57bfdedfe1 | ||
|
|
7bdc9c0cb0 | ||
|
|
0d5934d56b | ||
|
|
3a5a61aff9 | ||
|
|
a54b7baa7d | ||
|
|
cd63dd972d | ||
|
|
389058b9b4 |
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Jest Coverage - changed files
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: "refs/heads/develop"
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
|
||||||
|
|
||||||
|
- name: Fetch branch
|
||||||
|
run: git fetch origin ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
git checkout ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm install -g yarn && yarn
|
||||||
|
|
||||||
|
- name: npm run test:changedsince
|
||||||
|
run: cd frontend && npm run i18n:generate-hash && npm run test:changedsince
|
||||||
70
.github/workflows/staging-deployment.yaml
vendored
70
.github/workflows/staging-deployment.yaml
vendored
@@ -9,34 +9,46 @@ jobs:
|
|||||||
name: Deploy latest develop branch to staging
|
name: Deploy latest develop branch to staging
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: staging
|
environment: staging
|
||||||
|
permissions:
|
||||||
|
contents: 'read'
|
||||||
|
id-token: 'write'
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- id: 'auth'
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: 'google-github-actions/auth@v2'
|
||||||
env:
|
|
||||||
GITHUB_BRANCH: develop
|
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.HOST_DNS }}
|
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||||
username: ${{ secrets.USERNAME }}
|
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
- name: 'sdk'
|
||||||
command_timeout: 60m
|
uses: 'google-github-actions/setup-gcloud@v2'
|
||||||
script: |
|
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
- name: 'ssh'
|
||||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
shell: bash
|
||||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
env:
|
||||||
export OTELCOL_TAG="main"
|
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
docker system prune --force
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
docker pull signoz/signoz-otel-collector:main
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
docker pull signoz/signoz-schema-migrator:main
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
cd ~/signoz
|
run: |
|
||||||
git status
|
read -r -d '' COMMAND <<EOF || true
|
||||||
git add .
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||||
git fetch origin
|
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||||
git checkout ${GITHUB_BRANCH}
|
export OTELCOL_TAG="main"
|
||||||
git pull
|
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||||
make build-ee-query-service-amd64
|
docker system prune --force
|
||||||
make build-frontend-amd64
|
docker pull signoz/signoz-otel-collector:main
|
||||||
make run-signoz
|
docker pull signoz/signoz-schema-migrator:main
|
||||||
|
cd ~/signoz
|
||||||
|
git status
|
||||||
|
git add .
|
||||||
|
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||||
|
git fetch origin
|
||||||
|
git checkout ${GITHUB_BRANCH}
|
||||||
|
git pull
|
||||||
|
make build-ee-query-service-amd64
|
||||||
|
make build-frontend-amd64
|
||||||
|
make run-signoz
|
||||||
|
EOF
|
||||||
|
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
|||||||
68
.github/workflows/testing-deployment.yaml
vendored
68
.github/workflows/testing-deployment.yaml
vendored
@@ -9,35 +9,47 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: testing
|
environment: testing
|
||||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||||
|
permissions:
|
||||||
|
contents: 'read'
|
||||||
|
id-token: 'write'
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- id: 'auth'
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: 'google-github-actions/auth@v2'
|
||||||
|
with:
|
||||||
|
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||||
|
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||||
|
|
||||||
|
- name: 'sdk'
|
||||||
|
uses: 'google-github-actions/setup-gcloud@v2'
|
||||||
|
|
||||||
|
- name: 'ssh'
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
with:
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
host: ${{ secrets.HOST_DNS }}
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
username: ${{ secrets.USERNAME }}
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
run: |
|
||||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
read -r -d '' COMMAND <<EOF || true
|
||||||
command_timeout: 60m
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
script: |
|
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
export DEV_BUILD="1"
|
||||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||||
export DEV_BUILD="1"
|
docker system prune --force
|
||||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
cd ~/signoz
|
||||||
docker system prune --force
|
git status
|
||||||
cd ~/signoz
|
git add .
|
||||||
git status
|
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||||
git add .
|
git fetch origin
|
||||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
git checkout develop
|
||||||
git fetch origin
|
git pull
|
||||||
git checkout develop
|
# This is added to include the scenerio when new commit in PR is force-pushed
|
||||||
git pull
|
git branch -D ${GITHUB_BRANCH}
|
||||||
# This is added to include the scenerio when new commit in PR is force-pushed
|
git checkout --track origin/${GITHUB_BRANCH}
|
||||||
git branch -D ${GITHUB_BRANCH}
|
make build-ee-query-service-amd64
|
||||||
git checkout --track origin/${GITHUB_BRANCH}
|
make build-frontend-amd64
|
||||||
make build-ee-query-service-amd64
|
make run-signoz
|
||||||
make build-frontend-amd64
|
EOF
|
||||||
make run-signoz
|
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
|||||||
8
.versions-golang
Normal file
8
.versions-golang
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#### Auto generated by make versions/golang. DO NOT EDIT! ####
|
||||||
|
amd64=128d7baad667abc0e41a85673026a2cf9449ef40f384baf424aee45bc13f9235
|
||||||
|
arm=a5f77dc34ccae0d43269675508aab8fa9078ded6fa3e2dcee54f7c230018100d
|
||||||
|
arm64=1cdad16d01542a57caca4b0a6893a5b69d711d69dd6bb4483c77c1d092baec41
|
||||||
|
386=0c82e5195d14caa5daa01ea06a70139e7ea1edbd366c83259227c7d9965d4c5a
|
||||||
|
mips64le=25967f27f76031f31cd3ae2173958e151d8d961ca186ab4328af7a1895139a66
|
||||||
|
ppc64le=6fa49b4730622b79560a1fc2677b02a1ee7aac5b28490a2bda6134050108fb3a
|
||||||
|
s390x=4e2c0198c3db1c769e8e2e8a1e504dbb5e3eff0dad62f8f5c543b4823a89d81b
|
||||||
@@ -22,7 +22,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.42.0
|
image: signoz/query-service:0.46.0
|
||||||
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.42.0
|
image: signoz/frontend:0.46.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.17
|
image: signoz/signoz-otel-collector:0.88.24
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.17
|
image: signoz/signoz-schema-migrator:0.88.24
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -111,18 +111,18 @@ processors:
|
|||||||
|
|
||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
enabled: true
|
enabled: true
|
||||||
clickhousemetricswrite/prometheus:
|
clickhousemetricswrite/prometheus:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
# logging: {}
|
# logging: {}
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
extensions:
|
extensions:
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ rule_files:
|
|||||||
scrape_configs: []
|
scrape_configs: []
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/signoz_metrics
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -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.17}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||||
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.17
|
image: signoz/signoz-otel-collector:0.88.24
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -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.42.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.46.0}
|
||||||
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.42.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.46.0}
|
||||||
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.17}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.24}
|
||||||
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.17}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.24}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
64
deploy/docker/clickhouse-setup/keeper_config.xml
Normal file
64
deploy/docker/clickhouse-setup/keeper_config.xml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<logger>
|
||||||
|
<!-- Possible levels [1]:
|
||||||
|
|
||||||
|
- none (turns off logging)
|
||||||
|
- fatal
|
||||||
|
- critical
|
||||||
|
- error
|
||||||
|
- warning
|
||||||
|
- notice
|
||||||
|
- information
|
||||||
|
- debug
|
||||||
|
- trace
|
||||||
|
|
||||||
|
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
|
||||||
|
-->
|
||||||
|
<level>information</level>
|
||||||
|
<log>/var/log/clickhouse-keeper/clickhouse-keeper.log</log>
|
||||||
|
<errorlog>/var/log/clickhouse-keeper/clickhouse-keeper.err.log</errorlog>
|
||||||
|
<!-- Rotation policy
|
||||||
|
See https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/FileChannel.h#L54-L85
|
||||||
|
-->
|
||||||
|
<size>1000M</size>
|
||||||
|
<count>10</count>
|
||||||
|
<!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<listen_host>0.0.0.0</listen_host>
|
||||||
|
<max_connections>4096</max_connections>
|
||||||
|
|
||||||
|
<keeper_server>
|
||||||
|
<tcp_port>9181</tcp_port>
|
||||||
|
|
||||||
|
<!-- Must be unique among all keeper serves -->
|
||||||
|
<server_id>1</server_id>
|
||||||
|
|
||||||
|
<log_storage_path>/var/lib/clickhouse/coordination/logs</log_storage_path>
|
||||||
|
<snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
|
||||||
|
|
||||||
|
<coordination_settings>
|
||||||
|
<operation_timeout_ms>10000</operation_timeout_ms>
|
||||||
|
<min_session_timeout_ms>10000</min_session_timeout_ms>
|
||||||
|
<session_timeout_ms>100000</session_timeout_ms>
|
||||||
|
<raft_logs_level>information</raft_logs_level>
|
||||||
|
<compress_logs>false</compress_logs>
|
||||||
|
<!-- All settings listed in https://github.com/ClickHouse/ClickHouse/blob/master/src/Coordination/CoordinationSettings.h -->
|
||||||
|
</coordination_settings>
|
||||||
|
|
||||||
|
<!-- enable sanity hostname checks for cluster configuration (e.g. if localhost is used with remote endpoints) -->
|
||||||
|
<hostname_checks_enabled>true</hostname_checks_enabled>
|
||||||
|
<raft_configuration>
|
||||||
|
<server>
|
||||||
|
<id>1</id>
|
||||||
|
|
||||||
|
<!-- Internal port and hostname -->
|
||||||
|
<hostname>clickhouses-keeper-1</hostname>
|
||||||
|
<port>9234</port>
|
||||||
|
</server>
|
||||||
|
|
||||||
|
<!-- Add more servers here -->
|
||||||
|
|
||||||
|
</raft_configuration>
|
||||||
|
</keeper_server>
|
||||||
|
</clickhouse>
|
||||||
@@ -122,21 +122,20 @@ extensions:
|
|||||||
|
|
||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
enabled: true
|
enabled: true
|
||||||
clickhousemetricswrite/prometheus:
|
clickhousemetricswrite/prometheus:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
# logging: {}
|
|
||||||
|
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
# logging: {}
|
||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ rule_files:
|
|||||||
scrape_configs: []
|
scrape_configs: []
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/signoz_metrics
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
|||||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
// PAT APIs
|
// PAT APIs
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/metrics"
|
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/parser"
|
|
||||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
|
||||||
querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
|
|
||||||
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
|
|
||||||
ah.APIHandler.QueryRangeMetricsV2(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
|
|
||||||
|
|
||||||
if apiErrorObj != nil {
|
|
||||||
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
|
|
||||||
RespondError(w, apiErrorObj, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// prometheus instant query needs same timestamp
|
|
||||||
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
|
|
||||||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM {
|
|
||||||
metricsQueryRangeParams.Start = metricsQueryRangeParams.End
|
|
||||||
}
|
|
||||||
|
|
||||||
// round up the end to nearest multiple
|
|
||||||
if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER {
|
|
||||||
end := (metricsQueryRangeParams.End) / 1000
|
|
||||||
step := metricsQueryRangeParams.Step
|
|
||||||
metricsQueryRangeParams.End = (end / step * step) * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
type channelResult struct {
|
|
||||||
Series []*basemodel.Series
|
|
||||||
TableName string
|
|
||||||
Err error
|
|
||||||
Name string
|
|
||||||
Query string
|
|
||||||
}
|
|
||||||
|
|
||||||
execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) {
|
|
||||||
var seriesList []*basemodel.Series
|
|
||||||
var tableName []string
|
|
||||||
ch := make(chan channelResult, len(queries))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for name, query := range queries {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(name, query string) {
|
|
||||||
defer wg.Done()
|
|
||||||
seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query)
|
|
||||||
for _, series := range seriesList {
|
|
||||||
series.QueryName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ch <- channelResult{Series: seriesList, TableName: tableName}
|
|
||||||
}(name, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
errQuriesByName := make(map[string]string)
|
|
||||||
// read values from the channel
|
|
||||||
for r := range ch {
|
|
||||||
if r.Err != nil {
|
|
||||||
errs = append(errs, r.Err)
|
|
||||||
errQuriesByName[r.Name] = r.Query
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seriesList = append(seriesList, r.Series...)
|
|
||||||
tableName = append(tableName, r.TableName)
|
|
||||||
}
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
|
||||||
}
|
|
||||||
return seriesList, tableName, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) {
|
|
||||||
var seriesList []*basemodel.Series
|
|
||||||
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries {
|
|
||||||
if query.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func(name string, query *basemodel.PromQuery) {
|
|
||||||
var seriesList []*basemodel.Series
|
|
||||||
defer wg.Done()
|
|
||||||
tmpl := template.New("promql-query")
|
|
||||||
tmpl, tmplErr := tmpl.Parse(query.Query)
|
|
||||||
if tmplErr != nil {
|
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var queryBuf bytes.Buffer
|
|
||||||
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
|
|
||||||
if tmplErr != nil {
|
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
query.Query = queryBuf.String()
|
|
||||||
queryModel := basemodel.QueryRangeParams{
|
|
||||||
Start: time.UnixMilli(metricsQueryRangeParams.Start),
|
|
||||||
End: time.UnixMilli(metricsQueryRangeParams.End),
|
|
||||||
Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)),
|
|
||||||
Query: query.Query,
|
|
||||||
}
|
|
||||||
promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel)
|
|
||||||
if err != nil {
|
|
||||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
matrix, _ := promResult.Matrix()
|
|
||||||
for _, v := range matrix {
|
|
||||||
var s basemodel.Series
|
|
||||||
s.QueryName = name
|
|
||||||
s.Labels = v.Metric.Copy().Map()
|
|
||||||
for _, p := range v.Floats {
|
|
||||||
s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.F})
|
|
||||||
}
|
|
||||||
seriesList = append(seriesList, &s)
|
|
||||||
}
|
|
||||||
ch <- channelResult{Series: seriesList}
|
|
||||||
}(name, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(ch)
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
errQuriesByName := make(map[string]string)
|
|
||||||
// read values from the channel
|
|
||||||
for r := range ch {
|
|
||||||
if r.Err != nil {
|
|
||||||
errs = append(errs, r.Err)
|
|
||||||
errQuriesByName[r.Name] = r.Query
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seriesList = append(seriesList, r.Series...)
|
|
||||||
}
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
|
||||||
}
|
|
||||||
return seriesList, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var seriesList []*basemodel.Series
|
|
||||||
var tableName []string
|
|
||||||
var err error
|
|
||||||
var errQuriesByName map[string]string
|
|
||||||
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
|
|
||||||
case basemodel.QUERY_BUILDER:
|
|
||||||
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
|
|
||||||
if runQueries.Err != nil {
|
|
||||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
|
|
||||||
|
|
||||||
case basemodel.CLICKHOUSE:
|
|
||||||
queries := make(map[string]string)
|
|
||||||
|
|
||||||
for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries {
|
|
||||||
if chQuery.Disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tmpl := template.New("clickhouse-query")
|
|
||||||
tmpl, err := tmpl.Parse(chQuery.Query)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var query bytes.Buffer
|
|
||||||
|
|
||||||
// replace go template variables
|
|
||||||
querytemplate.AssignReservedVars(metricsQueryRangeParams)
|
|
||||||
|
|
||||||
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
queries[name] = query.String()
|
|
||||||
}
|
|
||||||
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries)
|
|
||||||
case basemodel.PROM:
|
|
||||||
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("invalid query type")
|
|
||||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
|
||||||
RespondError(w, apiErrObj, errQuriesByName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
|
|
||||||
len(seriesList) > 1 &&
|
|
||||||
(metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER ||
|
|
||||||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) {
|
|
||||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseFormat struct {
|
|
||||||
ResultType string `json:"resultType"`
|
|
||||||
Result []*basemodel.Series `json:"result"`
|
|
||||||
TableName []string `json:"tableName"`
|
|
||||||
}
|
|
||||||
resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName}
|
|
||||||
ah.Respond(w, resp)
|
|
||||||
}
|
|
||||||
@@ -329,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
r.Use(loggingMiddleware)
|
r.Use(loggingMiddleware)
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterMetricsRoutes(r, am)
|
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import (
|
|||||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
||||||
"go.signoz.io/signoz/ee/query-service/app"
|
"go.signoz.io/signoz/ee/query-service/app"
|
||||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
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/migrate"
|
||||||
"go.signoz.io/signoz/pkg/query-service/version"
|
"go.signoz.io/signoz/pkg/query-service/version"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
@@ -143,6 +145,12 @@ func main() {
|
|||||||
zap.L().Info("JWT secret key set successfully.")
|
zap.L().Info("JWT secret key set successfully.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := migrate.Migrate(constants.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||||
|
zap.L().Error("Failed to migrate", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zap.L().Info("Migration successful")
|
||||||
|
}
|
||||||
|
|
||||||
server, err := app.NewServer(serverOptions)
|
server, err := app.NewServer(serverOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
Name: basemodel.QueryBuilderPanels,
|
Name: basemodel.QueryBuilderPanels,
|
||||||
Active: true,
|
Active: true,
|
||||||
Usage: 0,
|
Usage: 0,
|
||||||
UsageLimit: 20,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
Name: basemodel.QueryBuilderAlerts,
|
Name: basemodel.QueryBuilderAlerts,
|
||||||
Active: true,
|
Active: true,
|
||||||
Usage: 0,
|
Usage: 0,
|
||||||
UsageLimit: 10,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nginx:1.25.2-alpine
|
FROM nginx:1.26-alpine
|
||||||
|
|
||||||
# Add Maintainer Info
|
# Add Maintainer Info
|
||||||
LABEL maintainer="signoz"
|
LABEL maintainer="signoz"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const config: Config.InitialOptions = {
|
|||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
|
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
|
||||||
|
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
||||||
modulePathIgnorePatterns: ['dist'],
|
modulePathIgnorePatterns: ['dist'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
@@ -20,8 +21,6 @@ const config: Config.InitialOptions = {
|
|||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
'^.+\\.(css|scss|sass|less)$': 'jest-preview/transforms/css',
|
|
||||||
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': 'jest-preview/transforms/file',
|
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)',
|
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)',
|
||||||
@@ -35,6 +34,14 @@ const config: Config.InitialOptions = {
|
|||||||
browsers: ['chromium', 'firefox', 'webkit'],
|
browsers: ['chromium', 'firefox', 'webkit'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
statements: 80,
|
||||||
|
branches: 65,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -13,15 +13,15 @@
|
|||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:coverage": "jest --coverage",
|
"jest:coverage": "jest --coverage",
|
||||||
"jest:watch": "jest --watch",
|
"jest:watch": "jest --watch",
|
||||||
"jest-preview": "jest-preview",
|
|
||||||
"test:debug": "npm-run-all -p test jest-preview",
|
|
||||||
"postinstall": "is-ci || yarn husky:configure",
|
"postinstall": "is-ci || yarn husky:configure",
|
||||||
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||||
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
||||||
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||||
"commitlint": "commitlint --edit $1"
|
"commitlint": "commitlint --edit $1",
|
||||||
|
"test": "jest --coverage",
|
||||||
|
"test:changedsince": "jest --changedSince=develop --coverage --silent"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.15.0"
|
"node": ">=16.15.0"
|
||||||
@@ -44,11 +44,14 @@
|
|||||||
"@sentry/webpack-plugin": "2.16.0",
|
"@sentry/webpack-plugin": "2.16.0",
|
||||||
"@signozhq/design-tokens": "0.0.8",
|
"@signozhq/design-tokens": "0.0.8",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
|
"@visx/group": "3.3.0",
|
||||||
|
"@visx/shape": "3.5.0",
|
||||||
|
"@visx/tooltip": "3.3.0",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "1.6.2",
|
"axios": "1.6.4",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@@ -178,7 +181,7 @@
|
|||||||
"@types/webpack-dev-server": "^4.7.2",
|
"@types/webpack-dev-server": "^4.7.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
"autoprefixer": "^9.0.0",
|
"autoprefixer": "10.4.19",
|
||||||
"babel-plugin-styled-components": "^1.12.0",
|
"babel-plugin-styled-components": "^1.12.0",
|
||||||
"compression-webpack-plugin": "9.0.0",
|
"compression-webpack-plugin": "9.0.0",
|
||||||
"copy-webpack-plugin": "^8.1.0",
|
"copy-webpack-plugin": "^8.1.0",
|
||||||
@@ -201,12 +204,12 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest-playwright-preset": "^1.7.2",
|
"jest-playwright-preset": "^1.7.2",
|
||||||
"jest-preview": "0.3.1",
|
|
||||||
"jest-styled-components": "^7.0.8",
|
"jest-styled-components": "^7.0.8",
|
||||||
"lint-staged": "^12.5.0",
|
"lint-staged": "^12.5.0",
|
||||||
"msw": "1.3.2",
|
"msw": "1.3.2",
|
||||||
"npm-run-all": "latest",
|
"npm-run-all": "latest",
|
||||||
"portfinder-sync": "^0.0.2",
|
"portfinder-sync": "^0.0.2",
|
||||||
|
"postcss": "8.4.38",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"raw-loader": "4.0.2",
|
"raw-loader": "4.0.2",
|
||||||
"react-hooks-testing-library": "0.6.0",
|
"react-hooks-testing-library": "0.6.0",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c1 2.538 2.5 2.962 3.5 3.808.942.78 1.481 1.845 1.5 2.961 0 1.122-.527 2.198-1.464 2.992C14.598 12.554 13.326 13 12 13s-2.598-.446-3.536-1.24C7.527 10.968 7 9.892 7 8.77c0-.255 0-.508.1-.762.085.25.236.48.443.673.207.193.463.342.75.437a2.334 2.334 0 001.767-.128c.263-.135.485-.32.65-.539.166-.22.269-.468.301-.727a1.452 1.452 0 00-.11-.765 1.699 1.699 0 00-.501-.644C8 4.115 11 2 12 2zM17 16l-5 6-5-6h10z" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 581 B |
@@ -15,6 +15,7 @@
|
|||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
"field_channel_name": "Name",
|
"field_channel_name": "Name",
|
||||||
|
"field_send_resolved": "Send resolved alerts",
|
||||||
"field_channel_type": "Type",
|
"field_channel_type": "Type",
|
||||||
"field_webhook_url": "Webhook URL",
|
"field_webhook_url": "Webhook URL",
|
||||||
"field_slack_recipient": "Recipient",
|
"field_slack_recipient": "Recipient",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"new_dashboard_title": "Sample Title",
|
"new_dashboard_title": "Sample Title",
|
||||||
"layout_saved_successfully": "Layout saved successfully",
|
"layout_saved_successfully": "Layout saved successfully",
|
||||||
"add_panel": "Add Panel",
|
"add_panel": "Add Panel",
|
||||||
|
"add_row": "Add Row",
|
||||||
"save_layout": "Save Layout",
|
"save_layout": "Save Layout",
|
||||||
"variable_updated_successfully": "Variable updated successfully",
|
"variable_updated_successfully": "Variable updated successfully",
|
||||||
"error_while_updating_variable": "Error while updating variable",
|
"error_while_updating_variable": "Error while updating variable",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
"field_channel_name": "Name",
|
"field_channel_name": "Name",
|
||||||
|
"field_send_resolved": "Send resolved alerts",
|
||||||
"field_channel_type": "Type",
|
"field_channel_type": "Type",
|
||||||
"field_webhook_url": "Webhook URL",
|
"field_webhook_url": "Webhook URL",
|
||||||
"field_slack_recipient": "Recipient",
|
"field_slack_recipient": "Recipient",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"new_dashboard_title": "Sample Title",
|
"new_dashboard_title": "Sample Title",
|
||||||
"layout_saved_successfully": "Layout saved successfully",
|
"layout_saved_successfully": "Layout saved successfully",
|
||||||
"add_panel": "Add Panel",
|
"add_panel": "Add Panel",
|
||||||
|
"add_row": "Add Row",
|
||||||
"save_layout": "Save Layout",
|
"save_layout": "Save Layout",
|
||||||
"full_view": "Full Screen View",
|
"full_view": "Full Screen View",
|
||||||
"variable_updated_successfully": "Variable updated successfully",
|
"variable_updated_successfully": "Variable updated successfully",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
email_configs: [
|
email_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
to: props.to,
|
to: props.to,
|
||||||
html: props.html,
|
html: props.html,
|
||||||
headers: props.headers,
|
headers: props.headers,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
msteams_configs: [
|
msteams_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
webhook_url: props.webhook_url,
|
webhook_url: props.webhook_url,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
pagerduty_configs: [
|
pagerduty_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
routing_key: props.routing_key,
|
routing_key: props.routing_key,
|
||||||
client: props.client,
|
client: props.client,
|
||||||
client_url: props.client_url,
|
client_url: props.client_url,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
slack_configs: [
|
slack_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_url: props.api_url,
|
api_url: props.api_url,
|
||||||
channel: props.channel,
|
channel: props.channel,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
webhook_configs: [
|
webhook_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
url: props.api_url,
|
url: props.api_url,
|
||||||
http_config: httpConfig,
|
http_config: httpConfig,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editEmail = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
email_configs: [
|
email_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
to: props.to,
|
to: props.to,
|
||||||
html: props.html,
|
html: props.html,
|
||||||
headers: props.headers,
|
headers: props.headers,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editMsTeams = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
msteams_configs: [
|
msteams_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
webhook_url: props.webhook_url,
|
webhook_url: props.webhook_url,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editOpsgenie = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
opsgenie_configs: [
|
opsgenie_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_key: props.api_key,
|
api_key: props.api_key,
|
||||||
description: props.description,
|
description: props.description,
|
||||||
priority: props.priority,
|
priority: props.priority,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editPager = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
pagerduty_configs: [
|
pagerduty_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
routing_key: props.routing_key,
|
routing_key: props.routing_key,
|
||||||
client: props.client,
|
client: props.client,
|
||||||
client_url: props.client_url,
|
client_url: props.client_url,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editSlack = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
slack_configs: [
|
slack_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_url: props.api_url,
|
api_url: props.api_url,
|
||||||
channel: props.channel,
|
channel: props.channel,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const editWebhook = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
webhook_configs: [
|
webhook_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
url: props.api_url,
|
url: props.api_url,
|
||||||
http_config: httpConfig,
|
http_config: httpConfig,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'api';
|
import { ApiV4Instance } from 'api';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
||||||
|
|
||||||
@@ -6,4 +6,6 @@ export const getMetricMeta = (
|
|||||||
metricName: string,
|
metricName: string,
|
||||||
servicename: string,
|
servicename: string,
|
||||||
): Promise<AxiosResponse<MetricMetaProps>> =>
|
): Promise<AxiosResponse<MetricMetaProps>> =>
|
||||||
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
|
ApiV4Instance.get(
|
||||||
|
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import {
|
|
||||||
MetricNameProps,
|
|
||||||
MetricNamesPayloadProps,
|
|
||||||
} from 'types/api/metrics/getMetricName';
|
|
||||||
|
|
||||||
export const getMetricName = async (
|
|
||||||
props: MetricNameProps,
|
|
||||||
): Promise<SuccessResponse<MetricNamesPayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`/metrics/autocomplete/list?match=${props || ''}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
import { ApiV3Instance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
TagKeyProps,
|
TagKeyProps,
|
||||||
@@ -8,15 +9,19 @@ import {
|
|||||||
TagValueProps,
|
TagValueProps,
|
||||||
TagValuesPayloadProps,
|
TagValuesPayloadProps,
|
||||||
} from 'types/api/metrics/getResourceAttributes';
|
} from 'types/api/metrics/getResourceAttributes';
|
||||||
|
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const getResourceAttributesTagKeys = async (
|
export const getResourceAttributesTagKeys = async (
|
||||||
props: TagKeyProps,
|
props: TagKeyProps,
|
||||||
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
|
`/autocomplete/attribute_keys?${createQueryParams({
|
||||||
props.match ? `&match=${props.match}` : ''
|
aggregateOperator: MetricAggregateOperator.RATE,
|
||||||
}`,
|
searchText: props.match,
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
aggregateAttribute: props.metricName,
|
||||||
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
|
|||||||
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
|
`/autocomplete/attribute_values?${createQueryParams({
|
||||||
|
aggregateOperator: MetricAggregateOperator.RATE,
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
aggregateAttribute: props.metricName,
|
||||||
|
attributeKey: props.tagKey,
|
||||||
|
searchText: '',
|
||||||
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
27
frontend/src/assets/Dashboard/PromQl.tsx
Normal file
27
frontend/src/assets/Dashboard/PromQl.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
function PromQLIcon({
|
||||||
|
fillColor,
|
||||||
|
}: {
|
||||||
|
fillColor: CSSProperties['color'];
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2c1 2.538 2.5 2.962 3.5 3.808.942.78 1.481 1.845 1.5 2.961 0 1.122-.527 2.198-1.464 2.992C14.598 12.554 13.326 13 12 13s-2.598-.446-3.536-1.24C7.527 10.968 7 9.892 7 8.77c0-.255 0-.508.1-.762.085.25.236.48.443.673.207.193.463.342.75.437a2.334 2.334 0 001.767-.128c.263-.135.485-.32.65-.539.166-.22.269-.468.301-.727a1.452 1.452 0 00-.11-.765 1.699 1.699 0 00-.501-.644C8 4.115 11 2 12 2zM17 16l-5 6-5-6h10z"
|
||||||
|
stroke={fillColor}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PromQLIcon;
|
||||||
@@ -16,6 +16,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
// interfaces
|
// interfaces
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||||
@@ -50,7 +51,11 @@ function LogGeneralField({
|
|||||||
}: LogFieldProps): JSX.Element {
|
}: LogFieldProps): JSX.Element {
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(dompurify.sanitize(fieldValue)),
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(fieldValue, {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
[fieldValue],
|
[fieldValue],
|
||||||
);
|
);
|
||||||
@@ -157,8 +162,8 @@ function ListLogView({
|
|||||||
const timestampValue = useMemo(
|
const timestampValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof flattenLogData.timestamp === 'string'
|
typeof flattenLogData.timestamp === 'string'
|
||||||
? dayjs(flattenLogData.timestamp).format()
|
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(flattenLogData.timestamp / 1e6).format(),
|
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
|
||||||
[flattenLogData.timestamp],
|
[flattenLogData.timestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
@@ -90,12 +91,12 @@ function RawLogView({
|
|||||||
const text = useMemo(
|
const text = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
|
? `${dayjs(data.timestamp).format(
|
||||||
data.body
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
}`
|
)} | ${attributesText} ${severityText} ${data.body}`
|
||||||
: `${dayjs(
|
: `${dayjs(data.timestamp / 1e6).format(
|
||||||
data.timestamp / 1e6,
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
).format()} | ${attributesText} ${severityText} ${data.body}`,
|
)} | ${attributesText} ${severityText} ${data.body}`,
|
||||||
[data.timestamp, data.body, severityText, attributesText],
|
[data.timestamp, data.body, severityText, attributesText],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,7 +145,9 @@ function RawLogView({
|
|||||||
|
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(dompurify.sanitize(text)),
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
[text],
|
[text],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import dompurify from 'dompurify';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
|
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
|
||||||
@@ -76,8 +77,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? dayjs(field).format()
|
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(field / 1e6).format();
|
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
return {
|
return {
|
||||||
children: (
|
children: (
|
||||||
<div className="table-timestamp">
|
<div className="table-timestamp">
|
||||||
@@ -107,7 +108,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
children: (
|
children: (
|
||||||
<TableBodyContent
|
<TableBodyContent
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: convert.toHtml(dompurify.sanitize(field)),
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(field, {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './DynamicColumnTable.syles.scss';
|
import './DynamicColumnTable.syles.scss';
|
||||||
|
|
||||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
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';
|
||||||
@@ -20,6 +21,7 @@ function DynamicColumnTable({
|
|||||||
columns,
|
columns,
|
||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
|
facingIssueBtn,
|
||||||
...restProps
|
...restProps
|
||||||
}: DynamicColumnTableProps): JSX.Element {
|
}: DynamicColumnTableProps): JSX.Element {
|
||||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||||
@@ -83,19 +85,22 @@ function DynamicColumnTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="DynamicColumnTable">
|
<div className="DynamicColumnTable">
|
||||||
{dynamicColumns && (
|
<Flex justify="flex-end" align="center" gap={8}>
|
||||||
<Dropdown
|
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
|
||||||
getPopupContainer={popupContainer}
|
{dynamicColumns && (
|
||||||
menu={{ items }}
|
<Dropdown
|
||||||
trigger={['click']}
|
getPopupContainer={popupContainer}
|
||||||
>
|
menu={{ items }}
|
||||||
<Button
|
trigger={['click']}
|
||||||
className="dynamicColumnTable-button filter-btn"
|
>
|
||||||
size="middle"
|
<Button
|
||||||
icon={<SlidersHorizontal size={14} />}
|
className="dynamicColumnTable-button filter-btn"
|
||||||
/>
|
size="middle"
|
||||||
</Dropdown>
|
icon={<SlidersHorizontal size={14} />}
|
||||||
)}
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columnsData}
|
columns={columnsData}
|
||||||
|
|||||||
@@ -2,6 +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 { TableDataSource } from './contants';
|
import { TableDataSource } from './contants';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetVisibleColumnsFunction = (
|
export type GetVisibleColumnsFunction = (
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.facing-issue-button {
|
||||||
|
color: var(--bg-amber-500);
|
||||||
|
border-color: var(--bg-amber-500);
|
||||||
|
|
||||||
|
.ant-btn:hover {
|
||||||
|
color: var(--bg-amber-400) !important;
|
||||||
|
border-color: var(--bg-amber-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
62
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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>
|
||||||
|
<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;
|
||||||
57
frontend/src/components/facingIssueBtn/util.ts
Normal file
57
frontend/src/components/facingIssueBtn/util.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
export const chartHelpMessage = (
|
||||||
|
selectedDashboard: Dashboard | undefined,
|
||||||
|
graphType: PANEL_TYPES,
|
||||||
|
): string => `
|
||||||
|
Hi Team,
|
||||||
|
|
||||||
|
I need help in creating this chart. Here are my dashboard details
|
||||||
|
|
||||||
|
Name: ${selectedDashboard?.data.title || ''}
|
||||||
|
Panel type: ${graphType}
|
||||||
|
Dashboard Id: ${selectedDashboard?.uuid || ''}
|
||||||
|
|
||||||
|
Thanks`;
|
||||||
|
|
||||||
|
export const dashboardHelpMessage = (
|
||||||
|
data: DashboardData | undefined,
|
||||||
|
selectedDashboard: Dashboard | undefined,
|
||||||
|
): string => `
|
||||||
|
Hi Team,
|
||||||
|
|
||||||
|
I need help with this dashboard. Here are my dashboard details
|
||||||
|
|
||||||
|
Name: ${data?.title || ''}
|
||||||
|
Dashboard Id: ${selectedDashboard?.uuid || ''}
|
||||||
|
|
||||||
|
Thanks`;
|
||||||
|
|
||||||
|
export const dashboardListMessage = `Hi Team,
|
||||||
|
|
||||||
|
I need help with dashboards.
|
||||||
|
|
||||||
|
Thanks`;
|
||||||
|
|
||||||
|
export const listAlertMessage = `Hi Team,
|
||||||
|
|
||||||
|
I need help with managing alerts.
|
||||||
|
|
||||||
|
Thanks`;
|
||||||
|
|
||||||
|
export const alertHelpMessage = (
|
||||||
|
alertDef: AlertDef,
|
||||||
|
ruleId: number,
|
||||||
|
): string => `
|
||||||
|
Hi Team,
|
||||||
|
|
||||||
|
I need help in configuring this alert. Here are my alert rule details
|
||||||
|
|
||||||
|
Name: ${alertDef?.alert || ''}
|
||||||
|
Alert Type: ${alertDef?.alertType || ''}
|
||||||
|
State: ${(alertDef as any)?.state || ''}
|
||||||
|
Alert Id: ${ruleId}
|
||||||
|
|
||||||
|
Thanks`;
|
||||||
@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
|
|||||||
[PANEL_TYPES.LIST]:
|
[PANEL_TYPES.LIST]:
|
||||||
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
|
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
|
||||||
[PANEL_TYPES.BAR]: Uplot,
|
[PANEL_TYPES.BAR]: Uplot,
|
||||||
|
[PANEL_TYPES.PIE]: null,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ export enum QueryParams {
|
|||||||
integration = 'integration',
|
integration = 'integration',
|
||||||
pagination = 'pagination',
|
pagination = 'pagination',
|
||||||
relativeTime = 'relativeTime',
|
relativeTime = 'relativeTime',
|
||||||
|
alertType = 'alertType',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,9 +285,15 @@ export enum PANEL_TYPES {
|
|||||||
LIST = 'list',
|
LIST = 'list',
|
||||||
TRACE = 'trace',
|
TRACE = 'trace',
|
||||||
BAR = 'bar',
|
BAR = 'bar',
|
||||||
|
PIE = 'pie',
|
||||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
export enum PANEL_GROUP_TYPES {
|
||||||
|
ROW = 'row',
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export enum ATTRIBUTE_TYPES {
|
export enum ATTRIBUTE_TYPES {
|
||||||
SUM = 'Sum',
|
SUM = 'Sum',
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
className={cx(
|
className={cx(
|
||||||
'app-layout',
|
'app-layout',
|
||||||
isDarkMode ? 'darkMode' : 'lightMode',
|
isDarkMode ? 'darkMode' : 'lightMode',
|
||||||
!collapsed ? 'docked' : '',
|
!collapsed && !renderFullScreen ? 'docked' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.billing-container {
|
.billing-container {
|
||||||
|
margin-bottom: 40px;
|
||||||
padding-top: 36px;
|
padding-top: 36px;
|
||||||
width: 65%;
|
width: 65%;
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,14 @@ function CreateAlertChannels({
|
|||||||
EmailChannel
|
EmailChannel
|
||||||
>
|
>
|
||||||
>({
|
>({
|
||||||
|
send_resolved: true,
|
||||||
text: `{{ range .Alerts -}}
|
text: `{{ range .Alerts -}}
|
||||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||||
|
|
||||||
*Summary:* {{ .Annotations.summary }}
|
*Summary:* {{ .Annotations.summary }}
|
||||||
*Description:* {{ .Annotations.description }}
|
*Description:* {{ .Annotations.description }}
|
||||||
*RelatedLogs:* {{ .Annotations.related_logs }}
|
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
|
||||||
*RelatedTraces:* {{ .Annotations.related_traces }}
|
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
|
||||||
|
|
||||||
*Details:*
|
*Details:*
|
||||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||||
@@ -119,7 +120,7 @@ function CreateAlertChannels({
|
|||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
}),
|
}),
|
||||||
@@ -158,7 +159,7 @@ function CreateAlertChannels({
|
|||||||
let request: WebhookChannel = {
|
let request: WebhookChannel = {
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
|
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
|
||||||
@@ -226,7 +227,7 @@ function CreateAlertChannels({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
routing_key: selectedConfig?.routing_key || '',
|
routing_key: selectedConfig?.routing_key || '',
|
||||||
client: selectedConfig?.client || '',
|
client: selectedConfig?.client || '',
|
||||||
client_url: selectedConfig?.client_url || '',
|
client_url: selectedConfig?.client_url || '',
|
||||||
@@ -274,7 +275,7 @@ function CreateAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
api_key: selectedConfig?.api_key || '',
|
api_key: selectedConfig?.api_key || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
description: selectedConfig?.description || '',
|
description: selectedConfig?.description || '',
|
||||||
message: selectedConfig?.message || '',
|
message: selectedConfig?.message || '',
|
||||||
priority: selectedConfig?.priority || '',
|
priority: selectedConfig?.priority || '',
|
||||||
@@ -312,7 +313,7 @@ function CreateAlertChannels({
|
|||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
to: selectedConfig?.to || '',
|
to: selectedConfig?.to || '',
|
||||||
html: selectedConfig?.html || '',
|
html: selectedConfig?.html || '',
|
||||||
headers: selectedConfig?.headers || {},
|
headers: selectedConfig?.headers || {},
|
||||||
@@ -350,7 +351,7 @@ function CreateAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
webhook_url: selectedConfig?.webhook_url || '',
|
webhook_url: selectedConfig?.webhook_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
|
import history from 'lib/history';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -18,15 +20,25 @@ import SelectAlertType from './SelectAlertType';
|
|||||||
|
|
||||||
function CreateRules(): JSX.Element {
|
function CreateRules(): JSX.Element {
|
||||||
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
||||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
|
||||||
AlertTypes.METRICS_BASED_ALERT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
const version = queryParams.get('version');
|
const version = queryParams.get('version');
|
||||||
|
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
|
||||||
|
|
||||||
const compositeQuery = useGetCompositeQueryParam();
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
|
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||||
|
if (!compositeQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||||
|
|
||||||
|
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||||
|
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
|
||||||
|
);
|
||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
@@ -48,21 +60,17 @@ function CreateRules(): JSX.Element {
|
|||||||
version: version || ENTITY_VERSION_V4,
|
version: version || ENTITY_VERSION_V4,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
queryParams.set(QueryParams.alertType, typ);
|
||||||
|
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||||
|
history.replace(generatedUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!compositeQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
|
||||||
|
|
||||||
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
|
||||||
|
|
||||||
if (alertType) {
|
if (alertType) {
|
||||||
onSelectType(alertType);
|
onSelectType(alertType);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [compositeQuery]);
|
}, [alertType]);
|
||||||
|
|
||||||
if (!initValues) {
|
if (!initValues) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function EditAlertChannels({
|
|||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
id,
|
id,
|
||||||
@@ -115,7 +115,7 @@ function EditAlertChannels({
|
|||||||
return {
|
return {
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
name: name || '',
|
name: name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
id,
|
id,
|
||||||
@@ -284,7 +284,7 @@ function EditAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
webhook_url: selectedConfig?.webhook_url || '',
|
webhook_url: selectedConfig?.webhook_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -64,6 +64,10 @@
|
|||||||
|
|
||||||
.view-options,
|
.view-options,
|
||||||
.actions {
|
.actions {
|
||||||
|
.info-icon {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -252,6 +256,10 @@
|
|||||||
color: var(--bg-ink-200);
|
color: var(--bg-ink-200);
|
||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './ExplorerOptions.styles.scss';
|
import './ExplorerOptions.styles.scss';
|
||||||
|
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -402,6 +403,28 @@ function ExplorerOptions({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
{sourcepage === DataSource.LOGS
|
||||||
|
? 'Learn more about Logs explorer '
|
||||||
|
: 'Learn more about Traces explorer '}
|
||||||
|
<Typography.Link
|
||||||
|
href={
|
||||||
|
sourcepage === DataSource.LOGS
|
||||||
|
? 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar'
|
||||||
|
: 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar'
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
here
|
||||||
|
</Typography.Link>{' '}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InfoCircleOutlined className="info-icon" />
|
||||||
|
</Tooltip>
|
||||||
<Tooltip title="Hide">
|
<Tooltip title="Hide">
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Form, FormInstance, Input, Select, Typography } from 'antd';
|
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
|
||||||
import { Store } from 'antd/lib/form/interface';
|
import { Store } from 'antd/lib/form/interface';
|
||||||
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
|
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@@ -95,6 +95,22 @@ function FormAlertChannels({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('field_send_resolved')}
|
||||||
|
labelAlign="left"
|
||||||
|
name="send_resolved"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={initialValue?.send_resolved}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setSelectedConfig((state) => ({
|
||||||
|
...state,
|
||||||
|
send_resolved: value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
||||||
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
||||||
<Select.Option value="slack" key="slack">
|
<Select.Option value="slack" key="slack">
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
.create-alert-modal {
|
.create-alert-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
.ant-modal-confirm-title {
|
.ant-modal-confirm-title {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-content {
|
.ant-modal-confirm-content {
|
||||||
.ant-typography {
|
.ant-typography {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-btns {
|
.ant-modal-confirm-btns {
|
||||||
button:nth-of-type(1) {
|
button:nth-of-type(1) {
|
||||||
background-color: var(--bg-slate-400);
|
background-color: var(--bg-slate-400);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-100);
|
||||||
.ant-modal-confirm-title {
|
.ant-modal-confirm-title {
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-content {
|
.ant-modal-confirm-content {
|
||||||
.ant-typography {
|
.ant-typography {
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-btns {
|
.ant-modal-confirm-btns {
|
||||||
button:nth-of-type(1) {
|
button:nth-of-type(1) {
|
||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-300);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.facing-issue-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import './QuerySection.styles.scss';
|
import './QuerySection.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Tooltip } from 'antd';
|
import { Button, Tabs, Tooltip } from 'antd';
|
||||||
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -48,6 +51,8 @@ function QuerySection({
|
|||||||
|
|
||||||
const renderChQueryUI = (): JSX.Element => <ChQuerySection />;
|
const renderChQueryUI = (): JSX.Element => <ChQuerySection />;
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const renderMetricUI = (): JSX.Element => (
|
const renderMetricUI = (): JSX.Element => (
|
||||||
<QueryBuilder
|
<QueryBuilder
|
||||||
panelType={panelType}
|
panelType={panelType}
|
||||||
@@ -113,14 +118,16 @@ function QuerySection({
|
|||||||
label: (
|
label: (
|
||||||
<Tooltip title="PromQL">
|
<Tooltip title="PromQL">
|
||||||
<Button className="nav-btns">
|
<Button className="nav-btns">
|
||||||
<img src="/Icons/promQL.svg" alt="Prom Ql" className="prom-ql-icon" />
|
<PromQLIcon
|
||||||
|
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
key: EQueryType.PROM,
|
key: EQueryType.PROM,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[isDarkMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
|
|||||||
<Col flex="none">
|
<Col flex="none">
|
||||||
<TextToolTip
|
<TextToolTip
|
||||||
text={t('user_tooltip_more_help')}
|
text={t('user_tooltip_more_help')}
|
||||||
url="https://signoz.io/docs/userguide/alerts-management/#create-alert-rules"
|
url="https://signoz.io/docs/userguide/alerts-management/?utm_source=product&utm_medium=create-alert#creating-a-new-alert-in-signoz"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
|
import { alertHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -138,15 +140,21 @@ function FormAlertRules({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set selectedQueryName based on the length of queryOptions
|
// Set selectedQueryName based on the length of queryOptions
|
||||||
setAlertDef((def) => ({
|
const selectedQueryName = alertDef?.condition?.selectedQueryName;
|
||||||
...def,
|
if (
|
||||||
condition: {
|
!selectedQueryName ||
|
||||||
...def.condition,
|
!queryOptions.some((option) => option.value === selectedQueryName)
|
||||||
selectedQueryName:
|
) {
|
||||||
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
setAlertDef((def) => ({
|
||||||
},
|
...def,
|
||||||
}));
|
condition: {
|
||||||
}, [currentQuery?.queryType, queryOptions]);
|
...def.condition,
|
||||||
|
selectedQueryName:
|
||||||
|
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||||
|
|
||||||
const onCancelHandler = useCallback(() => {
|
const onCancelHandler = useCallback(() => {
|
||||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||||
@@ -482,6 +490,8 @@ function FormAlertRules({
|
|||||||
alertDef?.broadcastToAll ||
|
alertDef?.broadcastToAll ||
|
||||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||||
|
|
||||||
|
const isRuleCreated = !ruleId || ruleId === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
@@ -514,6 +524,7 @@ function FormAlertRules({
|
|||||||
runQuery={handleRunQuery}
|
runQuery={handleRunQuery}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
|
key={currentQuery.queryType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RuleOptions
|
<RuleOptions
|
||||||
@@ -563,6 +574,22 @@ function FormAlertRules({
|
|||||||
</StyledLeftContainer>
|
</StyledLeftContainer>
|
||||||
<Col flex="1 1 300px">
|
<Col flex="1 1 300px">
|
||||||
<UserGuide queryType={currentQuery.queryType} />
|
<UserGuide queryType={currentQuery.queryType} />
|
||||||
|
<FacingIssueBtn
|
||||||
|
attributes={{
|
||||||
|
alert: alertDef?.alert,
|
||||||
|
alertType: alertDef?.alertType,
|
||||||
|
id: ruleId,
|
||||||
|
ruleType: alertDef?.ruleType,
|
||||||
|
state: (alertDef as any)?.state,
|
||||||
|
panelType,
|
||||||
|
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
|
||||||
|
}}
|
||||||
|
className="facing-issue-btn"
|
||||||
|
eventName="Alert: Facing Issues in alert"
|
||||||
|
buttonText="Need help with this alert?"
|
||||||
|
message={alertHelpMessage(alertDef, ruleId)}
|
||||||
|
onHoverText="Click here to get help with this alert"
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
|
|||||||
LIST: false,
|
LIST: false,
|
||||||
TRACE: false,
|
TRACE: false,
|
||||||
BAR: true,
|
BAR: true,
|
||||||
|
PIE: false,
|
||||||
EMPTY_WIDGET: false,
|
EMPTY_WIDGET: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function WidgetGraphComponent({
|
|||||||
|
|
||||||
const lineChartRef = useRef<ToggleGraphProps>();
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
||||||
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
Array(queryResponse.data?.payload?.data?.result?.length || 0).fill(true),
|
||||||
);
|
);
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ function WidgetGraphComponent({
|
|||||||
i: uuid,
|
i: uuid,
|
||||||
w: 6,
|
w: 6,
|
||||||
x: 0,
|
x: 0,
|
||||||
h: 3,
|
h: 6,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ function GridCardGraph({
|
|||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
const {
|
||||||
|
toScrollWidgetId,
|
||||||
|
setToScrollWidgetId,
|
||||||
|
variablesToGetUpdated,
|
||||||
|
} = useDashboard();
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
@@ -90,7 +94,11 @@ function GridCardGraph({
|
|||||||
const isEmptyWidget =
|
const isEmptyWidget =
|
||||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||||
|
|
||||||
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
|
const queryEnabledCondition =
|
||||||
|
isVisible &&
|
||||||
|
!isEmptyWidget &&
|
||||||
|
isQueryEnabled &&
|
||||||
|
isEmpty(variablesToGetUpdated);
|
||||||
|
|
||||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||||
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||||
@@ -166,7 +174,8 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const menuList =
|
const menuList =
|
||||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||||
widget.panelTypes === PANEL_TYPES.LIST
|
widget.panelTypes === PANEL_TYPES.LIST ||
|
||||||
|
widget.panelTypes === PANEL_TYPES.PIE
|
||||||
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||||
: headerMenuList;
|
: headerMenuList;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import './GridCardLayout.styles.scss';
|
import './GridCardLayout.styles.scss';
|
||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip } from 'antd';
|
import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd';
|
||||||
|
import { useForm } from 'antd/es/form/Form';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
|
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
@@ -12,12 +16,21 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { FullscreenIcon } from 'lucide-react';
|
import {
|
||||||
|
FullscreenIcon,
|
||||||
|
GripVertical,
|
||||||
|
MoveDown,
|
||||||
|
MoveUp,
|
||||||
|
Settings,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -27,6 +40,7 @@ import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { EditMenuAction, ViewMenuAction } from './config';
|
import { EditMenuAction, ViewMenuAction } from './config';
|
||||||
import GridCard from './GridCard';
|
import GridCard from './GridCard';
|
||||||
@@ -45,6 +59,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
layouts,
|
layouts,
|
||||||
setLayouts,
|
setLayouts,
|
||||||
|
panelMap,
|
||||||
|
setPanelMap,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
@@ -65,6 +81,26 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
|
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
|
||||||
|
|
||||||
|
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [currentSelectRowId, setCurrentSelectRowId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [currentPanelMap, setCurrentPanelMap] = useState<
|
||||||
|
Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPanelMap(panelMap);
|
||||||
|
}, [panelMap]);
|
||||||
|
|
||||||
|
const [form] = useForm<{
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
const updateDashboardMutation = useUpdateDashboard();
|
const updateDashboardMutation = useUpdateDashboard();
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@@ -87,7 +123,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDashboardLayout(layouts);
|
setDashboardLayout(sortLayout(layouts));
|
||||||
}, [layouts]);
|
}, [layouts]);
|
||||||
|
|
||||||
const onSaveHandler = (): void => {
|
const onSaveHandler = (): void => {
|
||||||
@@ -97,6 +133,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
...selectedDashboard,
|
...selectedDashboard,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
|
panelMap: { ...currentPanelMap },
|
||||||
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
@@ -106,8 +143,9 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (updatedDashboard.payload) {
|
if (updatedDashboard.payload) {
|
||||||
if (updatedDashboard.payload.data.layout)
|
if (updatedDashboard.payload.data.layout)
|
||||||
setLayouts(updatedDashboard.payload.data.layout);
|
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
featureResponse.refetch();
|
featureResponse.refetch();
|
||||||
@@ -130,7 +168,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
);
|
);
|
||||||
if (!isEqual(filterLayout, filterDashboardLayout)) {
|
if (!isEqual(filterLayout, filterDashboardLayout)) {
|
||||||
setDashboardLayout(layout);
|
const updatedLayout = sortLayout(layout);
|
||||||
|
setDashboardLayout(updatedLayout);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,35 +206,335 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dashboardLayout]);
|
}, [dashboardLayout]);
|
||||||
|
|
||||||
|
function handleAddRow(): void {
|
||||||
|
if (!selectedDashboard) return;
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
|
||||||
|
widgets: [],
|
||||||
|
collapsed: false,
|
||||||
|
};
|
||||||
|
const currentRowIdx = 0;
|
||||||
|
for (let j = currentRowIdx; j < dashboardLayout.length; j++) {
|
||||||
|
if (!currentPanelMap[dashboardLayout[j].i]) {
|
||||||
|
newRowWidgetMap.widgets.push(dashboardLayout[j]);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...selectedDashboard.data,
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
i: id,
|
||||||
|
w: 12,
|
||||||
|
minW: 12,
|
||||||
|
minH: 1,
|
||||||
|
maxH: 1,
|
||||||
|
x: 0,
|
||||||
|
h: 1,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||||
|
],
|
||||||
|
panelMap: { ...currentPanelMap, [id]: newRowWidgetMap },
|
||||||
|
widgets: [
|
||||||
|
...(selectedDashboard.data.widgets || []),
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
title: 'Sample Row',
|
||||||
|
description: '',
|
||||||
|
panelTypes: PANEL_GROUP_TYPES.ROW,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
uuid: selectedDashboard.uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDashboardMutation.mutate(updatedDashboard, {
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onSuccess: (updatedDashboard) => {
|
||||||
|
if (updatedDashboard.payload) {
|
||||||
|
if (updatedDashboard.payload.data.layout)
|
||||||
|
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||||
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
featureResponse.refetch();
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onError: () => {
|
||||||
|
notifications.error({
|
||||||
|
message: SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowSettingsClick = (id: string): void => {
|
||||||
|
setIsSettingsModalOpen(true);
|
||||||
|
setCurrentSelectRowId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSettingsModalSubmit = (): void => {
|
||||||
|
const newTitle = form.getFieldValue('title');
|
||||||
|
if (!selectedDashboard) return;
|
||||||
|
|
||||||
|
if (!currentSelectRowId) return;
|
||||||
|
|
||||||
|
const currentWidget = selectedDashboard?.data?.widgets?.find(
|
||||||
|
(e) => e.id === currentSelectRowId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentWidget) return;
|
||||||
|
|
||||||
|
currentWidget.title = newTitle;
|
||||||
|
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||||
|
(e) => e.id !== currentSelectRowId,
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedWidgets?.push(currentWidget);
|
||||||
|
|
||||||
|
const updatedSelectedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...selectedDashboard.data,
|
||||||
|
widgets: updatedWidgets,
|
||||||
|
},
|
||||||
|
uuid: selectedDashboard.uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||||
|
onSuccess: (updatedDashboard) => {
|
||||||
|
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||||
|
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||||
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
}
|
||||||
|
if (setPanelMap)
|
||||||
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
|
form.setFieldValue('title', '');
|
||||||
|
setIsSettingsModalOpen(false);
|
||||||
|
setCurrentSelectRowId(null);
|
||||||
|
featureResponse.refetch();
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onError: () => {
|
||||||
|
notifications.error({
|
||||||
|
message: SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const handleRowCollapse = (id: string): void => {
|
||||||
|
if (!selectedDashboard) return;
|
||||||
|
const rowProperties = { ...currentPanelMap[id] };
|
||||||
|
const updatedPanelMap = { ...currentPanelMap };
|
||||||
|
|
||||||
|
let updatedDashboardLayout = [...dashboardLayout];
|
||||||
|
if (rowProperties.collapsed === true) {
|
||||||
|
rowProperties.collapsed = false;
|
||||||
|
const widgetsInsideTheRow = rowProperties.widgets;
|
||||||
|
let maxY = 0;
|
||||||
|
widgetsInsideTheRow.forEach((w) => {
|
||||||
|
maxY = Math.max(maxY, w.y + w.h);
|
||||||
|
});
|
||||||
|
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||||
|
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||||
|
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||||
|
|
||||||
|
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||||
|
updatedDashboardLayout[j].y += maxY;
|
||||||
|
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||||
|
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||||
|
updatedDashboardLayout[j].i
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
|
].widgets.map((w) => ({
|
||||||
|
...w,
|
||||||
|
y: w.y + maxY,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||||
|
} else {
|
||||||
|
rowProperties.collapsed = true;
|
||||||
|
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||||
|
|
||||||
|
let widgetsInsideTheRow: Layout[] = [];
|
||||||
|
let isPanelMapUpdated = false;
|
||||||
|
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||||
|
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||||
|
rowProperties.widgets = widgetsInsideTheRow;
|
||||||
|
widgetsInsideTheRow = [];
|
||||||
|
isPanelMapUpdated = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isPanelMapUpdated) {
|
||||||
|
rowProperties.widgets = widgetsInsideTheRow;
|
||||||
|
}
|
||||||
|
let maxY = 0;
|
||||||
|
widgetsInsideTheRow.forEach((w) => {
|
||||||
|
maxY = Math.max(maxY, w.y + w.h);
|
||||||
|
});
|
||||||
|
const currentRowWidget = dashboardLayout[currentIdx];
|
||||||
|
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||||
|
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||||
|
}
|
||||||
|
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||||
|
updatedDashboardLayout[j].y += maxY;
|
||||||
|
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||||
|
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||||
|
updatedDashboardLayout[j].i
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
|
].widgets.map((w) => ({
|
||||||
|
...w,
|
||||||
|
y: w.y + maxY,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||||
|
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setCurrentPanelMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...updatedPanelMap,
|
||||||
|
[id]: {
|
||||||
|
...rowProperties,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
setDashboardLayout(sortLayout(updatedDashboardLayout));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||||
|
if (currentPanelMap[oldItem.i]) {
|
||||||
|
const differenceY = newItem.y - oldItem.y;
|
||||||
|
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
|
||||||
|
...w,
|
||||||
|
y: w.y + differenceY,
|
||||||
|
}));
|
||||||
|
setCurrentPanelMap((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[oldItem.i]: {
|
||||||
|
...prev[oldItem.i],
|
||||||
|
widgets: widgetsInsideRow,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowDelete = (): void => {
|
||||||
|
if (!selectedDashboard) return;
|
||||||
|
|
||||||
|
if (!currentSelectRowId) return;
|
||||||
|
|
||||||
|
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||||
|
(e) => e.id !== currentSelectRowId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedLayout =
|
||||||
|
selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
const updatedPanelMap = { ...currentPanelMap };
|
||||||
|
delete updatedPanelMap[currentSelectRowId];
|
||||||
|
|
||||||
|
const updatedSelectedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
...selectedDashboard.data,
|
||||||
|
widgets: updatedWidgets,
|
||||||
|
layout: updatedLayout,
|
||||||
|
panelMap: updatedPanelMap,
|
||||||
|
},
|
||||||
|
uuid: selectedDashboard.uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||||
|
onSuccess: (updatedDashboard) => {
|
||||||
|
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||||
|
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||||
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
|
}
|
||||||
|
if (setPanelMap)
|
||||||
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setCurrentSelectRowId(null);
|
||||||
|
featureResponse.refetch();
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onError: () => {
|
||||||
|
notifications.error({
|
||||||
|
message: SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonContainer>
|
<Flex justify="flex-end" gap={8} align="center">
|
||||||
<Tooltip title="Open in Full Screen">
|
<FacingIssueBtn
|
||||||
<Button
|
attributes={{
|
||||||
className="periscope-btn"
|
uuid: selectedDashboard?.uuid,
|
||||||
loading={updateDashboardMutation.isLoading}
|
title: data?.title,
|
||||||
onClick={handle.enter}
|
screen: 'Dashboard Details',
|
||||||
icon={<FullscreenIcon size={16} />}
|
}}
|
||||||
disabled={updateDashboardMutation.isLoading}
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
/>
|
buttonText="Need help with this dashboard?"
|
||||||
</Tooltip>
|
message={dashboardHelpMessage(data, selectedDashboard)}
|
||||||
|
onHoverText="Click here to get help for this dashboard"
|
||||||
|
/>
|
||||||
|
<ButtonContainer>
|
||||||
|
<Tooltip title="Open in Full Screen">
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
loading={updateDashboardMutation.isLoading}
|
||||||
|
onClick={handle.enter}
|
||||||
|
icon={<FullscreenIcon size={16} />}
|
||||||
|
disabled={updateDashboardMutation.isLoading}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{!isDashboardLocked && addPanelPermission && (
|
{!isDashboardLocked && addPanelPermission && (
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
onClick={onAddPanelHandler}
|
onClick={onAddPanelHandler}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
data-testid="add-panel"
|
data-testid="add-panel"
|
||||||
>
|
>
|
||||||
{t('dashboard:add_panel')}
|
{t('dashboard:add_panel')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ButtonContainer>
|
{!isDashboardLocked && addPanelPermission && (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={(): void => handleAddRow()}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
data-testid="add-row"
|
||||||
|
>
|
||||||
|
{t('dashboard:add_row')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonContainer>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||||
<ReactGridLayout
|
<ReactGridLayout
|
||||||
cols={12}
|
cols={12}
|
||||||
rowHeight={100}
|
rowHeight={45}
|
||||||
autoSize
|
autoSize
|
||||||
width={100}
|
width={100}
|
||||||
useCSSTransforms
|
useCSSTransforms
|
||||||
@@ -204,6 +543,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
isResizable={!isDashboardLocked && addPanelPermission}
|
isResizable={!isDashboardLocked && addPanelPermission}
|
||||||
allowOverlap={false}
|
allowOverlap={false}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
|
onDragStop={handleDragStop}
|
||||||
draggableHandle=".drag-handle"
|
draggableHandle=".drag-handle"
|
||||||
layout={dashboardLayout}
|
layout={dashboardLayout}
|
||||||
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
||||||
@@ -212,6 +552,58 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
const { i: id } = layout;
|
const { i: id } = layout;
|
||||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||||
|
|
||||||
|
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
|
||||||
|
const rowWidgetProperties = currentPanelMap[id] || {};
|
||||||
|
return (
|
||||||
|
<CardContainer
|
||||||
|
className="row-card"
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
key={id}
|
||||||
|
data-grid={JSON.stringify(currentWidget)}
|
||||||
|
>
|
||||||
|
<div className={cx('row-panel')}>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
disabled={updateDashboardMutation.isLoading}
|
||||||
|
icon={
|
||||||
|
rowWidgetProperties.collapsed ? (
|
||||||
|
<MoveDown size={14} />
|
||||||
|
) : (
|
||||||
|
<MoveUp size={14} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
onClick={(): void => handleRowCollapse(id)}
|
||||||
|
/>
|
||||||
|
<Typography.Text>{currentWidget.title}</Typography.Text>
|
||||||
|
<Button
|
||||||
|
icon={<Settings size={14} />}
|
||||||
|
type="text"
|
||||||
|
onClick={(): void => handleRowSettingsClick(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{rowWidgetProperties.collapsed && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<GripVertical size={14} />}
|
||||||
|
className="drag-handle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!rowWidgetProperties.collapsed && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<Trash2 size={14} />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
setCurrentSelectRowId(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||||
@@ -224,7 +616,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||||
>
|
>
|
||||||
<GridCard
|
<GridCard
|
||||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||||
headerMenuList={widgetActions}
|
headerMenuList={widgetActions}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
version={selectedDashboard?.data?.version}
|
version={selectedDashboard?.data?.version}
|
||||||
@@ -235,6 +627,46 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ReactGridLayout>
|
</ReactGridLayout>
|
||||||
|
<Modal
|
||||||
|
open={isSettingsModalOpen}
|
||||||
|
title="Row Options"
|
||||||
|
destroyOnClose
|
||||||
|
footer={null}
|
||||||
|
onCancel={(): void => {
|
||||||
|
setIsSettingsModalOpen(false);
|
||||||
|
setCurrentSelectRowId(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} onFinish={onSettingsModalSubmit} requiredMark>
|
||||||
|
<Form.Item required name={['title']}>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter row name here..."
|
||||||
|
defaultValue={defaultTo(
|
||||||
|
widgets?.find((widget) => widget.id === currentSelectRowId)
|
||||||
|
?.title as string,
|
||||||
|
'Sample Title',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Apply Changes
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
open={isDeleteModalOpen}
|
||||||
|
title="Delete Row"
|
||||||
|
destroyOnClose
|
||||||
|
onCancel={(): void => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setCurrentSelectRowId(null);
|
||||||
|
}}
|
||||||
|
onOk={(): void => handleRowDelete()}
|
||||||
|
>
|
||||||
|
<Typography.Text>Are you sure you want to delete this row</Typography.Text>
|
||||||
|
</Modal>
|
||||||
</FullScreen>
|
</FullScreen>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
|
|||||||
i: PANEL_TYPES.EMPTY_WIDGET,
|
i: PANEL_TYPES.EMPTY_WIDGET,
|
||||||
w: 6,
|
w: 6,
|
||||||
x: 0,
|
x: 0,
|
||||||
h: 3,
|
h: 6,
|
||||||
y: 0,
|
y: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ interface Props {
|
|||||||
export const CardContainer = styled.div<Props>`
|
export const CardContainer = styled.div<Props>`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
&.row-card {
|
||||||
|
.row-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.enable-resize {
|
&.enable-resize {
|
||||||
:hover {
|
:hover {
|
||||||
.react-resizable-handle {
|
.react-resizable-handle {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const GridPanelSwitch = forwardRef<
|
|||||||
thresholds,
|
thresholds,
|
||||||
},
|
},
|
||||||
[PANEL_TYPES.LIST]: null,
|
[PANEL_TYPES.LIST]: null,
|
||||||
|
[PANEL_TYPES.PIE]: null,
|
||||||
[PANEL_TYPES.TRACE]: null,
|
[PANEL_TYPES.TRACE]: null,
|
||||||
[PANEL_TYPES.BAR]: {
|
[PANEL_TYPES.BAR]: {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type PropsTypePropsMap = {
|
|||||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||||
[PANEL_TYPES.TRACE]: null;
|
[PANEL_TYPES.TRACE]: null;
|
||||||
|
[PANEL_TYPES.PIE]: null;
|
||||||
[PANEL_TYPES.LIST]: null;
|
[PANEL_TYPES.LIST]: null;
|
||||||
[PANEL_TYPES.BAR]: UplotProps & {
|
[PANEL_TYPES.BAR]: UplotProps & {
|
||||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Input, Typography } from 'antd';
|
|||||||
import type { ColumnsType } from 'antd/es/table/interface';
|
import type { ColumnsType } from 'antd/es/table/interface';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import DropDown from 'components/DropDown/DropDown';
|
import DropDown from 'components/DropDown/DropDown';
|
||||||
|
import { listAlertMessage } from 'components/facingIssueBtn/util';
|
||||||
import {
|
import {
|
||||||
DynamicColumnsKey,
|
DynamicColumnsKey,
|
||||||
TableDataSource,
|
TableDataSource,
|
||||||
@@ -337,7 +338,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
<TextToolTip
|
<TextToolTip
|
||||||
{...{
|
{...{
|
||||||
text: `More details on how to create alerts`,
|
text: `More details on how to create alerts`,
|
||||||
url: 'https://signoz.io/docs/userguide/alerts-management/',
|
url:
|
||||||
|
'https://signoz.io/docs/alerts/?utm_source=product&utm_medium=list-alerts',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -358,6 +360,15 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
pagination={{
|
pagination={{
|
||||||
defaultCurrent: Number(paginationParam) || 1,
|
defaultCurrent: Number(paginationParam) || 1,
|
||||||
}}
|
}}
|
||||||
|
facingIssueBtn={{
|
||||||
|
attributes: {
|
||||||
|
screen: 'Alert list page',
|
||||||
|
},
|
||||||
|
eventName: 'Alert: Facing Issues in alert',
|
||||||
|
buttonText: 'Facing issues with alerts?',
|
||||||
|
message: listAlertMessage,
|
||||||
|
onHoverText: 'Click here to get help with alerts',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
|||||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { dashboardListMessage } from 'components/facingIssueBtn/util';
|
||||||
import {
|
import {
|
||||||
DynamicColumnsKey,
|
DynamicColumnsKey,
|
||||||
TableDataSource,
|
TableDataSource,
|
||||||
@@ -321,7 +322,8 @@ function DashboardsList(): JSX.Element {
|
|||||||
<TextToolTip
|
<TextToolTip
|
||||||
{...{
|
{...{
|
||||||
text: `More details on how to create dashboards`,
|
text: `More details on how to create dashboards`,
|
||||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
url:
|
||||||
|
'https://signoz.io/docs/userguide/dashboards?utm_source=product&utm_medium=list-dashboard',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
@@ -385,6 +387,15 @@ function DashboardsList(): JSX.Element {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showSorterTooltip
|
showSorterTooltip
|
||||||
|
facingIssueBtn={{
|
||||||
|
attributes: {
|
||||||
|
screen: 'Dashboard list page',
|
||||||
|
},
|
||||||
|
eventName: 'Dashboard: Facing Issues in dashboard',
|
||||||
|
buttonText: 'Facing issues with dashboards?',
|
||||||
|
message: dashboardListMessage,
|
||||||
|
onHoverText: 'Click here to get help with dashboards',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ function LogControls(): JSX.Element | null {
|
|||||||
logs.map((log) => {
|
logs.map((log) => {
|
||||||
const timestamp =
|
const timestamp =
|
||||||
typeof log.timestamp === 'string'
|
typeof log.timestamp === 'string'
|
||||||
? dayjs(log.timestamp).format()
|
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(log.timestamp / 1e6).format();
|
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
|
|
||||||
return FlatLogData({
|
return FlatLogData({
|
||||||
...log,
|
...log,
|
||||||
|
|||||||
@@ -531,8 +531,8 @@ function LogsExplorerViews({
|
|||||||
logs.map((log) => {
|
logs.map((log) => {
|
||||||
const timestamp =
|
const timestamp =
|
||||||
typeof log.timestamp === 'string'
|
typeof log.timestamp === 'string'
|
||||||
? dayjs(log.timestamp).format()
|
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(log.timestamp / 1e6).format();
|
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||||
|
|
||||||
return FlatLogData({
|
return FlatLogData({
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -608,6 +608,7 @@ function LogsExplorerViews({
|
|||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
onClick={handleToggleShowFormatOptions}
|
onClick={handleToggleShowFormatOptions}
|
||||||
icon={<Sliders size={14} />}
|
icon={<Sliders size={14} />}
|
||||||
|
data-testid="periscope-btn"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showFormatMenuItems && (
|
{showFormatMenuItems && (
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { render, RenderResult } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
|
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||||
|
import { server } from 'mocks-server/server';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||||
|
import i18n from 'ReactI18';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
import LogsExplorerViews from '..';
|
||||||
|
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
||||||
|
|
||||||
|
const logExplorerRoute = '/logs/logs-explorer';
|
||||||
|
|
||||||
|
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||||
|
|
||||||
|
const lodsQueryServerRequest = (): void =>
|
||||||
|
server.use(
|
||||||
|
rest.post(queryRangeURL, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// mocking the graph components in this test as this should be handled separately
|
||||||
|
jest.mock(
|
||||||
|
'container/TimeSeriesView/TimeSeriesView',
|
||||||
|
() =>
|
||||||
|
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||||
|
function () {
|
||||||
|
return <div>Time Series Chart</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
jest.mock(
|
||||||
|
'container/LogsExplorerChart',
|
||||||
|
() =>
|
||||||
|
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||||
|
function () {
|
||||||
|
return <div>Histogram Chart</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('constants/panelTypes', () => ({
|
||||||
|
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('d3-interpolate', () => ({
|
||||||
|
interpolate: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useGetExplorerQueryRange: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
|
||||||
|
beforeEach(() => {
|
||||||
|
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||||
|
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderer = (): RenderResult =>
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={[logExplorerRoute]}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<MockQueryClientProvider>
|
||||||
|
<QueryBuilderProvider>
|
||||||
|
<VirtuosoMockContext.Provider
|
||||||
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
|
>
|
||||||
|
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
|
||||||
|
</VirtuosoMockContext.Provider>
|
||||||
|
</QueryBuilderProvider>
|
||||||
|
</MockQueryClientProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('LogsExplorerViews -', () => {
|
||||||
|
it('render correctly with props - list and table', async () => {
|
||||||
|
lodsQueryServerRequest();
|
||||||
|
const { queryByText, queryByTestId } = renderer();
|
||||||
|
|
||||||
|
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
|
||||||
|
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
|
||||||
|
|
||||||
|
expect(document.querySelector('.menu-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const menuItems = document.querySelectorAll('.menu-items .item');
|
||||||
|
expect(menuItems.length).toBe(3);
|
||||||
|
|
||||||
|
// switch to table view
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'{"container_id":"container_id","container_name":"container_name","driver":"driver","eta":"2m0s","location":"frontend","log_level":"INFO","message":"Dispatch successful","service":"frontend","span_id":"span_id","trace_id":"span_id"}',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check isLoading state', async () => {
|
||||||
|
lodsQueryServerRequest();
|
||||||
|
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||||
|
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||||
|
isLoading: true,
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
const { queryByText, queryByTestId } = renderer();
|
||||||
|
|
||||||
|
// switch to table view
|
||||||
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check error state', async () => {
|
||||||
|
lodsQueryServerRequest();
|
||||||
|
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||||
|
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||||
|
isLoading: false,
|
||||||
|
isFetching: false,
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
const { queryByText, queryByTestId } = renderer();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText('Something went wrong. Please try again or contact support.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch to table view
|
||||||
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText('Something went wrong. Please try again or contact support.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal file
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export const logsQueryRangeSuccessNewFormatResponse = {
|
||||||
|
data: {
|
||||||
|
result: [],
|
||||||
|
resultType: '',
|
||||||
|
newResult: {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
resultType: '',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
queryName: 'A',
|
||||||
|
series: null,
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
timestamp: '2024-02-15T21:20:22Z',
|
||||||
|
data: {
|
||||||
|
attributes_bool: {},
|
||||||
|
attributes_float64: {},
|
||||||
|
attributes_int64: {},
|
||||||
|
attributes_string: {
|
||||||
|
container_id: 'container_id',
|
||||||
|
container_name: 'container_name',
|
||||||
|
driver: 'driver',
|
||||||
|
eta: '2m0s',
|
||||||
|
location: 'frontend',
|
||||||
|
log_level: 'INFO',
|
||||||
|
message: 'Dispatch successful',
|
||||||
|
service: 'frontend',
|
||||||
|
span_id: 'span_id',
|
||||||
|
trace_id: 'span_id',
|
||||||
|
},
|
||||||
|
body:
|
||||||
|
'2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
|
||||||
|
id: 'id',
|
||||||
|
resources_string: {
|
||||||
|
'container.name': 'container_name',
|
||||||
|
},
|
||||||
|
severity_number: 0,
|
||||||
|
severity_text: '',
|
||||||
|
span_id: '',
|
||||||
|
trace_flags: 0,
|
||||||
|
trace_id: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
|
||||||
import { DownloadOptions } from 'container/Download/Download.types';
|
import { DownloadOptions } from 'container/Download/Download.types';
|
||||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ export enum FORMULA {
|
|||||||
ERROR_PERCENTAGE = 'A*100/B',
|
ERROR_PERCENTAGE = 'A*100/B',
|
||||||
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
||||||
APDEX_TRACES = '((B + C)/2)/A',
|
APDEX_TRACES = '((B + C)/2)/A',
|
||||||
APDEX_DELTA_SPAN_METRICS = '(B + C/2)/A',
|
APDEX_DELTA_SPAN_METRICS = '((B + C)/2)/A',
|
||||||
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export const getNearestHighestBucketValue = (
|
|||||||
value: number,
|
value: number,
|
||||||
buckets: number[],
|
buckets: number[],
|
||||||
): string => {
|
): string => {
|
||||||
|
// sort the buckets
|
||||||
|
buckets.sort((a, b) => a - b);
|
||||||
const nearestBucket = buckets.find((bucket) => bucket >= value);
|
const nearestBucket = buckets.find((bucket) => bucket >= value);
|
||||||
return nearestBucket?.toString() || '+Inf';
|
return nearestBucket?.toString() || '+Inf';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const PANEL_TYPES_INITIAL_QUERY = {
|
|||||||
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
||||||
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
||||||
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,146 +1,59 @@
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
||||||
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 { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import {
|
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
|
||||||
listViewInitialLogQuery,
|
|
||||||
listViewInitialTraceQuery,
|
|
||||||
PANEL_TYPES_INITIAL_QUERY,
|
|
||||||
} from './constants';
|
|
||||||
import menuItems from './menuItems';
|
import menuItems from './menuItems';
|
||||||
import { Card, Container, Text } from './styles';
|
import { Card, Container, Text } from './styles';
|
||||||
|
|
||||||
function DashboardGraphSlider(): JSX.Element {
|
function DashboardGraphSlider(): JSX.Element {
|
||||||
const {
|
const { handleToggleDashboardSlider } = useDashboard();
|
||||||
handleToggleDashboardSlider,
|
|
||||||
layouts,
|
|
||||||
selectedDashboard,
|
|
||||||
} = useDashboard();
|
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const updateDashboardMutation = useUpdateDashboard();
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
|
handleToggleDashboardSlider(false);
|
||||||
updateDashboardMutation.mutateAsync(
|
const queryParamsLog = {
|
||||||
{
|
graphType: name,
|
||||||
uuid: selectedDashboard?.uuid || '',
|
widgetId: id,
|
||||||
data: {
|
[QueryParams.compositeQuery]: JSON.stringify({
|
||||||
title: data?.title || '',
|
...PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
variables: data?.variables || {},
|
builder: {
|
||||||
description: data?.description || '',
|
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
||||||
name: data?.name || '',
|
queryData: [
|
||||||
tags: data?.tags || [],
|
|
||||||
version: data?.version || 'v3',
|
|
||||||
layout: [
|
|
||||||
{
|
{
|
||||||
i: id,
|
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
||||||
w: 6,
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
x: 0,
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
h: 3,
|
offset: 0,
|
||||||
y: 0,
|
pageSize: 100,
|
||||||
},
|
|
||||||
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
|
||||||
[]),
|
|
||||||
],
|
|
||||||
widgets: [
|
|
||||||
...(data?.widgets || []),
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
isStacked: false,
|
|
||||||
nullZeroValues: '',
|
|
||||||
opacity: '',
|
|
||||||
panelTypes: name,
|
|
||||||
query:
|
|
||||||
name === PANEL_TYPES.LIST
|
|
||||||
? listViewInitialLogQuery
|
|
||||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
timePreferance: 'GLOBAL_TIME',
|
|
||||||
softMax: null,
|
|
||||||
softMin: null,
|
|
||||||
selectedLogFields: [
|
|
||||||
{
|
|
||||||
dataType: 'string',
|
|
||||||
type: '',
|
|
||||||
name: 'body',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: 'string',
|
|
||||||
type: '',
|
|
||||||
name: 'timestamp',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectedTracesFields: [
|
|
||||||
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
};
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data.payload) {
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
const queryParamsLog = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify({
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
builder: {
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
graphType: name,
|
graphType: name,
|
||||||
widgetId: id,
|
widgetId: id,
|
||||||
[QueryParams.compositeQuery]: JSON.stringify(
|
[QueryParams.compositeQuery]: JSON.stringify(
|
||||||
PANEL_TYPES_INITIAL_QUERY[name],
|
PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (name === PANEL_TYPES.LIST) {
|
if (name === PANEL_TYPES.LIST) {
|
||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
notifications.success({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { BarChart3, LineChart, List, SigmaSquare, Table } from 'lucide-react';
|
import {
|
||||||
|
BarChart3,
|
||||||
|
LineChart,
|
||||||
|
List,
|
||||||
|
PieChart,
|
||||||
|
SigmaSquare,
|
||||||
|
Table,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Items: ItemsProps[] = [
|
const Items: ItemsProps[] = [
|
||||||
{
|
{
|
||||||
@@ -28,6 +35,11 @@ const Items: ItemsProps[] = [
|
|||||||
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
|
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
|
||||||
display: 'Bar',
|
display: 'Bar',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: PANEL_TYPES.PIE,
|
||||||
|
icon: <PieChart size={32} color={Color.BG_ROBIN_400} />,
|
||||||
|
display: 'Pie',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ItemsProps {
|
export interface ItemsProps {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Row } from 'antd';
|
import { Row } from 'antd';
|
||||||
|
import { isNull } from 'lodash-es';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import { convertVariablesToDbFormat } from './util';
|
|
||||||
import VariableItem from './VariableItem';
|
import VariableItem from './VariableItem';
|
||||||
|
|
||||||
function DashboardVariableSelection(): JSX.Element | null {
|
function DashboardVariableSelection(): JSX.Element | null {
|
||||||
@@ -11,15 +11,14 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
|
variablesToGetUpdated,
|
||||||
|
setVariablesToGetUpdated,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
const { data } = selectedDashboard || {};
|
||||||
|
|
||||||
const { variables } = data || {};
|
const { variables } = data || {};
|
||||||
|
|
||||||
const [update, setUpdate] = useState<boolean>(false);
|
|
||||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
|
||||||
|
|
||||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,8 +44,27 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
}, [variables]);
|
}, [variables]);
|
||||||
|
|
||||||
const onVarChanged = (name: string): void => {
|
const onVarChanged = (name: string): void => {
|
||||||
setLastUpdatedVar(name);
|
/**
|
||||||
setUpdate(!update);
|
* this function takes care of adding the dependent variables to current update queue and removing
|
||||||
|
* the updated variable name from the queue
|
||||||
|
*/
|
||||||
|
const dependentVariables = variablesTableData
|
||||||
|
?.map((variable: any) => {
|
||||||
|
if (variable.type === 'QUERY') {
|
||||||
|
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||||
|
const queryValue = variable.queryValue || '';
|
||||||
|
const dependVarReMatch = queryValue.match(re);
|
||||||
|
if (dependVarReMatch !== null && dependVarReMatch.length > 0) {
|
||||||
|
return variable.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((val: string | null) => !isNull(val));
|
||||||
|
setVariablesToGetUpdated((prev) => [
|
||||||
|
...prev.filter((v) => v !== name),
|
||||||
|
...dependentVariables,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onValueUpdate = (
|
const onValueUpdate = (
|
||||||
@@ -54,39 +72,46 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
id: string,
|
id: string,
|
||||||
value: IDashboardVariable['selectedValue'],
|
value: IDashboardVariable['selectedValue'],
|
||||||
allSelected: boolean,
|
allSelected: boolean,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): void => {
|
): void => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const newVariablesArr = variablesTableData.map(
|
|
||||||
(variable: IDashboardVariable) => {
|
|
||||||
const variableCopy = { ...variable };
|
|
||||||
|
|
||||||
if (variableCopy.id === id) {
|
|
||||||
variableCopy.selectedValue = value;
|
|
||||||
variableCopy.allSelected = allSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return variableCopy;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
updateLocalStorageDashboardVariables(name, value, allSelected);
|
updateLocalStorageDashboardVariables(name, value, allSelected);
|
||||||
|
|
||||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
|
||||||
|
|
||||||
if (selectedDashboard) {
|
if (selectedDashboard) {
|
||||||
setSelectedDashboard({
|
setSelectedDashboard((prev) => {
|
||||||
...selectedDashboard,
|
if (prev) {
|
||||||
data: {
|
const oldVariables = prev?.data.variables;
|
||||||
...selectedDashboard?.data,
|
// this is added to handle case where we have two different
|
||||||
variables: {
|
// schemas for variable response
|
||||||
...variables,
|
if (oldVariables[id]) {
|
||||||
},
|
oldVariables[id] = {
|
||||||
},
|
...oldVariables[id],
|
||||||
|
selectedValue: value,
|
||||||
|
allSelected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (oldVariables[name]) {
|
||||||
|
oldVariables[name] = {
|
||||||
|
...oldVariables[name],
|
||||||
|
selectedValue: value,
|
||||||
|
allSelected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
data: {
|
||||||
|
...prev?.data,
|
||||||
|
variables: {
|
||||||
|
...oldVariables,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onVarChanged(name);
|
onVarChanged(name);
|
||||||
|
|
||||||
setUpdate(!update);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,13 +132,12 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
<VariableItem
|
<VariableItem
|
||||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||||
existingVariables={variables}
|
existingVariables={variables}
|
||||||
lastUpdatedVar={lastUpdatedVar}
|
|
||||||
variableData={{
|
variableData={{
|
||||||
name: variable.name,
|
name: variable.name,
|
||||||
...variable,
|
...variable,
|
||||||
change: update,
|
|
||||||
}}
|
}}
|
||||||
onValueUpdate={onValueUpdate}
|
onValueUpdate={onValueUpdate}
|
||||||
|
variablesToGetUpdated={variablesToGetUpdated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -68,7 +68,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -82,7 +82,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -110,7 +110,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -131,7 +131,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={customVariableData}
|
variableData={customVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -146,7 +146,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface VariableItemProps {
|
|||||||
arg1: IDashboardVariable['selectedValue'],
|
arg1: IDashboardVariable['selectedValue'],
|
||||||
allSelected: boolean,
|
allSelected: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
lastUpdatedVar: string;
|
variablesToGetUpdated: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSelectValue = (
|
const getSelectValue = (
|
||||||
@@ -49,7 +49,7 @@ function VariableItem({
|
|||||||
variableData,
|
variableData,
|
||||||
existingVariables,
|
existingVariables,
|
||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
lastUpdatedVar,
|
variablesToGetUpdated,
|
||||||
}: VariableItemProps): JSX.Element {
|
}: VariableItemProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
[],
|
[],
|
||||||
@@ -108,16 +108,10 @@ function VariableItem({
|
|||||||
|
|
||||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
|
||||||
// If the variable is dependent on the last updated variable
|
|
||||||
// and contains the last updated variable in its query (of the form `{{.var}}`)
|
|
||||||
// then we need to update the value of the variable
|
|
||||||
const queryValue = variableData.queryValue || '';
|
|
||||||
const dependVarReMatch = queryValue.match(re);
|
|
||||||
if (
|
if (
|
||||||
variableData.type === 'QUERY' &&
|
variableData.type === 'QUERY' &&
|
||||||
dependVarReMatch !== null &&
|
variableData.name &&
|
||||||
dependVarReMatch.length > 0
|
variablesToGetUpdated.includes(variableData.name)
|
||||||
) {
|
) {
|
||||||
let value = variableData.selectedValue;
|
let value = variableData.selectedValue;
|
||||||
let allSelected = false;
|
let allSelected = false;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
.prom-ql-icon {
|
.prom-ql-icon {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
color: var(--bg-vanilla-200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ant-btn-default {
|
.ant-btn-default {
|
||||||
@@ -54,6 +55,10 @@
|
|||||||
.ant-tabs-tab-active {
|
.ant-tabs-tab-active {
|
||||||
.nav-btns {
|
.nav-btns {
|
||||||
background: var(--bg-vanilla-300) !important;
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
|
||||||
|
.prom-ql-icon {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import './QuerySection.styles.scss';
|
import './QuerySection.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||||
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
|
import { getDefaultWidgetData } from 'container/NewWidget/utils';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
@@ -51,12 +56,17 @@ function QuerySection({
|
|||||||
|
|
||||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const { widgets } = selectedDashboard?.data || {};
|
const { widgets } = selectedDashboard?.data || {};
|
||||||
|
|
||||||
const getWidget = useCallback(() => {
|
const getWidget = useCallback(() => {
|
||||||
const widgetId = urlQuery.get('widgetId');
|
const widgetId = urlQuery.get('widgetId');
|
||||||
return widgets?.find((e) => e.id === widgetId);
|
return defaultTo(
|
||||||
}, [widgets, urlQuery]);
|
widgets?.find((e) => e.id === widgetId),
|
||||||
|
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||||
|
);
|
||||||
|
}, [urlQuery, widgets, selectedGraph]);
|
||||||
|
|
||||||
const selectedWidget = getWidget() as Widgets;
|
const selectedWidget = getWidget() as Widgets;
|
||||||
|
|
||||||
@@ -191,7 +201,9 @@ function QuerySection({
|
|||||||
label: (
|
label: (
|
||||||
<Tooltip title="PromQL">
|
<Tooltip title="PromQL">
|
||||||
<Button className="nav-btns">
|
<Button className="nav-btns">
|
||||||
<img src="/Icons/promQL.svg" alt="Prom Ql" className="prom-ql-icon" />
|
<PromQLIcon
|
||||||
|
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -223,7 +235,10 @@ function QuerySection({
|
|||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
<TextToolTip
|
||||||
|
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
||||||
|
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
loading={queryResponse.isFetching}
|
loading={queryResponse.isFetching}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -67,12 +67,13 @@ function LeftContainer({
|
|||||||
setRequestData((prev) => ({
|
setRequestData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedTime: selectedTime.enum || prev.selectedTime,
|
selectedTime: selectedTime.enum || prev.selectedTime,
|
||||||
|
globalSelectedInterval,
|
||||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||||
query: stagedQuery,
|
query: stagedQuery,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [stagedQuery, selectedTime]);
|
}, [stagedQuery, selectedTime, globalSelectedInterval]);
|
||||||
|
|
||||||
const queryResponse = useGetQueryRange(
|
const queryResponse = useGetQueryRange(
|
||||||
requestData,
|
requestData,
|
||||||
|
|||||||
4
frontend/src/container/NewWidget/NewWidget.styles.scss
Normal file
4
frontend/src/container/NewWidget/NewWidget.styles.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.facing-issue-btn-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr max-content;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.VALUE]: true,
|
[PANEL_TYPES.VALUE]: true,
|
||||||
[PANEL_TYPES.TABLE]: true,
|
[PANEL_TYPES.TABLE]: true,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.BAR]: true,
|
[PANEL_TYPES.BAR]: true,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
@@ -36,6 +37,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.VALUE]: false,
|
[PANEL_TYPES.VALUE]: false,
|
||||||
[PANEL_TYPES.TABLE]: false,
|
[PANEL_TYPES.TABLE]: false,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.BAR]: true,
|
[PANEL_TYPES.BAR]: true,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
@@ -45,6 +47,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.TIME_SERIES]: false,
|
[PANEL_TYPES.TIME_SERIES]: false,
|
||||||
[PANEL_TYPES.VALUE]: true,
|
[PANEL_TYPES.VALUE]: true,
|
||||||
[PANEL_TYPES.TABLE]: true,
|
[PANEL_TYPES.TABLE]: true,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
[PANEL_TYPES.BAR]: false,
|
[PANEL_TYPES.BAR]: false,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
@@ -56,6 +59,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.VALUE]: false,
|
[PANEL_TYPES.VALUE]: false,
|
||||||
[PANEL_TYPES.TABLE]: false,
|
[PANEL_TYPES.TABLE]: false,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.BAR]: false,
|
[PANEL_TYPES.BAR]: false,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
@@ -66,6 +70,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.VALUE]: true,
|
[PANEL_TYPES.VALUE]: true,
|
||||||
[PANEL_TYPES.TABLE]: true,
|
[PANEL_TYPES.TABLE]: true,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.BAR]: true,
|
[PANEL_TYPES.BAR]: true,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
@@ -76,6 +81,7 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = {
|
|||||||
[PANEL_TYPES.VALUE]: true,
|
[PANEL_TYPES.VALUE]: true,
|
||||||
[PANEL_TYPES.TABLE]: false,
|
[PANEL_TYPES.TABLE]: false,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
[PANEL_TYPES.BAR]: true,
|
[PANEL_TYPES.BAR]: true,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
@@ -88,6 +94,7 @@ export const panelTypeVsPanelTimePreferences: {
|
|||||||
[PANEL_TYPES.VALUE]: true,
|
[PANEL_TYPES.VALUE]: true,
|
||||||
[PANEL_TYPES.TABLE]: true,
|
[PANEL_TYPES.TABLE]: true,
|
||||||
[PANEL_TYPES.LIST]: false,
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: true,
|
||||||
[PANEL_TYPES.BAR]: true,
|
[PANEL_TYPES.BAR]: true,
|
||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './NewWidget.styles.scss';
|
||||||
|
|
||||||
import { LockFilled, WarningOutlined } from '@ant-design/icons';
|
import { LockFilled, WarningOutlined } from '@ant-design/icons';
|
||||||
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
|
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
|
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@@ -14,6 +18,7 @@ import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { defaultTo, isUndefined } from 'lodash-es';
|
||||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +50,11 @@ import {
|
|||||||
RightContainerWrapper,
|
RightContainerWrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import { NewWidgetProps } from './types';
|
import { NewWidgetProps } from './types';
|
||||||
import { getIsQueryModified, handleQueryChange } from './utils';
|
import {
|
||||||
|
getDefaultWidgetData,
|
||||||
|
getIsQueryModified,
|
||||||
|
handleQueryChange,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -80,10 +89,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
|
|
||||||
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
||||||
|
|
||||||
|
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const widgetId = query.get('widgetId');
|
||||||
|
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||||
|
const isWidgetNotPresent = isUndefined(selectedWidget);
|
||||||
|
if (isWidgetNotPresent) {
|
||||||
|
setIsNewDashboard(true);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getWidget = useCallback(() => {
|
const getWidget = useCallback(() => {
|
||||||
const widgetId = query.get('widgetId');
|
const widgetId = query.get('widgetId');
|
||||||
return widgets?.find((e) => e.id === widgetId);
|
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||||
}, [query, widgets]);
|
return defaultTo(
|
||||||
|
selectedWidget,
|
||||||
|
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||||
|
) as Widgets;
|
||||||
|
}, [query, selectedGraph, widgets]);
|
||||||
|
|
||||||
const [selectedWidget, setSelectedWidget] = useState(getWidget());
|
const [selectedWidget, setSelectedWidget] = useState(getWidget());
|
||||||
|
|
||||||
@@ -227,33 +252,70 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const widgetId = query.get('widgetId');
|
||||||
|
let updatedLayout = selectedDashboard.data.layout || [];
|
||||||
|
if (isNewDashboard) {
|
||||||
|
updatedLayout = [
|
||||||
|
{
|
||||||
|
i: widgetId || '',
|
||||||
|
w: 6,
|
||||||
|
x: 0,
|
||||||
|
h: 6,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
...updatedLayout,
|
||||||
|
];
|
||||||
|
}
|
||||||
const dashboard: Dashboard = {
|
const dashboard: Dashboard = {
|
||||||
...selectedDashboard,
|
...selectedDashboard,
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.uuid,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
widgets: [
|
widgets: isNewDashboard
|
||||||
...preWidgets,
|
? [
|
||||||
{
|
...afterWidgets,
|
||||||
...(selectedWidget || ({} as Widgets)),
|
{
|
||||||
description: selectedWidget?.description || '',
|
...(selectedWidget || ({} as Widgets)),
|
||||||
timePreferance: selectedTime.enum,
|
description: selectedWidget?.description || '',
|
||||||
isStacked: selectedWidget?.isStacked || false,
|
timePreferance: selectedTime.enum,
|
||||||
opacity: selectedWidget?.opacity || '1',
|
isStacked: selectedWidget?.isStacked || false,
|
||||||
nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
|
opacity: selectedWidget?.opacity || '1',
|
||||||
title: selectedWidget?.title,
|
nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
|
||||||
yAxisUnit: selectedWidget?.yAxisUnit,
|
title: selectedWidget?.title,
|
||||||
panelTypes: graphType,
|
yAxisUnit: selectedWidget?.yAxisUnit,
|
||||||
query: currentQuery,
|
panelTypes: graphType,
|
||||||
thresholds: selectedWidget?.thresholds,
|
query: currentQuery,
|
||||||
softMin: selectedWidget?.softMin || 0,
|
thresholds: selectedWidget?.thresholds,
|
||||||
softMax: selectedWidget?.softMax || 0,
|
softMin: selectedWidget?.softMin || 0,
|
||||||
fillSpans: selectedWidget?.fillSpans,
|
softMax: selectedWidget?.softMax || 0,
|
||||||
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
fillSpans: selectedWidget?.fillSpans,
|
||||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||||
},
|
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||||
...afterWidgets,
|
},
|
||||||
],
|
]
|
||||||
|
: [
|
||||||
|
...preWidgets,
|
||||||
|
{
|
||||||
|
...(selectedWidget || ({} as Widgets)),
|
||||||
|
description: selectedWidget?.description || '',
|
||||||
|
timePreferance: selectedTime.enum,
|
||||||
|
isStacked: selectedWidget?.isStacked || false,
|
||||||
|
opacity: selectedWidget?.opacity || '1',
|
||||||
|
nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
|
||||||
|
title: selectedWidget?.title,
|
||||||
|
yAxisUnit: selectedWidget?.yAxisUnit,
|
||||||
|
panelTypes: graphType,
|
||||||
|
query: currentQuery,
|
||||||
|
thresholds: selectedWidget?.thresholds,
|
||||||
|
softMin: selectedWidget?.softMin || 0,
|
||||||
|
softMax: selectedWidget?.softMax || 0,
|
||||||
|
fillSpans: selectedWidget?.fillSpans,
|
||||||
|
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||||
|
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||||
|
},
|
||||||
|
...afterWidgets,
|
||||||
|
],
|
||||||
|
layout: [...updatedLayout],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -274,6 +336,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
|
query,
|
||||||
|
isNewDashboard,
|
||||||
preWidgets,
|
preWidgets,
|
||||||
selectedWidget,
|
selectedWidget,
|
||||||
selectedTime.enum,
|
selectedTime.enum,
|
||||||
@@ -363,33 +427,49 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ButtonContainer>
|
<div className="facing-issue-btn-container">
|
||||||
{isSaveDisabled && (
|
<FacingIssueBtn
|
||||||
<Tooltip title={MESSAGE.PANEL}>
|
attributes={{
|
||||||
|
uuid: selectedDashboard?.uuid,
|
||||||
|
title: selectedDashboard?.data.title,
|
||||||
|
panelType: graphType,
|
||||||
|
widgetId: query.get('widgetId'),
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
screen: 'Dashboard list page',
|
||||||
|
}}
|
||||||
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
|
buttonText="Need help with this chart?"
|
||||||
|
message={chartHelpMessage(selectedDashboard, graphType)}
|
||||||
|
onHoverText="Click here to get help in creating chart"
|
||||||
|
/>
|
||||||
|
<ButtonContainer>
|
||||||
|
{isSaveDisabled && (
|
||||||
|
<Tooltip title={MESSAGE.PANEL}>
|
||||||
|
<Button
|
||||||
|
icon={<LockFilled />}
|
||||||
|
type="primary"
|
||||||
|
disabled={isSaveDisabled}
|
||||||
|
onClick={onSaveDashboard}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSaveDisabled && (
|
||||||
<Button
|
<Button
|
||||||
icon={<LockFilled />}
|
|
||||||
type="primary"
|
type="primary"
|
||||||
|
data-testid="new-widget-save"
|
||||||
|
loading={updateDashboardMutation.isLoading}
|
||||||
disabled={isSaveDisabled}
|
disabled={isSaveDisabled}
|
||||||
onClick={onSaveDashboard}
|
onClick={onSaveDashboard}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
||||||
|
</ButtonContainer>
|
||||||
{!isSaveDisabled && (
|
</div>
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
data-testid="new-widget-save"
|
|
||||||
loading={updateDashboardMutation.isLoading}
|
|
||||||
disabled={isSaveDisabled}
|
|
||||||
onClick={onSaveDashboard}
|
|
||||||
>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
|
||||||
</ButtonContainer>
|
|
||||||
|
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<LeftContainerWrapper flex={5}>
|
<LeftContainerWrapper flex={5}>
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import {
|
|||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
listViewInitialLogQuery,
|
||||||
|
listViewInitialTraceQuery,
|
||||||
|
PANEL_TYPES_INITIAL_QUERY,
|
||||||
|
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||||
import { isEqual, set, unset } from 'lodash-es';
|
import { isEqual, set, unset } from 'lodash-es';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
@@ -25,6 +31,7 @@ export type PartialPanelTypes = {
|
|||||||
[PANEL_TYPES.TABLE]: 'table';
|
[PANEL_TYPES.TABLE]: 'table';
|
||||||
[PANEL_TYPES.TIME_SERIES]: 'graph';
|
[PANEL_TYPES.TIME_SERIES]: 'graph';
|
||||||
[PANEL_TYPES.VALUE]: 'value';
|
[PANEL_TYPES.VALUE]: 'value';
|
||||||
|
[PANEL_TYPES.PIE]: 'pie';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const panelTypeDataSourceFormValuesMap: Record<
|
export const panelTypeDataSourceFormValuesMap: Record<
|
||||||
@@ -43,6 +50,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -58,6 +68,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -71,6 +84,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -87,6 +103,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -102,6 +121,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -115,6 +137,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -131,6 +156,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -146,6 +174,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'orderBy',
|
'orderBy',
|
||||||
'functions',
|
'functions',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -159,6 +190,62 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[PANEL_TYPES.PIE]: {
|
||||||
|
[DataSource.LOGS]: {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
'filters',
|
||||||
|
'aggregateOperator',
|
||||||
|
'aggregateAttribute',
|
||||||
|
'groupBy',
|
||||||
|
'limit',
|
||||||
|
'having',
|
||||||
|
'orderBy',
|
||||||
|
'functions',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[DataSource.METRICS]: {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
'filters',
|
||||||
|
'aggregateOperator',
|
||||||
|
'aggregateAttribute',
|
||||||
|
'groupBy',
|
||||||
|
'limit',
|
||||||
|
'having',
|
||||||
|
'orderBy',
|
||||||
|
'functions',
|
||||||
|
'spaceAggregation',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[DataSource.TRACES]: {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
'filters',
|
||||||
|
'aggregateOperator',
|
||||||
|
'aggregateAttribute',
|
||||||
|
'groupBy',
|
||||||
|
'limit',
|
||||||
|
'having',
|
||||||
|
'orderBy',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -190,6 +277,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'reduceTo',
|
'reduceTo',
|
||||||
'having',
|
'having',
|
||||||
'functions',
|
'functions',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -203,6 +293,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'reduceTo',
|
'reduceTo',
|
||||||
'functions',
|
'functions',
|
||||||
'spaceAggregation',
|
'spaceAggregation',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -216,6 +309,9 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
|||||||
'limit',
|
'limit',
|
||||||
'having',
|
'having',
|
||||||
'orderBy',
|
'orderBy',
|
||||||
|
'queryName',
|
||||||
|
'expression',
|
||||||
|
'disabled',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -257,3 +353,38 @@ export function handleQueryChange(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDefaultWidgetData = (
|
||||||
|
id: string,
|
||||||
|
name: PANEL_TYPES,
|
||||||
|
): Widgets => ({
|
||||||
|
id,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
isStacked: false,
|
||||||
|
nullZeroValues: '',
|
||||||
|
opacity: '',
|
||||||
|
panelTypes: name,
|
||||||
|
query:
|
||||||
|
name === PANEL_TYPES.LIST
|
||||||
|
? listViewInitialLogQuery
|
||||||
|
: PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
|
timePreferance: 'GLOBAL_TIME',
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
selectedLogFields: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'timestamp',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedTracesFields: [
|
||||||
|
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default function EnvironmentDetails(): JSX.Element {
|
|||||||
|
|
||||||
<div className="request-entity-container">
|
<div className="request-entity-container">
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
Cannot find what you’re looking for? Request a data source
|
Cannot find what you’re looking for? Request an environment
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
LeftCircleOutlined,
|
LeftCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Space, Steps, Typography } from 'antd';
|
import { Button, Space, Steps, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||||
@@ -17,6 +18,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
|||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isEmpty, isNull } from 'lodash-es';
|
import { isEmpty, isNull } from 'lodash-es';
|
||||||
|
import { HelpCircle } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||||
@@ -379,6 +381,31 @@ export default function ModuleStepsContainer({
|
|||||||
history.push('/');
|
history.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFacingIssuesClick = (): void => {
|
||||||
|
logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', {
|
||||||
|
dataSource: selectedDataSource?.id,
|
||||||
|
framework: selectedFramework,
|
||||||
|
environment: selectedEnvironment,
|
||||||
|
module: activeStep?.module?.id,
|
||||||
|
step: activeStep?.step?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = `Hi Team,
|
||||||
|
|
||||||
|
I am facing issues sending data to SigNoz. Here are my application details
|
||||||
|
|
||||||
|
Data Source: ${selectedDataSource?.name}
|
||||||
|
Framework:
|
||||||
|
Environment:
|
||||||
|
Module: ${activeStep?.module?.id}
|
||||||
|
|
||||||
|
Thanks
|
||||||
|
`;
|
||||||
|
if (window.Intercom) {
|
||||||
|
window.Intercom('showNewMessage', message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="onboarding-module-steps">
|
<div className="onboarding-module-steps">
|
||||||
<div className="steps-container">
|
<div className="steps-container">
|
||||||
@@ -455,6 +482,15 @@ export default function ModuleStepsContainer({
|
|||||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={handleFacingIssuesClick}
|
||||||
|
danger
|
||||||
|
icon={<HelpCircle size={14} />}
|
||||||
|
>
|
||||||
|
Facing issues sending data to SigNoz?
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.piechart-no-data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.piechart-container {
|
||||||
|
height: 90%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.piechart-tooltip {
|
||||||
|
|
||||||
|
.piechart-indicator {
|
||||||
|
width: 15px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.piechart-legend {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.piechart-legend-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.piechart-legend-label {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
218
frontend/src/container/PanelWrapper/PiePanelWrapper.tsx
Normal file
218
frontend/src/container/PanelWrapper/PiePanelWrapper.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import './PiePanelWrapper.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Group } from '@visx/group';
|
||||||
|
import { Pie } from '@visx/shape';
|
||||||
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
|
||||||
|
import { getLabel, lightenColor, tooltipStyles } from './utils';
|
||||||
|
|
||||||
|
// refernce: https://www.youtube.com/watch?v=bL3P9CqQkKw
|
||||||
|
function PiePanelWrapper({
|
||||||
|
queryResponse,
|
||||||
|
widget,
|
||||||
|
}: PanelWrapperProps): JSX.Element {
|
||||||
|
const [active, setActive] = useState<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
tooltipOpen,
|
||||||
|
tooltipLeft,
|
||||||
|
tooltipTop,
|
||||||
|
tooltipData,
|
||||||
|
hideTooltip,
|
||||||
|
showTooltip,
|
||||||
|
} = useTooltip<TooltipData>();
|
||||||
|
|
||||||
|
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
||||||
|
scroll: true,
|
||||||
|
detectBounds: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const panelData =
|
||||||
|
queryResponse.data?.payload?.data.newResult.data.result || [];
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const pieChartData: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
}[] = [].concat(
|
||||||
|
...(panelData
|
||||||
|
.map((d) =>
|
||||||
|
d.series?.map((s) => ({
|
||||||
|
label:
|
||||||
|
d.series?.length === 1
|
||||||
|
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
|
||||||
|
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
|
||||||
|
value: s.values[0].value,
|
||||||
|
color: generateColor(
|
||||||
|
d.series?.length === 1
|
||||||
|
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
|
||||||
|
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
|
||||||
|
themeColors.chartcolors,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.filter((d) => d !== undefined) as never[]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let size = 0;
|
||||||
|
let width = 0;
|
||||||
|
let height = 0;
|
||||||
|
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
if (chartRef.current) {
|
||||||
|
const { offsetWidth, offsetHeight } = chartRef.current;
|
||||||
|
size = Math.min(offsetWidth, offsetHeight);
|
||||||
|
width = offsetWidth;
|
||||||
|
height = offsetHeight;
|
||||||
|
}
|
||||||
|
const half = size / 2;
|
||||||
|
|
||||||
|
const getFillColor = (color: string): string => {
|
||||||
|
if (active === null) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
const lightenedColor = lightenColor(color, 0.4); // Adjust the opacity value (0.7 in this case)
|
||||||
|
return active.color === color ? color : lightenedColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!pieChartData.length && <div className="piechart-no-data">No data</div>}
|
||||||
|
{pieChartData.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="piechart-container" ref={chartRef}>
|
||||||
|
<svg width={width} height={height} ref={containerRef}>
|
||||||
|
<Group top={height / 2} left={width / 2}>
|
||||||
|
<Pie
|
||||||
|
data={pieChartData}
|
||||||
|
pieValue={(data: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
}): number => parseFloat(data.value)}
|
||||||
|
outerRadius={({ data }): number => {
|
||||||
|
if (!active) return half - 3;
|
||||||
|
return data.label === active.label ? half : half - 3;
|
||||||
|
}}
|
||||||
|
padAngle={0.02}
|
||||||
|
cornerRadius={3}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
(pie) =>
|
||||||
|
pie.arcs.map((arc, index) => {
|
||||||
|
const { label } = arc.data;
|
||||||
|
const [centroidX, centroidY] = pie.path.centroid(arc);
|
||||||
|
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.6;
|
||||||
|
const arcPath = pie.path(arc);
|
||||||
|
const arcFill = arc.data.color;
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`arc-${label}-${index}`}
|
||||||
|
onMouseEnter={(): void => {
|
||||||
|
showTooltip({
|
||||||
|
tooltipData: {
|
||||||
|
label,
|
||||||
|
value: arc.data.value,
|
||||||
|
color: arc.data.color,
|
||||||
|
key: label,
|
||||||
|
},
|
||||||
|
tooltipTop: centroidY + height / 2,
|
||||||
|
tooltipLeft: centroidX + width / 2,
|
||||||
|
});
|
||||||
|
setActive(arc.data);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(): void => {
|
||||||
|
hideTooltip();
|
||||||
|
setActive(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
|
||||||
|
{hasSpaceForLabel && (
|
||||||
|
<text
|
||||||
|
x={centroidX}
|
||||||
|
y={centroidY}
|
||||||
|
dy=".33em"
|
||||||
|
fill="#000"
|
||||||
|
fontSize={10}
|
||||||
|
textAnchor="middle"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
{arc.data.label}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Pie>
|
||||||
|
</Group>
|
||||||
|
</svg>
|
||||||
|
{tooltipOpen && tooltipData && (
|
||||||
|
<TooltipInPortal
|
||||||
|
top={tooltipTop}
|
||||||
|
left={tooltipLeft}
|
||||||
|
style={{
|
||||||
|
...tooltipStyles,
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
|
||||||
|
}}
|
||||||
|
className="piechart-tooltip"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: tooltipData.color,
|
||||||
|
}}
|
||||||
|
className="piechart-indicator"
|
||||||
|
/>
|
||||||
|
{tooltipData.key}
|
||||||
|
<div className="tooltip-value">{tooltipData.value}</div>
|
||||||
|
</TooltipInPortal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="piechart-legend">
|
||||||
|
{pieChartData.length > 0 &&
|
||||||
|
pieChartData.map((data) => (
|
||||||
|
<div
|
||||||
|
key={data.label}
|
||||||
|
className="piechart-legend-item"
|
||||||
|
onMouseEnter={(): void => {
|
||||||
|
setActive(data);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(): void => {
|
||||||
|
setActive(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: getFillColor(data.color),
|
||||||
|
}}
|
||||||
|
className="piechart-legend-label"
|
||||||
|
/>
|
||||||
|
{data.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PiePanelWrapper;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
|
||||||
import ListPanelWrapper from './ListPanelWrapper';
|
import ListPanelWrapper from './ListPanelWrapper';
|
||||||
|
import PiePanelWrapper from './PiePanelWrapper';
|
||||||
import TablePanelWrapper from './TablePanelWrapper';
|
import TablePanelWrapper from './TablePanelWrapper';
|
||||||
import UplotPanelWrapper from './UplotPanelWrapper';
|
import UplotPanelWrapper from './UplotPanelWrapper';
|
||||||
import ValuePanelWrapper from './ValuePanelWrapper';
|
import ValuePanelWrapper from './ValuePanelWrapper';
|
||||||
@@ -12,5 +13,6 @@ export const PanelTypeVsPanelWrapper = {
|
|||||||
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
|
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
|
||||||
[PANEL_TYPES.TRACE]: null,
|
[PANEL_TYPES.TRACE]: null,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||||
|
[PANEL_TYPES.PIE]: PiePanelWrapper,
|
||||||
[PANEL_TYPES.BAR]: UplotPanelWrapper,
|
[PANEL_TYPES.BAR]: UplotPanelWrapper,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,3 +22,10 @@ export type PanelWrapperProps = {
|
|||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
selectedGraph?: PANEL_TYPES;
|
selectedGraph?: PANEL_TYPES;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TooltipData = {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|||||||
73
frontend/src/container/PanelWrapper/utils.ts
Normal file
73
frontend/src/container/PanelWrapper/utils.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { defaultStyles } from '@visx/tooltip';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export const tooltipStyles = {
|
||||||
|
...defaultStyles,
|
||||||
|
minWidth: 60,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||||
|
color: 'white',
|
||||||
|
zIndex: 9999,
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px 10px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLabel = (
|
||||||
|
label: string,
|
||||||
|
query: Query,
|
||||||
|
queryName: string,
|
||||||
|
isQueryContentMultipleResult = false, // If there are more than one aggregation return by the query, this should be set to true. Default is false.
|
||||||
|
): string => {
|
||||||
|
let finalQuery;
|
||||||
|
if (!isQueryContentMultipleResult) {
|
||||||
|
finalQuery = query.builder.queryData.find((q) => q.queryName === queryName);
|
||||||
|
if (!finalQuery) {
|
||||||
|
// If the query is not found in queryData, then check in queryFormulas
|
||||||
|
finalQuery = query.builder.queryFormulas.find(
|
||||||
|
(q) => q.queryName === queryName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalQuery) {
|
||||||
|
if (finalQuery.legend !== '') {
|
||||||
|
return finalQuery.legend;
|
||||||
|
}
|
||||||
|
if (label !== undefined) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
return queryName;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to convert a hex color to RGB format
|
||||||
|
const hexToRgb = (
|
||||||
|
color: string,
|
||||||
|
): { r: number; g: number; b: number } | null => {
|
||||||
|
const hex = color.replace(
|
||||||
|
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
||||||
|
(m, r, g, b) => r + r + g + g + b + b,
|
||||||
|
);
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lightenColor = (color: string, opacity: number): string => {
|
||||||
|
// Convert the hex color to RGB format
|
||||||
|
const rgbColor = hexToRgb(color);
|
||||||
|
if (!rgbColor) return color; // Return the original color if unable to parse
|
||||||
|
|
||||||
|
// Extract the RGB components
|
||||||
|
const { r, g, b } = rgbColor;
|
||||||
|
|
||||||
|
// Create a new RGBA color string with the specified opacity
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import i18n from 'ReactI18';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
import ChangeHistory from '../index';
|
||||||
|
import { pipelineData, pipelineDataHistory } from './testUtils';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ChangeHistory test', () => {
|
||||||
|
it('should render changeHistory correctly', () => {
|
||||||
|
const { getAllByText, getByText } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<ChangeHistory pipelineData={pipelineData} />
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// change History table headers
|
||||||
|
[
|
||||||
|
'Version',
|
||||||
|
'Deployment Stage',
|
||||||
|
'Last Deploy Message',
|
||||||
|
'Last Deployed Time',
|
||||||
|
'Edited by',
|
||||||
|
].forEach((text) => expect(getByText(text)).toBeInTheDocument());
|
||||||
|
|
||||||
|
// table content
|
||||||
|
expect(getAllByText('test-user').length).toBe(2);
|
||||||
|
expect(getAllByText('Deployment was successful').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test deployment stage and icon based on history data', () => {
|
||||||
|
const { getByText, container } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<ChangeHistory
|
||||||
|
pipelineData={{
|
||||||
|
...pipelineData,
|
||||||
|
history: pipelineDataHistory,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// assertion for different deployment stages
|
||||||
|
expect(container.querySelector('[data-icon="loading"]')).toBeInTheDocument();
|
||||||
|
expect(getByText('In Progress')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
container.querySelector('[data-icon="exclamation-circle"]'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByText('Dirty')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
container.querySelector('[data-icon="close-circle"]'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByText('Failed')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
container.querySelector('[data-icon="minus-circle"]'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByText('Unknown')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.ant-table-row').length).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user