Compare commits
139 Commits
variables-
...
better-rat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
946f30b08a | ||
|
|
831540eaf0 | ||
|
|
22c10f9479 | ||
|
|
e748fb0655 | ||
|
|
fdc54a62a9 | ||
|
|
abe0ab69b0 | ||
|
|
e623c92615 | ||
|
|
dc5917db01 | ||
|
|
d6a7f0b6f4 | ||
|
|
471803115e | ||
|
|
8403a3362d | ||
|
|
64d46bc855 | ||
|
|
c9fee27604 | ||
|
|
f1b6b2d3d8 | ||
|
|
468f056530 | ||
|
|
7086470ce2 | ||
|
|
352296c6cd | ||
|
|
975307a8b8 | ||
|
|
12377be809 | ||
|
|
9d90b8d19c | ||
|
|
5005923ef4 | ||
|
|
db4338be42 | ||
|
|
c7d0598ec0 | ||
|
|
4978fb9599 | ||
|
|
7b18c3ba06 | ||
|
|
92cdb36879 | ||
|
|
580f0b816e | ||
|
|
b770fc2457 | ||
|
|
c177230cce | ||
|
|
2112047a02 | ||
|
|
03c193d5a1 | ||
|
|
b83b295318 | ||
|
|
fbe75cd057 | ||
|
|
860145fb1d | ||
|
|
2fe75e74cd | ||
|
|
8e19c346a4 | ||
|
|
1b33efe4cc | ||
|
|
2642338672 | ||
|
|
845dc00568 | ||
|
|
a1090bfdc5 | ||
|
|
44f41c55f9 | ||
|
|
42ac9ab6fe | ||
|
|
5c02250aae | ||
|
|
c49a9dac1a | ||
|
|
abc2ec2155 | ||
|
|
4dc5615d2f | ||
|
|
6c350f30aa | ||
|
|
6664e1bc02 | ||
|
|
438cbcef87 | ||
|
|
829e1f0920 | ||
|
|
68d25a8989 | ||
|
|
cc90321ac0 | ||
|
|
bdcae62bf9 | ||
|
|
4e26189778 | ||
|
|
952ab58023 | ||
|
|
3ca2fff5c5 | ||
|
|
ef3a9adb48 | ||
|
|
975f141604 | ||
|
|
c206f4fa5c | ||
|
|
e88e24e434 | ||
|
|
94e0423479 | ||
|
|
5891fbc229 | ||
|
|
8137ec54ba | ||
|
|
f7b80524a5 | ||
|
|
4be0508dd2 | ||
|
|
a31c4b8339 | ||
|
|
d7846338ce | ||
|
|
5dac1ad20a | ||
|
|
8d704c331c | ||
|
|
f8e47496fa | ||
|
|
6fef9d9676 | ||
|
|
190767fd0a | ||
|
|
1e78786cae | ||
|
|
6448fb17e7 | ||
|
|
f2e33d7ca9 | ||
|
|
6c7167a224 | ||
|
|
00421235b0 | ||
|
|
0e2b67059b | ||
|
|
910c44cefc | ||
|
|
8bad036423 | ||
|
|
a21830132f | ||
|
|
9419f56e95 | ||
|
|
347868c18b | ||
|
|
17e20e7f41 | ||
|
|
2b0da82f94 | ||
|
|
911362cecf | ||
|
|
481f9620d3 | ||
|
|
e5be431f18 | ||
|
|
503ed45a99 | ||
|
|
28818fbaac | ||
|
|
c0e40614bf | ||
|
|
2d732ae4a9 | ||
|
|
8466e31e02 | ||
|
|
efdaf7ee43 | ||
|
|
0dec94a5c6 | ||
|
|
204728ff60 | ||
|
|
e51f4d986d | ||
|
|
337a941d0d | ||
|
|
fc4b55cb34 | ||
|
|
96cb8053df | ||
|
|
5651d69485 | ||
|
|
a6e492880d | ||
|
|
80b3c3e256 | ||
|
|
0806420dd7 | ||
|
|
18e240e3d1 | ||
|
|
d0965a24c5 | ||
|
|
7ed689693f | ||
|
|
90ae55264a | ||
|
|
bf4c792cdb | ||
|
|
dd097821d1 | ||
|
|
701b8803ac | ||
|
|
2728ddd255 | ||
|
|
5187ed58a0 | ||
|
|
2180118094 | ||
|
|
78d1e19e60 | ||
|
|
5588c7dd3f | ||
|
|
b70d50f2b3 | ||
|
|
728f699051 | ||
|
|
87499d1ead | ||
|
|
5fa8686fcf | ||
|
|
dc2db524c7 | ||
|
|
b3545b767a | ||
|
|
08f3b089f4 | ||
|
|
1d8e5b6c0f | ||
|
|
0dcded59e5 | ||
|
|
262beef8f9 | ||
|
|
43cc6dea92 | ||
|
|
6684640abe | ||
|
|
0a146910d6 | ||
|
|
690ed0f7f1 | ||
|
|
5bcf7de440 | ||
|
|
703983a5f9 | ||
|
|
766a2123c5 | ||
|
|
a476c68f7e | ||
|
|
fc15aa6f1c | ||
|
|
4192fd573d | ||
|
|
ca13d80205 | ||
|
|
8d84ce8f06 | ||
|
|
09ea7b9eb5 |
83
.github/workflows/docs.yml
vendored
Normal file
83
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
name: "Update PR labels and Block PR until related docs are shipped for the feature"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
types: [opened, edited, labeled, unlabeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs_label_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check PR Title and Manage Labels
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prTitle = context.payload.pull_request.title;
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
// Fetch the current PR details to get labels
|
||||||
|
const pr = await github.rest.pulls.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = pr.data.labels.map(label => label.name);
|
||||||
|
|
||||||
|
if (prTitle.startsWith('feat:')) {
|
||||||
|
const hasDocsRequired = labels.includes('docs required');
|
||||||
|
const hasDocsShipped = labels.includes('docs shipped');
|
||||||
|
const hasDocsNotRequired = labels.includes('docs not required');
|
||||||
|
|
||||||
|
// If "docs not required" is present, skip the checks
|
||||||
|
if (hasDocsNotRequired && !hasDocsRequired) {
|
||||||
|
console.log("Skipping checks due to 'docs not required' label.");
|
||||||
|
return; // Exit the script early
|
||||||
|
}
|
||||||
|
|
||||||
|
// If "docs shipped" is present, remove "docs required" if it exists
|
||||||
|
if (hasDocsShipped && hasDocsRequired) {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
name: 'docs required'
|
||||||
|
});
|
||||||
|
console.log("Removed 'docs required' label.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
|
||||||
|
if (!hasDocsRequired && !hasDocsShipped) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
labels: ['docs required']
|
||||||
|
});
|
||||||
|
console.log("Added 'docs required' label.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the updated labels after any changes
|
||||||
|
const updatedPr = await github.rest.pulls.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedLabels = updatedPr.data.labels.map(label => label.name);
|
||||||
|
const updatedHasDocsRequired = updatedLabels.includes('docs required');
|
||||||
|
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
|
||||||
|
|
||||||
|
// Block PR if "docs required" is still present and "docs shipped" is missing
|
||||||
|
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
|
||||||
|
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
|
||||||
|
}
|
||||||
1
.github/workflows/staging-deployment.yaml
vendored
1
.github/workflows/staging-deployment.yaml
vendored
@@ -38,6 +38,7 @@ jobs:
|
|||||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||||
export OTELCOL_TAG="main"
|
export OTELCOL_TAG="main"
|
||||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||||
|
export KAFKA_SPAN_EVAL="true"
|
||||||
docker system prune --force
|
docker system prune --force
|
||||||
docker pull signoz/signoz-otel-collector:main
|
docker pull signoz/signoz-otel-collector:main
|
||||||
docker pull signoz/signoz-schema-migrator:main
|
docker pull signoz/signoz-schema-migrator:main
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -79,7 +79,7 @@ build-query-service-static:
|
|||||||
@if [ $(DEV_BUILD) != "" ]; then \
|
@if [ $(DEV_BUILD) != "" ]; then \
|
||||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||||
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
|
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
|
||||||
else \
|
else \
|
||||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||||
@@ -188,13 +188,4 @@ check-no-ee-references:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./pkg/query-service/app/metrics/...
|
go test ./pkg/query-service/...
|
||||||
go test ./pkg/query-service/cache/...
|
|
||||||
go test ./pkg/query-service/app/...
|
|
||||||
go test ./pkg/query-service/app/querier/...
|
|
||||||
go test ./pkg/query-service/converter/...
|
|
||||||
go test ./pkg/query-service/formatter/...
|
|
||||||
go test ./pkg/query-service/tests/integration/...
|
|
||||||
go test ./pkg/query-service/rules/...
|
|
||||||
go test ./pkg/query-service/collectorsimulator/...
|
|
||||||
go test ./pkg/query-service/postprocess/...
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ services:
|
|||||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:0.23.5
|
image: signoz/alertmanager:0.23.7
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
command:
|
command:
|
||||||
@@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.55.0
|
image: signoz/query-service:0.56.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.55.0
|
image: signoz/frontend:0.56.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.102.10
|
image: signoz/signoz-otel-collector:0.111.5
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -214,7 +214,6 @@ services:
|
|||||||
- /:/hostfs:ro
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
|
||||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||||
ports:
|
ports:
|
||||||
# - "1777:1777" # pprof extension
|
# - "1777:1777" # pprof extension
|
||||||
@@ -238,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.102.10
|
image: signoz/signoz-schema-migrator:0.111.5
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -131,8 +131,7 @@ processors:
|
|||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
@@ -142,7 +141,6 @@ exporters:
|
|||||||
# logging: {}
|
# logging: {}
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/signoz_logs
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
use_new_schema: true
|
use_new_schema: true
|
||||||
extensions:
|
extensions:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
container_name: signoz-alertmanager
|
container_name: signoz-alertmanager
|
||||||
image: signoz/alertmanager:0.23.5
|
image: signoz/alertmanager:0.23.7
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -69,7 +69,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -84,7 +84,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.102.10
|
image: signoz/signoz-otel-collector:0.111.5
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ x-db-depend: &db-depend
|
|||||||
depends_on:
|
depends_on:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
otel-collector-migrator:
|
otel-collector-migrator-sync:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
# clickhouse-2:
|
# clickhouse-2:
|
||||||
# condition: service_healthy
|
# condition: service_healthy
|
||||||
@@ -147,7 +147,7 @@ services:
|
|||||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
|
||||||
container_name: signoz-alertmanager
|
container_name: signoz-alertmanager
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
@@ -162,7 +162,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.55.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -201,7 +201,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.55.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -212,11 +212,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../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-sync:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator-sync
|
||||||
command:
|
command:
|
||||||
|
- "sync"
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
|
- "--up="
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -225,9 +227,25 @@ services:
|
|||||||
# clickhouse-3:
|
# clickhouse-3:
|
||||||
# condition: service_healthy
|
# condition: service_healthy
|
||||||
|
|
||||||
|
otel-collector-migrator-async:
|
||||||
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||||
|
container_name: otel-migrator-async
|
||||||
|
command:
|
||||||
|
- "async"
|
||||||
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
|
- "--up="
|
||||||
|
depends_on:
|
||||||
|
clickhouse:
|
||||||
|
condition: service_healthy
|
||||||
|
otel-collector-migrator-sync:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
# clickhouse-2:
|
||||||
|
# condition: service_healthy
|
||||||
|
# clickhouse-3:
|
||||||
|
# condition: service_healthy
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -244,7 +262,6 @@ services:
|
|||||||
- /:/hostfs:ro
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
|
||||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||||
ports:
|
ports:
|
||||||
# - "1777:1777" # pprof extension
|
# - "1777:1777" # pprof extension
|
||||||
@@ -262,7 +279,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
otel-collector-migrator:
|
otel-collector-migrator-sync:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
query-service:
|
query-service:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ services:
|
|||||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
|
||||||
container_name: signoz-alertmanager
|
container_name: signoz-alertmanager
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
@@ -167,7 +167,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.55.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -191,6 +191,7 @@ services:
|
|||||||
- GODEBUG=netdns=go
|
- GODEBUG=netdns=go
|
||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||||
|
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
@@ -207,7 +208,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.55.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -219,7 +220,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.102.10}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -233,7 +234,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -250,7 +251,6 @@ services:
|
|||||||
- /:/hostfs:ro
|
- /:/hostfs:ro
|
||||||
environment:
|
environment:
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
|
||||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||||
ports:
|
ports:
|
||||||
# - "1777:1777" # pprof extension
|
# - "1777:1777" # pprof extension
|
||||||
|
|||||||
@@ -142,8 +142,7 @@ extensions:
|
|||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
@@ -152,7 +151,6 @@ exporters:
|
|||||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/signoz_logs
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
use_new_schema: true
|
use_new_schema: true
|
||||||
# logging: {}
|
# logging: {}
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ import (
|
|||||||
|
|
||||||
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
|
validPath := false
|
||||||
|
for _, allowedPrefix := range gateway.AllowedPrefix {
|
||||||
|
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
||||||
|
validPath = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validPath {
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
|||||||
if anomalyQueryExists {
|
if anomalyQueryExists {
|
||||||
// ensure all queries have metric data source, and there should be only one anomaly query
|
// ensure all queries have metric data source, and there should be only one anomaly query
|
||||||
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
||||||
if query.DataSource != v3.DataSourceMetrics {
|
// What is query.QueryName == query.Expression doing here?
|
||||||
|
// In the current implementation, the way to recognize if a query is a formula is by
|
||||||
|
// checking if the expression is the same as the query name. if the expression is different
|
||||||
|
// then it is a formula. otherwise, it is simple builder query.
|
||||||
|
if query.DataSource != v3.DataSourceMetrics && query.QueryName == query.Expression {
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,13 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
|||||||
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
|
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
|
||||||
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
|
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
|
||||||
)
|
)
|
||||||
|
default:
|
||||||
|
provider = anomaly.NewDailyProvider(
|
||||||
|
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
|
||||||
|
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||||
|
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
|
||||||
|
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
|
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/rules"
|
"go.signoz.io/signoz/ee/query-service/rules"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
"go.signoz.io/signoz/pkg/query-service/migrate"
|
"go.signoz.io/signoz/pkg/query-service/migrate"
|
||||||
"go.signoz.io/signoz/pkg/query-service/model"
|
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
|
||||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||||
@@ -348,7 +347,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user.User.OrgId == "" {
|
if user.User.OrgId == "" {
|
||||||
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
@@ -765,8 +764,9 @@ func makeRulesManager(
|
|||||||
Cache: cache,
|
Cache: cache,
|
||||||
EvalDelay: baseconst.GetEvalDelay(),
|
EvalDelay: baseconst.GetEvalDelay(),
|
||||||
|
|
||||||
PrepareTaskFunc: rules.PrepareTaskFunc,
|
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||||
UseLogsNewSchema: useLogsNewSchema,
|
PrepareTestRuleFunc: rules.TestNotification,
|
||||||
|
UseLogsNewSchema: useLogsNewSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create Manager
|
// create Manager
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
RoutePrefix string = "/api/gateway"
|
RoutePrefix string = "/api/gateway"
|
||||||
AllowedPrefix string = "/v1/workspaces/me"
|
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me", "/v2/deployments/me"}
|
||||||
)
|
)
|
||||||
|
|
||||||
type proxy struct {
|
type proxy struct {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,6 +135,13 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.HostsInfraMonitoring,
|
||||||
|
Active: constants.EnableHostsInfraMonitoring(),
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ProPlan = basemodel.FeatureSet{
|
var ProPlan = basemodel.FeatureSet{
|
||||||
@@ -249,6 +257,13 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.HostsInfraMonitoring,
|
||||||
|
Active: constants.EnableHostsInfraMonitoring(),
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnterprisePlan = basemodel.FeatureSet{
|
var EnterprisePlan = basemodel.FeatureSet{
|
||||||
@@ -378,4 +393,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.HostsInfraMonitoring,
|
||||||
|
Active: constants.EnableHostsInfraMonitoring(),
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package rules
|
package rules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||||
@@ -79,6 +84,106 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
|||||||
return task, nil
|
return task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNotification prepares a dummy rule for given rule parameters and
|
||||||
|
// sends a test notification. returns alert count and error (if any)
|
||||||
|
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if opts.Rule == nil {
|
||||||
|
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedRule := opts.Rule
|
||||||
|
var alertname = parsedRule.AlertName
|
||||||
|
if alertname == "" {
|
||||||
|
// alertname is not mandatory for testing, so picking
|
||||||
|
// a random string here
|
||||||
|
alertname = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// append name to indicate this is test alert
|
||||||
|
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
|
||||||
|
|
||||||
|
var rule baserules.Rule
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if parsedRule.RuleType == baserules.RuleTypeThreshold {
|
||||||
|
|
||||||
|
// add special labels for test alerts
|
||||||
|
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
|
||||||
|
parsedRule.Labels[labels.RuleSourceLabel] = ""
|
||||||
|
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
|
||||||
|
|
||||||
|
// create a threshold rule
|
||||||
|
rule, err = baserules.NewThresholdRule(
|
||||||
|
alertname,
|
||||||
|
parsedRule,
|
||||||
|
opts.FF,
|
||||||
|
opts.Reader,
|
||||||
|
opts.UseLogsNewSchema,
|
||||||
|
baserules.WithSendAlways(),
|
||||||
|
baserules.WithSendUnmatched(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||||
|
return 0, basemodel.BadRequest(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if parsedRule.RuleType == baserules.RuleTypeProm {
|
||||||
|
|
||||||
|
// create promql rule
|
||||||
|
rule, err = baserules.NewPromRule(
|
||||||
|
alertname,
|
||||||
|
parsedRule,
|
||||||
|
opts.Logger,
|
||||||
|
opts.Reader,
|
||||||
|
opts.ManagerOpts.PqlEngine,
|
||||||
|
baserules.WithSendAlways(),
|
||||||
|
baserules.WithSendUnmatched(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||||
|
return 0, basemodel.BadRequest(err)
|
||||||
|
}
|
||||||
|
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly {
|
||||||
|
// create anomaly rule
|
||||||
|
rule, err = NewAnomalyRule(
|
||||||
|
alertname,
|
||||||
|
parsedRule,
|
||||||
|
opts.FF,
|
||||||
|
opts.Reader,
|
||||||
|
opts.Cache,
|
||||||
|
baserules.WithSendAlways(),
|
||||||
|
baserules.WithSendUnmatched(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||||
|
return 0, basemodel.BadRequest(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set timestamp to current utc time
|
||||||
|
ts := time.Now().UTC()
|
||||||
|
|
||||||
|
count, err := rule.Eval(ctx, ts)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
|
||||||
|
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
||||||
|
}
|
||||||
|
alertsFound, ok := count.(int)
|
||||||
|
if !ok {
|
||||||
|
return 0, basemodel.InternalError(fmt.Errorf("something went wrong"))
|
||||||
|
}
|
||||||
|
rule.SendAlerts(ctx, ts, 0, time.Duration(1*time.Minute), opts.NotifyFunc)
|
||||||
|
|
||||||
|
return alertsFound, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newTask returns an appropriate group for
|
// newTask returns an appropriate group for
|
||||||
// rule type
|
// rule type
|
||||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@dnd-kit/core": "6.1.0",
|
"@dnd-kit/core": "6.1.0",
|
||||||
"@dnd-kit/modifiers": "7.0.0",
|
"@dnd-kit/modifiers": "7.0.0",
|
||||||
"@dnd-kit/sortable": "8.0.0",
|
"@dnd-kit/sortable": "8.0.0",
|
||||||
"@grafana/data": "^9.5.2",
|
"@grafana/data": "^11.2.3",
|
||||||
"@mdx-js/loader": "2.3.0",
|
"@mdx-js/loader": "2.3.0",
|
||||||
"@mdx-js/react": "2.3.0",
|
"@mdx-js/react": "2.3.0",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"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.7.4",
|
"axios": "1.7.7",
|
||||||
"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",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"fontfaceobserver": "2.3.0",
|
"fontfaceobserver": "2.3.0",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.7",
|
||||||
"i18next": "^21.6.12",
|
"i18next": "^21.6.12",
|
||||||
"i18next-browser-languagedetector": "^6.1.3",
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
"i18next-http-backend": "^1.3.2",
|
"i18next-http-backend": "^1.3.2",
|
||||||
@@ -87,6 +87,8 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "0.379.0",
|
"lucide-react": "0.379.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
|
"overlayscrollbars": "^2.8.1",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
"posthog-js": "1.160.3",
|
"posthog-js": "1.160.3",
|
||||||
"rc-tween-one": "3.0.6",
|
"rc-tween-one": "3.0.6",
|
||||||
@@ -107,11 +109,10 @@
|
|||||||
"react-query": "3.39.3",
|
"react-query": "3.39.3",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-router-dom-v5-compat": "6.27.0",
|
||||||
"react-syntax-highlighter": "15.5.0",
|
"react-syntax-highlighter": "15.5.0",
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
"react-virtuoso": "4.0.3",
|
"react-virtuoso": "4.0.3",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
|
||||||
"overlayscrollbars": "^2.8.1",
|
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
@@ -123,10 +124,10 @@
|
|||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
"uplot": "1.6.26",
|
"uplot": "1.6.31",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.94.0",
|
||||||
"webpack-dev-server": "^4.15.1",
|
"webpack-dev-server": "^4.15.1",
|
||||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||||
"xstate": "^4.31.0"
|
"xstate": "^4.31.0"
|
||||||
|
|||||||
BIN
frontend/public/Images/signoz-hero-image.webp
Normal file
BIN
frontend/public/Images/signoz-hero-image.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
1
frontend/public/css/uPlot.min.css
vendored
Normal file
1
frontend/public/css/uPlot.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
|
||||||
BIN
frontend/public/fonts/FiraCode-VariableFont_wght.ttf
Normal file
BIN
frontend/public/fonts/FiraCode-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
BIN
frontend/public/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/SpaceMono-Regular.ttf
Normal file
BIN
frontend/public/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/WorkSans-VariableFont_wght.ttf
Normal file
BIN
frontend/public/fonts/WorkSans-VariableFont_wght.ttf
Normal file
Binary file not shown.
@@ -43,11 +43,18 @@
|
|||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
"option_30min": "30 mins",
|
"option_30min": "30 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
"option_4hours": "4 hours",
|
"option_2hours": "2 hours",
|
||||||
"option_3hours": "3 hours",
|
"option_3hours": "3 hours",
|
||||||
|
"option_4hours": "4 hours",
|
||||||
|
"option_5hours": "5 hours",
|
||||||
"option_6hours": "6 hours",
|
"option_6hours": "6 hours",
|
||||||
|
"option_8hours": "8 hours",
|
||||||
|
"option_10hours": "10 hours",
|
||||||
"option_12hours": "12 hours",
|
"option_12hours": "12 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
|
"option_48hours": "48 hours",
|
||||||
|
"option_72hours": "72 hours",
|
||||||
|
"option_1week": "1 week",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
@@ -56,6 +63,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
@@ -110,6 +118,8 @@
|
|||||||
"choose_alert_type": "Choose a type for the alert",
|
"choose_alert_type": "Choose a type for the alert",
|
||||||
"metric_based_alert": "Metric based Alert",
|
"metric_based_alert": "Metric based Alert",
|
||||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
|
"anomaly_based_alert": "Anomaly based Alert",
|
||||||
|
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
"log_based_alert": "Log-based Alert",
|
"log_based_alert": "Log-based Alert",
|
||||||
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
||||||
"traces_based_alert": "Trace-based Alert",
|
"traces_based_alert": "Trace-based Alert",
|
||||||
|
|||||||
@@ -29,12 +29,24 @@
|
|||||||
"text_condition1": "Send a notification when",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
|
"option_1min": "1 min",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
"option_10min": "10 mins",
|
"option_10min": "10 mins",
|
||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
|
"option_30min": "30 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
|
"option_2hours": "2 hours",
|
||||||
|
"option_3hours": "3 hours",
|
||||||
"option_4hours": "4 hours",
|
"option_4hours": "4 hours",
|
||||||
|
"option_5hours": "5 hours",
|
||||||
|
"option_6hours": "6 hours",
|
||||||
|
"option_8hours": "8 hours",
|
||||||
|
"option_10hours": "10 hours",
|
||||||
|
"option_12hours": "12 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
|
"option_48hours": "48 hours",
|
||||||
|
"option_72hours": "72 hours",
|
||||||
|
"option_1week": "1 week",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
@@ -43,6 +55,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
|
|||||||
@@ -13,9 +13,12 @@
|
|||||||
"button_no": "No",
|
"button_no": "No",
|
||||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Choose a detection method",
|
||||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
"alert_form_step2": "Define the metric",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Define Alert Conditions",
|
||||||
|
"alert_form_step4": "Alert Configuration",
|
||||||
|
"threshold_alert_desc": "An alert is triggered whenever a metric deviates from an expected threshold.",
|
||||||
|
"anomaly_detection_alert_desc": "An alert is triggered whenever a metric deviates from an expected pattern.",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"confirm_save_content_part1": "Your alert built with",
|
||||||
@@ -35,6 +38,7 @@
|
|||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when",
|
"text_condition1": "Send a notification when",
|
||||||
|
"text_condition1_anomaly": "Send notification when the observed value for",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_1min": "1 min",
|
"option_1min": "1 min",
|
||||||
@@ -43,11 +47,18 @@
|
|||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
"option_30min": "30 mins",
|
"option_30min": "30 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
|
"option_2hours": "2 hours",
|
||||||
"option_3hours": "3 hours",
|
"option_3hours": "3 hours",
|
||||||
"option_4hours": "4 hours",
|
"option_4hours": "4 hours",
|
||||||
|
"option_5hours": "5 hours",
|
||||||
"option_6hours": "6 hours",
|
"option_6hours": "6 hours",
|
||||||
|
"option_8hours": "8 hours",
|
||||||
|
"option_10hours": "10 hours",
|
||||||
"option_12hours": "12 hours",
|
"option_12hours": "12 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
|
"option_48hours": "48 hours",
|
||||||
|
"option_72hours": "72 hours",
|
||||||
|
"option_1week": "1 week",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
@@ -56,6 +67,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
@@ -109,7 +121,9 @@
|
|||||||
"user_tooltip_more_help": "More details on how to create alerts",
|
"user_tooltip_more_help": "More details on how to create alerts",
|
||||||
"choose_alert_type": "Choose a type for the alert",
|
"choose_alert_type": "Choose a type for the alert",
|
||||||
"metric_based_alert": "Metric based Alert",
|
"metric_based_alert": "Metric based Alert",
|
||||||
|
"anomaly_based_alert": "Anomaly based Alert",
|
||||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
|
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
"log_based_alert": "Log-based Alert",
|
"log_based_alert": "Log-based Alert",
|
||||||
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
||||||
"traces_based_alert": "Trace-based Alert",
|
"traces_based_alert": "Trace-based Alert",
|
||||||
|
|||||||
@@ -1,30 +1,50 @@
|
|||||||
{
|
{
|
||||||
"breadcrumb": "Messaging Queues",
|
"breadcrumb": "Messaging Queues",
|
||||||
"header": "Kafka / Overview",
|
"header": "Kafka / Overview",
|
||||||
"overview": {
|
"overview": {
|
||||||
"title": "Start sending data in as little as 20 minutes",
|
"title": "Start sending data in as little as 20 minutes",
|
||||||
"subtitle": "Connect and Monitor Your Data Streams"
|
"subtitle": "Connect and Monitor Your Data Streams"
|
||||||
},
|
},
|
||||||
"configureConsumer": {
|
"configureConsumer": {
|
||||||
"title": "Configure Consumer",
|
"title": "Configure Consumer",
|
||||||
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||||
"button": "Get Started"
|
"button": "Get Started"
|
||||||
},
|
},
|
||||||
"configureProducer": {
|
"configureProducer": {
|
||||||
"title": "Configure Producer",
|
"title": "Configure Producer",
|
||||||
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||||
"button": "Get Started"
|
"button": "Get Started"
|
||||||
},
|
},
|
||||||
"monitorKafka": {
|
"monitorKafka": {
|
||||||
"title": "Monitor kafka",
|
"title": "Monitor kafka",
|
||||||
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||||
"button": "Get Started"
|
"button": "Get Started"
|
||||||
},
|
},
|
||||||
"summarySection": {
|
"summarySection": {
|
||||||
"viewDetailsButton": "View Details"
|
"viewDetailsButton": "View Details",
|
||||||
},
|
"consumer": {
|
||||||
"confirmModal": {
|
"title": "Consumer lag view",
|
||||||
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
"description": "Connect and Monitor Your Data Streams"
|
||||||
"okText": "Proceed"
|
},
|
||||||
}
|
"producer": {
|
||||||
}
|
"title": "Producer latency view",
|
||||||
|
"description": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"partition": {
|
||||||
|
"title": "Partition Latency view",
|
||||||
|
"description": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"dropRate": {
|
||||||
|
"title": "Drop Rate view",
|
||||||
|
"description": "Connect and Monitor Your Data Streams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmModal": {
|
||||||
|
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||||
|
"okText": "Proceed"
|
||||||
|
},
|
||||||
|
"overviewSummarySection": {
|
||||||
|
"title": "Monitor Your Data Streams",
|
||||||
|
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,12 +29,24 @@
|
|||||||
"text_condition1": "Send a notification when",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
|
"option_1min": "1 min",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
"option_10min": "10 mins",
|
"option_10min": "10 mins",
|
||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
|
"option_30min": "30 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
|
"option_2hours": "2 hours",
|
||||||
|
"option_3hours": "3 hours",
|
||||||
"option_4hours": "4 hours",
|
"option_4hours": "4 hours",
|
||||||
|
"option_5hours": "5 hours",
|
||||||
|
"option_6hours": "6 hours",
|
||||||
|
"option_8hours": "8 hours",
|
||||||
|
"option_10hours": "10 hours",
|
||||||
|
"option_12hours": "12 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
|
"option_48hours": "48 hours",
|
||||||
|
"option_72hours": "72 hours",
|
||||||
|
"option_1week": "1 week",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
@@ -43,6 +55,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||||
"SERVICE_MAP": "SigNoz | Service Map",
|
"SERVICE_MAP": "SigNoz | Service Map",
|
||||||
"GET_STARTED": "SigNoz | Get Started",
|
"GET_STARTED": "SigNoz | Get Started",
|
||||||
|
"ONBOARDING": "SigNoz | Get Started",
|
||||||
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
|
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
|
||||||
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
||||||
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
|
import getOrgUser from 'api/user/getOrgUser';
|
||||||
import loginApi from 'api/user/login';
|
import loginApi from 'api/user/login';
|
||||||
import { Logout } from 'api/utils';
|
import { Logout } from 'api/utils';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
@@ -8,8 +9,10 @@ import ROUTES from 'constants/routes';
|
|||||||
import useLicense from 'hooks/useLicense';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { ReactChild, useEffect, useMemo } from 'react';
|
import { isEmpty, isNull } from 'lodash-es';
|
||||||
|
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@@ -17,6 +20,7 @@ import { AppState } from 'store/reducers';
|
|||||||
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
||||||
|
import { Organization } from 'types/api/user/getOrganization';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { routePermission } from 'utils/permission';
|
import { routePermission } from 'utils/permission';
|
||||||
|
|
||||||
@@ -31,6 +35,19 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
org,
|
||||||
|
orgPreferences,
|
||||||
|
user,
|
||||||
|
role,
|
||||||
|
isUserFetching,
|
||||||
|
isUserFetchingError,
|
||||||
|
isLoggedIn: isLoggedInState,
|
||||||
|
isFetchingOrgPreferences,
|
||||||
|
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const mapRoutes = useMemo(
|
const mapRoutes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
@@ -44,18 +61,21 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOnboardingComplete = useMemo(
|
||||||
|
() =>
|
||||||
|
orgPreferences?.find(
|
||||||
|
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||||
|
)?.value,
|
||||||
|
[orgPreferences],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: licensesData,
|
data: licensesData,
|
||||||
isFetching: isFetchingLicensesData,
|
isFetching: isFetchingLicensesData,
|
||||||
} = useLicense();
|
} = useLicense();
|
||||||
|
|
||||||
const {
|
|
||||||
isUserFetching,
|
|
||||||
isUserFetchingError,
|
|
||||||
isLoggedIn: isLoggedInState,
|
|
||||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
|
|
||||||
const { t } = useTranslation(['common']);
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
@@ -66,6 +86,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
|
|
||||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||||
|
|
||||||
|
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||||
|
|
||||||
const isLocalStorageLoggedIn =
|
const isLocalStorageLoggedIn =
|
||||||
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
|
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
|
||||||
|
|
||||||
@@ -81,6 +103,63 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
|
||||||
|
queryFn: () => {
|
||||||
|
if (orgData && orgData.id !== undefined) {
|
||||||
|
return getOrgUser({
|
||||||
|
orgId: orgData.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
queryKey: ['getOrgUser'],
|
||||||
|
enabled: !isEmpty(orgData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkFirstTimeUser = (): boolean => {
|
||||||
|
const users = orgUsers?.payload || [];
|
||||||
|
|
||||||
|
const remainingUsers = users.filter(
|
||||||
|
(user) => user.email !== 'admin@signoz.cloud',
|
||||||
|
);
|
||||||
|
|
||||||
|
return remainingUsers.length === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
|
||||||
|
const shouldShowOnboarding = (): boolean => {
|
||||||
|
// Only run this effect if the org users and preferences are loaded
|
||||||
|
|
||||||
|
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
|
||||||
|
const isFirstUser = checkFirstTimeUser();
|
||||||
|
|
||||||
|
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||||
|
return isFirstUser && !isOnboardingComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRedirectForOrgOnboarding = (key: string): void => {
|
||||||
|
if (
|
||||||
|
isLoggedInState &&
|
||||||
|
!isFetchingOrgPreferences &&
|
||||||
|
!isLoadingOrgUsers &&
|
||||||
|
!isEmpty(orgUsers?.payload) &&
|
||||||
|
!isNull(orgPreferences)
|
||||||
|
) {
|
||||||
|
if (key === 'ONBOARDING' && isOnboardingComplete) {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstTimeUser = checkFirstTimeUser();
|
||||||
|
|
||||||
|
if (isFirstTimeUser && !isOnboardingComplete) {
|
||||||
|
history.push(ROUTES.ONBOARDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUserLoginIfTokenPresent = async (
|
const handleUserLoginIfTokenPresent = async (
|
||||||
key: keyof typeof ROUTES,
|
key: keyof typeof ROUTES,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -102,6 +181,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
response.payload.refreshJwt,
|
response.payload.refreshJwt,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
handleRedirectForOrgOnboarding(key);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userResponse &&
|
userResponse &&
|
||||||
route &&
|
route &&
|
||||||
@@ -129,7 +210,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
) {
|
) {
|
||||||
handleUserLoginIfTokenPresent(key);
|
handleUserLoginIfTokenPresent(key);
|
||||||
} else {
|
} else {
|
||||||
// user does have localstorage values
|
handleRedirectForOrgOnboarding(key);
|
||||||
|
|
||||||
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
||||||
}
|
}
|
||||||
@@ -160,6 +241,45 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [isFetchingLicensesData]);
|
}, [isFetchingLicensesData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||||
|
setOrgData(org[0]);
|
||||||
|
}
|
||||||
|
}, [org]);
|
||||||
|
|
||||||
|
const handleRouting = (): void => {
|
||||||
|
const showOrgOnboarding = shouldShowOnboarding();
|
||||||
|
|
||||||
|
if (showOrgOnboarding && !isOnboardingComplete) {
|
||||||
|
history.push(ROUTES.ONBOARDING);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { isPrivate } = currentRoute || {
|
||||||
|
isPrivate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoggedInState && role && role !== 'ADMIN') {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPrivate) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isEmpty(user) &&
|
||||||
|
!isFetchingOrgPreferences &&
|
||||||
|
!isEmpty(orgUsers?.payload) &&
|
||||||
|
!isNull(orgPreferences)
|
||||||
|
) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentRoute, user, role, orgUsers, orgPreferences]);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
@@ -181,9 +301,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
handlePrivateRoutes(key);
|
handlePrivateRoutes(key);
|
||||||
} else {
|
} else {
|
||||||
// no need to fetch the user and make user fetching false
|
// no need to fetch the user and make user fetching false
|
||||||
|
|
||||||
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
||||||
history.push(ROUTES.APPLICATION);
|
handleRouting();
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_USER_IS_FETCH,
|
type: UPDATE_USER_IS_FETCH,
|
||||||
@@ -195,7 +314,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
} else if (pathname === ROUTES.HOME_PAGE) {
|
} else if (pathname === ROUTES.HOME_PAGE) {
|
||||||
// routing to application page over root page
|
// routing to application page over root page
|
||||||
if (isLoggedInState) {
|
if (isLoggedInState) {
|
||||||
history.push(ROUTES.APPLICATION);
|
handleRouting();
|
||||||
} else {
|
} else {
|
||||||
navigateToLoginIfNotLoggedIn();
|
navigateToLoginIfNotLoggedIn();
|
||||||
}
|
}
|
||||||
@@ -208,13 +327,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [dispatch, isLoggedInState, currentRoute, licensesData]);
|
}, [
|
||||||
|
dispatch,
|
||||||
|
isLoggedInState,
|
||||||
|
currentRoute,
|
||||||
|
licensesData,
|
||||||
|
orgUsers,
|
||||||
|
orgPreferences,
|
||||||
|
]);
|
||||||
|
|
||||||
if (isUserFetchingError) {
|
if (isUserFetchingError) {
|
||||||
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
|
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserFetching) {
|
if (isUserFetching || isLoading) {
|
||||||
return <Spinner tip="Loading..." />;
|
return <Spinner tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ConfigProvider } from 'antd';
|
|||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@@ -24,13 +25,20 @@ import AlertRuleProvider from 'providers/Alert';
|
|||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
|
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app';
|
import {
|
||||||
|
UPDATE_FEATURE_FLAG_RESPONSE,
|
||||||
|
UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||||
|
UPDATE_ORG_PREFERENCES,
|
||||||
|
} from 'types/actions/app';
|
||||||
import AppReducer, { User } from 'types/reducer/app';
|
import AppReducer, { User } from 'types/reducer/app';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||||
|
|
||||||
import PrivateRoute from './Private';
|
import PrivateRoute from './Private';
|
||||||
@@ -65,6 +73,41 @@ function App(): JSX.Element {
|
|||||||
const isPremiumSupportEnabled =
|
const isPremiumSupportEnabled =
|
||||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
|
|
||||||
|
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
|
||||||
|
queryFn: () => getAllOrgPreferences(),
|
||||||
|
queryKey: ['getOrgPreferences'],
|
||||||
|
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (orgPreferences && !isLoadingOrgPreferences) {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||||
|
payload: {
|
||||||
|
isFetchingOrgPreferences: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_ORG_PREFERENCES,
|
||||||
|
payload: {
|
||||||
|
orgPreferences: orgPreferences.payload?.data || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||||
|
payload: {
|
||||||
|
isFetchingOrgPreferences: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isLoggedInState, role, dispatch]);
|
||||||
|
|
||||||
const featureResponse = useGetFeatureFlag((allFlags) => {
|
const featureResponse = useGetFeatureFlag((allFlags) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||||
@@ -182,6 +225,16 @@ function App(): JSX.Element {
|
|||||||
}, [isLoggedInState, isOnBasicPlan, user]);
|
}, [isLoggedInState, isOnBasicPlan, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (pathname === ROUTES.ONBOARDING) {
|
||||||
|
window.Intercom('update', {
|
||||||
|
hide_default_launcher: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.Intercom('update', {
|
||||||
|
hide_default_launcher: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
trackPageView(pathname);
|
trackPageView(pathname);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
@@ -204,6 +257,7 @@ function App(): JSX.Element {
|
|||||||
user,
|
user,
|
||||||
licenseData,
|
licenseData,
|
||||||
isPremiumSupportEnabled,
|
isPremiumSupportEnabled,
|
||||||
|
pathname,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -239,36 +293,38 @@ function App(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<NotificationProvider>
|
<CompatRouter>
|
||||||
<PrivateRoute>
|
<NotificationProvider>
|
||||||
<ResourceProvider>
|
<PrivateRoute>
|
||||||
<QueryBuilderProvider>
|
<ResourceProvider>
|
||||||
<DashboardProvider>
|
<QueryBuilderProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<DashboardProvider>
|
||||||
<AlertRuleProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</AlertRuleProvider>
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ export const Onboarding = Loadable(
|
|||||||
() => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'),
|
() => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const OrgOnboarding = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "OrgOnboarding" */ 'pages/OrgOnboarding'),
|
||||||
|
);
|
||||||
|
|
||||||
export const DashboardPage = Loadable(
|
export const DashboardPage = Loadable(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
OldLogsExplorer,
|
OldLogsExplorer,
|
||||||
Onboarding,
|
Onboarding,
|
||||||
OrganizationSettings,
|
OrganizationSettings,
|
||||||
|
OrgOnboarding,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
PipelinePage,
|
PipelinePage,
|
||||||
ServiceMapPage,
|
ServiceMapPage,
|
||||||
@@ -68,6 +69,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'GET_STARTED',
|
key: 'GET_STARTED',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.ONBOARDING,
|
||||||
|
exact: false,
|
||||||
|
component: OrgOnboarding,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'ONBOARDING',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: LogsIndexToFields,
|
component: LogsIndexToFields,
|
||||||
path: ROUTES.LOGS_INDEX_FIELDS,
|
path: ROUTES.LOGS_INDEX_FIELDS,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const apiV2 = '/api/v2/';
|
|||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
export const apiV4 = '/api/v4/';
|
export const apiV4 = '/api/v4/';
|
||||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||||
|
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||||
export const apiAlertManager = '/api/alertmanager/';
|
export const apiAlertManager = '/api/alertmanager/';
|
||||||
|
|
||||||
export default apiV1;
|
export default apiV1;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import apiV1, {
|
|||||||
apiV3,
|
apiV3,
|
||||||
apiV4,
|
apiV4,
|
||||||
gatewayApiV1,
|
gatewayApiV1,
|
||||||
|
gatewayApiV2,
|
||||||
} from './apiV1';
|
} from './apiV1';
|
||||||
import { Logout } from './utils';
|
import { Logout } from './utils';
|
||||||
|
|
||||||
@@ -169,6 +170,19 @@ GatewayApiV1Instance.interceptors.response.use(
|
|||||||
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
|
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// gateway Api V2
|
||||||
|
export const GatewayApiV2Instance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
GatewayApiV2Instance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejected,
|
||||||
|
);
|
||||||
|
|
||||||
|
GatewayApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
AxiosAlertManagerInstance.interceptors.response.use(
|
AxiosAlertManagerInstance.interceptors.response.use(
|
||||||
interceptorsResponse,
|
interceptorsResponse,
|
||||||
interceptorRejected,
|
interceptorRejected,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ApiBaseInstance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
export interface OnboardingStatusResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
attribute?: string;
|
||||||
|
error_message?: string;
|
||||||
|
status?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOnboardingStatus = async (props: {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
endpointService?: string;
|
||||||
|
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
|
||||||
|
const { endpointService, ...rest } = props;
|
||||||
|
try {
|
||||||
|
const response = await ApiBaseInstance.post(
|
||||||
|
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
|
||||||
|
rest,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getOnboardingStatus;
|
||||||
20
frontend/src/api/onboarding/updateProfile.ts
Normal file
20
frontend/src/api/onboarding/updateProfile.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { GatewayApiV2Instance } from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { UpdateProfileProps } from 'types/api/onboarding/types';
|
||||||
|
|
||||||
|
const updateProfile = async (
|
||||||
|
props: UpdateProfileProps,
|
||||||
|
): Promise<SuccessResponse<UpdateProfileProps> | ErrorResponse> => {
|
||||||
|
const response = await GatewayApiV2Instance.put('/profiles/me', {
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateProfile;
|
||||||
18
frontend/src/api/preferences/getAllOrgPreferences.ts
Normal file
18
frontend/src/api/preferences/getAllOrgPreferences.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const getAllOrgPreferences = async (): Promise<
|
||||||
|
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.get(`/org/preferences`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllOrgPreferences;
|
||||||
18
frontend/src/api/preferences/getAllUserPreference.ts
Normal file
18
frontend/src/api/preferences/getAllUserPreference.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const getAllUserPreferences = async (): Promise<
|
||||||
|
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.get(`/user/preferences`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllUserPreferences;
|
||||||
20
frontend/src/api/preferences/getOrgPreference.ts
Normal file
20
frontend/src/api/preferences/getOrgPreference.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const getOrgPreference = async ({
|
||||||
|
preferenceID,
|
||||||
|
}: {
|
||||||
|
preferenceID: string;
|
||||||
|
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
|
||||||
|
const response = await axios.get(`/org/preferences/${preferenceID}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getOrgPreference;
|
||||||
22
frontend/src/api/preferences/getUserPreference.ts
Normal file
22
frontend/src/api/preferences/getUserPreference.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const getUserPreference = async ({
|
||||||
|
preferenceID,
|
||||||
|
}: {
|
||||||
|
preferenceID: string;
|
||||||
|
}): Promise<
|
||||||
|
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.get(`/user/preferences/${preferenceID}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getUserPreference;
|
||||||
28
frontend/src/api/preferences/updateOrgPreference.ts
Normal file
28
frontend/src/api/preferences/updateOrgPreference.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
UpdateOrgPreferenceProps,
|
||||||
|
UpdateOrgPreferenceResponseProps,
|
||||||
|
} from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const updateOrgPreference = async (
|
||||||
|
preferencePayload: UpdateOrgPreferenceProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.put(
|
||||||
|
`/org/preferences/${preferencePayload.preferenceID}`,
|
||||||
|
{
|
||||||
|
preference_value: preferencePayload.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateOrgPreference;
|
||||||
25
frontend/src/api/preferences/updateUserPreference.ts
Normal file
25
frontend/src/api/preferences/updateUserPreference.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
UpdateUserPreferenceProps,
|
||||||
|
UpdateUserPreferenceResponseProps,
|
||||||
|
} from 'types/api/preferences/userOrgPreferences';
|
||||||
|
|
||||||
|
const updateUserPreference = async (
|
||||||
|
preferencePayload: UpdateUserPreferenceProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.put(`/user/preferences`, {
|
||||||
|
preference_value: preferencePayload.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateUserPreference;
|
||||||
18
frontend/src/api/user/inviteUsers.ts
Normal file
18
frontend/src/api/user/inviteUsers.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { InviteUsersResponse, UsersProps } from 'types/api/user/inviteUsers';
|
||||||
|
|
||||||
|
const inviteUsers = async (
|
||||||
|
users: UsersProps,
|
||||||
|
): Promise<SuccessResponse<InviteUsersResponse>> => {
|
||||||
|
const response = await axios.post(`/invite/bulk`, users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default inviteUsers;
|
||||||
@@ -1,46 +1,3 @@
|
|||||||
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 onboardingHelpMessage = (
|
export const onboardingHelpMessage = (
|
||||||
dataSourceName: string,
|
dataSourceName: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
@@ -55,35 +12,3 @@ Module: ${moduleId}
|
|||||||
|
|
||||||
Thanks
|
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`;
|
|
||||||
|
|
||||||
export const integrationsListMessage = `Hi Team,
|
|
||||||
|
|
||||||
I need help with Integrations.
|
|
||||||
|
|
||||||
Thanks`;
|
|
||||||
|
|
||||||
export const integrationDetailMessage = (
|
|
||||||
selectedIntegration: string,
|
|
||||||
): string => `
|
|
||||||
Hi Team,
|
|
||||||
|
|
||||||
I need help in configuring this integration.
|
|
||||||
|
|
||||||
Integration Id: ${selectedIntegration}
|
|
||||||
|
|
||||||
Thanks`;
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function LogDetail({
|
|||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
width="60%"
|
||||||
|
maskStyle={{ background: 'none' }}
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||||
|
|||||||
@@ -195,21 +195,20 @@ function ListLogView({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container
|
<Container
|
||||||
$isActiveLog={isHighlighted}
|
$isActiveLog={
|
||||||
|
isHighlighted ||
|
||||||
|
activeLog?.id === logData.id ||
|
||||||
|
activeContextLog?.id === logData.id
|
||||||
|
}
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
|
$logType={logType}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={handleDetailedView}
|
onClick={handleDetailedView}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
>
|
>
|
||||||
<div className="log-line">
|
<div className="log-line">
|
||||||
<LogStateIndicator
|
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||||
type={logType}
|
|
||||||
isActive={
|
|
||||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
|
||||||
}
|
|
||||||
fontSize={fontSize}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<LogContainer fontSize={fontSize}>
|
<LogContainer fontSize={fontSize}>
|
||||||
<LogGeneralField
|
<LogGeneralField
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { getActiveLogBackground } from 'utils/logs';
|
||||||
|
|
||||||
interface LogTextProps {
|
interface LogTextProps {
|
||||||
linesPerRow?: number;
|
linesPerRow?: number;
|
||||||
@@ -15,6 +15,7 @@ interface LogContainerProps {
|
|||||||
export const Container = styled(Card)<{
|
export const Container = styled(Card)<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
|
$logType: string;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
}>`
|
}>`
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -41,13 +42,8 @@ export const Container = styled(Card)<{
|
|||||||
? `padding:0.3rem 0.6rem;`
|
? `padding:0.3rem 0.6rem;`
|
||||||
: ``}
|
: ``}
|
||||||
|
|
||||||
${({ $isActiveLog, $isDarkMode }): string =>
|
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||||
$isActiveLog
|
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
|
||||||
? `background-color: ${
|
|
||||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
|
||||||
} !important`
|
|
||||||
: ''}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Text = styled(Typography.Text)`
|
export const Text = styled(Typography.Text)`
|
||||||
|
|||||||
@@ -41,10 +41,4 @@
|
|||||||
background-color: var(--bg-sakura-500);
|
background-color: var(--bg-sakura-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isActive {
|
|
||||||
.line {
|
|
||||||
background-color: var(--bg-robin-400, #7190f9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ describe('LogStateIndicator', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly when isActive is true', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
|
|
||||||
);
|
|
||||||
const indicator = container.firstChild as HTMLElement;
|
|
||||||
expect(indicator.classList.contains('isActive')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly with different types', () => {
|
it('renders correctly with different types', () => {
|
||||||
const { container: containerInfo } = render(
|
const { container: containerInfo } = render(
|
||||||
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
|
||||||
|
|||||||
@@ -44,22 +44,16 @@ export const LogType = {
|
|||||||
|
|
||||||
function LogStateIndicator({
|
function LogStateIndicator({
|
||||||
type,
|
type,
|
||||||
isActive,
|
|
||||||
fontSize,
|
fontSize,
|
||||||
}: {
|
}: {
|
||||||
type: string;
|
type: string;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
isActive?: boolean;
|
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
<div className="log-state-indicator">
|
||||||
<div className={cx('line', type, fontSize)}> </div>
|
<div className={cx('line', type, fontSize)}> </div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogStateIndicator.defaultProps = {
|
|
||||||
isActive: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogStateIndicator;
|
export default LogStateIndicator;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
body: 'Sample log Message',
|
body: 'Sample log Message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
|
scope_string: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
@@ -40,6 +41,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
body: 'Sample log Message',
|
body: 'Sample log Message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
|
scope_string: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
@@ -62,6 +64,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
body: 'Sample log Message',
|
body: 'Sample log Message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
|
scope_string: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
@@ -83,6 +86,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
body: 'Sample log',
|
body: 'Sample log',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
|
scope_string: {},
|
||||||
attributes_string: {
|
attributes_string: {
|
||||||
log_level: 'INFO' as never,
|
log_level: 'INFO' as never,
|
||||||
},
|
},
|
||||||
@@ -112,6 +116,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
attributesString: {},
|
attributesString: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
|
scope_string: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
severity_text: 'WARN',
|
severity_text: 'WARN',
|
||||||
};
|
};
|
||||||
@@ -130,6 +135,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
severity_number: 0,
|
severity_number: 0,
|
||||||
body: 'Sample log message',
|
body: 'Sample log message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
|
scope_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
@@ -166,6 +172,7 @@ describe('logIndicatorBySeverityNumber', () => {
|
|||||||
body: 'Sample log Message',
|
body: 'Sample log Message',
|
||||||
resources_string: {},
|
resources_string: {},
|
||||||
attributesString: {},
|
attributesString: {},
|
||||||
|
scope_string: {},
|
||||||
attributes_string: {},
|
attributes_string: {},
|
||||||
attributesInt: {},
|
attributesInt: {},
|
||||||
attributesFloat: {},
|
attributesFloat: {},
|
||||||
|
|||||||
@@ -162,20 +162,15 @@ function RawLogView({
|
|||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
$isHightlightedLog={isHighlighted}
|
$isHightlightedLog={isHighlighted}
|
||||||
$isActiveLog={isActiveLog}
|
$isActiveLog={
|
||||||
|
activeLog?.id === data.id || activeContextLog?.id === data.id || isActiveLog
|
||||||
|
}
|
||||||
|
$logType={logType}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
>
|
>
|
||||||
<LogStateIndicator
|
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||||
type={logType}
|
|
||||||
isActive={
|
|
||||||
activeLog?.id === data.id ||
|
|
||||||
activeContextLog?.id === data.id ||
|
|
||||||
isActiveLog
|
|
||||||
}
|
|
||||||
fontSize={fontSize}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RawLogContent
|
<RawLogContent
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
$isHightlightedLog: boolean;
|
$isHightlightedLog: boolean;
|
||||||
|
$logType: string;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
}>`
|
}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -34,11 +35,12 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
: `margin: 2px 0;`}
|
: `margin: 2px 0;`}
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
${({ $isActiveLog, $logType }): string =>
|
||||||
|
getActiveLogBackground($isActiveLog, true, $logType)}
|
||||||
|
|
||||||
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||||
|
|
||||||
${({ $isHightlightedLog, $isDarkMode }): string =>
|
${({ $isHightlightedLog, $isDarkMode }): string =>
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
linesPerRow,
|
linesPerRow,
|
||||||
fontSize,
|
fontSize,
|
||||||
appendTo = 'center',
|
appendTo = 'center',
|
||||||
activeContextLog,
|
|
||||||
activeLog,
|
|
||||||
isListViewPanel,
|
isListViewPanel,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -90,9 +88,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
<div className="table-timestamp">
|
<div className="table-timestamp">
|
||||||
<LogStateIndicator
|
<LogStateIndicator
|
||||||
type={getLogIndicatorTypeForTable(item)}
|
type={getLogIndicatorTypeForTable(item)}
|
||||||
isActive={
|
|
||||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
|
||||||
}
|
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||||
@@ -130,16 +125,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
},
|
},
|
||||||
...(appendTo === 'end' ? fieldColumns : []),
|
...(appendTo === 'end' ? fieldColumns : []),
|
||||||
];
|
];
|
||||||
}, [
|
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
|
||||||
fields,
|
|
||||||
isListViewPanel,
|
|
||||||
appendTo,
|
|
||||||
isDarkMode,
|
|
||||||
linesPerRow,
|
|
||||||
activeLog?.id,
|
|
||||||
activeContextLog?.id,
|
|
||||||
fontSize,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ function DynamicColumnTable({
|
|||||||
className="dynamicColumnTable-button filter-btn"
|
className="dynamicColumnTable-button filter-btn"
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<SlidersHorizontal size={14} />}
|
icon={<SlidersHorizontal size={14} />}
|
||||||
|
data-testid="additional-filters-button"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
|
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
|
||||||
|
[AlertTypes.ANOMALY_BASED_ALERT]: DataSource.METRICS,
|
||||||
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
|
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
|
||||||
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
|
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
|
||||||
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
|
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ export enum FeatureKeys {
|
|||||||
GATEWAY = 'GATEWAY',
|
GATEWAY = 'GATEWAY',
|
||||||
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||||
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
||||||
|
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,4 +36,9 @@ export enum QueryParams {
|
|||||||
topic = 'topic',
|
topic = 'topic',
|
||||||
partition = 'partition',
|
partition = 'partition',
|
||||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||||
|
ruleType = 'ruleType',
|
||||||
|
configDetail = 'configDetail',
|
||||||
|
getStartedSource = 'getStartedSource',
|
||||||
|
getStartedSourceService = 'getStartedSourceService',
|
||||||
|
mqServiceView = 'mqServiceView',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
|||||||
value: QueryFunctionsTypes.TIME_SHIFT,
|
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||||
label: 'Time Shift',
|
label: 'Time Shift',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
label: 'Time Shift',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
|
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||||
@@ -80,10 +84,15 @@ interface QueryFunctionConfigType {
|
|||||||
showInput: boolean;
|
showInput: boolean;
|
||||||
inputType?: string;
|
inputType?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
||||||
|
anomaly: {
|
||||||
|
showInput: false,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
cutOffMin: {
|
cutOffMin: {
|
||||||
showInput: true,
|
showInput: true,
|
||||||
inputType: 'text',
|
inputType: 'text',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ROUTES = {
|
|||||||
TRACE_DETAIL: '/trace/:id',
|
TRACE_DETAIL: '/trace/:id',
|
||||||
TRACES_EXPLORER: '/traces-explorer',
|
TRACES_EXPLORER: '/traces-explorer',
|
||||||
GET_STARTED: '/get-started',
|
GET_STARTED: '/get-started',
|
||||||
|
ONBOARDING: '/onboarding',
|
||||||
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
|
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
|
||||||
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
|
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
|
||||||
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
.anomaly-alert-evaluation-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-chart-section {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.has-multi-series-data {
|
||||||
|
width: calc(100% - 240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-no-data-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 240px;
|
||||||
|
padding: 0px 8px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-title {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-item-color {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot {
|
||||||
|
.u-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-legend {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.u-series {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip {
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 8px 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 500px;
|
||||||
|
width: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: none; /* Hide tooltip by default */
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip-series {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip-series-name {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip-band {
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-tooltip-marker {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import 'uplot/dist/uPlot.min.css';
|
||||||
|
import './AnomalyAlertEvaluationView.styles.scss';
|
||||||
|
|
||||||
|
import { Checkbox, Typography } from 'antd';
|
||||||
|
import Search from 'antd/es/input/Search';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import getAxes from 'lib/uPlotLib/utils/getAxes';
|
||||||
|
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||||
|
import { LineChart } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
import tooltipPlugin from './tooltipPlugin';
|
||||||
|
|
||||||
|
function UplotChart({
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
chartRef,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
options: any;
|
||||||
|
chartRef: any;
|
||||||
|
}): JSX.Element {
|
||||||
|
const plotInstance = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (plotInstance.current) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
plotInstance.current.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
plotInstance.current = new uPlot(options, data, chartRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
if (plotInstance.current) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
plotInstance.current.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [data, options, chartRef]);
|
||||||
|
|
||||||
|
return <div ref={chartRef} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnomalyAlertEvaluationView({
|
||||||
|
data,
|
||||||
|
yAxisUnit,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
yAxisUnit: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { spline } = uPlot.paths;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const _spline = spline ? spline() : undefined;
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const [seriesData, setSeriesData] = useState<any>({});
|
||||||
|
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [filteredSeriesKeys, setFilteredSeriesKeys] = useState<string[]>([]);
|
||||||
|
const [allSeries, setAllSeries] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const chartData = getUplotChartDataForAnomalyDetection(data, isDarkMode);
|
||||||
|
setSeriesData(chartData);
|
||||||
|
|
||||||
|
setAllSeries(Object.keys(chartData));
|
||||||
|
|
||||||
|
setFilteredSeriesKeys(Object.keys(chartData));
|
||||||
|
}, [data, isDarkMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const seriesKeys = Object.keys(seriesData);
|
||||||
|
if (seriesKeys.length === 1) {
|
||||||
|
setSelectedSeries(seriesKeys[0]); // Automatically select if only one series
|
||||||
|
} else {
|
||||||
|
setSelectedSeries(null); // Default to "Show All" if multiple series
|
||||||
|
}
|
||||||
|
}, [seriesData]);
|
||||||
|
|
||||||
|
const handleSeriesChange = (series: string | null): void => {
|
||||||
|
setSelectedSeries(series);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bandsPlugin = {
|
||||||
|
hooks: {
|
||||||
|
draw: [
|
||||||
|
(u: any): void => {
|
||||||
|
if (!selectedSeries) return;
|
||||||
|
|
||||||
|
const { ctx } = u;
|
||||||
|
const upperBandIdx = 3;
|
||||||
|
const lowerBandIdx = 4;
|
||||||
|
|
||||||
|
const xData = u.data[0];
|
||||||
|
const yUpperData = u.data[upperBandIdx];
|
||||||
|
const yLowerData = u.data[lowerBandIdx];
|
||||||
|
|
||||||
|
const strokeStyle =
|
||||||
|
u.series[1]?.stroke || seriesData[selectedSeries].color;
|
||||||
|
const fillStyle =
|
||||||
|
typeof strokeStyle === 'string'
|
||||||
|
? strokeStyle.replace(')', ', 0.1)')
|
||||||
|
: 'rgba(255, 255, 255, 0.1)';
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
const firstX = u.valToPos(xData[0], 'x', true);
|
||||||
|
const firstUpperY = u.valToPos(yUpperData[0], 'y', true);
|
||||||
|
ctx.moveTo(firstX, firstUpperY);
|
||||||
|
|
||||||
|
for (let i = 0; i < xData.length; i++) {
|
||||||
|
const x = u.valToPos(xData[i], 'x', true);
|
||||||
|
const y = u.valToPos(yUpperData[i], 'y', true);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = xData.length - 1; i >= 0; i--) {
|
||||||
|
const x = u.valToPos(xData[i], 'x', true);
|
||||||
|
const y = u.valToPos(yLowerData[i], 'y', true);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = fillStyle;
|
||||||
|
ctx.fill();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialData = allSeries.length
|
||||||
|
? [
|
||||||
|
seriesData[allSeries[0]].data[0], // Shared X-axis
|
||||||
|
...allSeries.map((key) => seriesData[key].data[1]), // Map through Y-axis data for all series
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height - 36,
|
||||||
|
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
|
||||||
|
focus: {
|
||||||
|
alpha: 0.3,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true,
|
||||||
|
live: false,
|
||||||
|
isolate: true,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
lock: false,
|
||||||
|
focus: {
|
||||||
|
prox: 1e6,
|
||||||
|
bias: 1,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
size: (
|
||||||
|
u: { series: { [x: string]: { points: { size: number } } } },
|
||||||
|
seriesIdx: string | number,
|
||||||
|
): number => u.series[seriesIdx].points.size * 3,
|
||||||
|
width: (u: any, seriesIdx: any, size: number): number => size / 4,
|
||||||
|
stroke: (
|
||||||
|
u: {
|
||||||
|
series: {
|
||||||
|
[x: string]: { points: { stroke: (arg0: any, arg1: any) => any } };
|
||||||
|
};
|
||||||
|
},
|
||||||
|
seriesIdx: string | number,
|
||||||
|
): string => `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
||||||
|
fill: (): string => '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: 'Time',
|
||||||
|
},
|
||||||
|
...(selectedSeries
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: `Main Series`,
|
||||||
|
stroke: seriesData[selectedSeries].color,
|
||||||
|
width: 2,
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
spanGaps: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Predicted Value`,
|
||||||
|
stroke: seriesData[selectedSeries].color,
|
||||||
|
width: 1,
|
||||||
|
dash: [2, 2],
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
spanGaps: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Upper Band`,
|
||||||
|
stroke: 'transparent',
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
spanGaps: true,
|
||||||
|
points: {
|
||||||
|
show: false,
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Lower Band`,
|
||||||
|
stroke: 'transparent',
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
spanGaps: true,
|
||||||
|
points: {
|
||||||
|
show: false,
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: allSeries.map((seriesKey) => ({
|
||||||
|
label: seriesKey,
|
||||||
|
stroke: seriesData[seriesKey].color,
|
||||||
|
width: 2,
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
spanGaps: true,
|
||||||
|
}))),
|
||||||
|
],
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: true,
|
||||||
|
spanGaps: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
...getYAxisScaleForAnomalyDetection({
|
||||||
|
seriesData,
|
||||||
|
selectedSeries,
|
||||||
|
initialData,
|
||||||
|
yAxisUnit,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (searchText: string): void => {
|
||||||
|
if (!searchText || searchText.length === 0) {
|
||||||
|
setFilteredSeriesKeys(allSeries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredSeries = allSeries.filter((series) =>
|
||||||
|
series.toLowerCase().includes(searchText.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilteredSeriesKeys(filteredSeries);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchValueChange = useDebouncedFn((event): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const value = event?.target?.value || '';
|
||||||
|
|
||||||
|
handleSearch(value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="anomaly-alert-evaluation-view">
|
||||||
|
<div
|
||||||
|
className={`anomaly-alert-evaluation-view-chart-section ${
|
||||||
|
allSeries.length > 1 ? 'has-multi-series-data' : ''
|
||||||
|
}`}
|
||||||
|
ref={graphRef}
|
||||||
|
>
|
||||||
|
{allSeries.length > 0 ? (
|
||||||
|
<UplotChart
|
||||||
|
data={selectedSeries ? seriesData[selectedSeries].data : initialData}
|
||||||
|
options={options}
|
||||||
|
chartRef={chartRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="anomaly-alert-evaluation-view-no-data-container">
|
||||||
|
<LineChart size={48} strokeWidth={0.5} />
|
||||||
|
|
||||||
|
<Typography>No Data</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{allSeries.length > 1 && (
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-selection">
|
||||||
|
{allSeries.length > 1 && (
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-list">
|
||||||
|
<Search
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-search"
|
||||||
|
placeholder="Search a series"
|
||||||
|
allowClear
|
||||||
|
onChange={handleSearchValueChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-list-items">
|
||||||
|
{filteredSeriesKeys.length > 0 && (
|
||||||
|
<Checkbox
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-item"
|
||||||
|
type="checkbox"
|
||||||
|
name="series"
|
||||||
|
value="all"
|
||||||
|
checked={selectedSeries === null}
|
||||||
|
onChange={(): void => handleSeriesChange(null)}
|
||||||
|
>
|
||||||
|
Show All
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredSeriesKeys.map((seriesKey) => (
|
||||||
|
<div key={seriesKey}>
|
||||||
|
<Checkbox
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-item"
|
||||||
|
key={seriesKey}
|
||||||
|
type="checkbox"
|
||||||
|
name="series"
|
||||||
|
value={seriesKey}
|
||||||
|
checked={selectedSeries === seriesKey}
|
||||||
|
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-item-color"
|
||||||
|
style={{ backgroundColor: seriesData[seriesKey].color }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{seriesKey}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredSeriesKeys.length === 0 && (
|
||||||
|
<Typography>No series found</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnomalyAlertEvaluationView;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import AnomalyAlertEvaluationView from './AnomalyAlertEvaluationView';
|
||||||
|
|
||||||
|
export default AnomalyAlertEvaluationView;
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
|
||||||
|
const tooltipPlugin = (
|
||||||
|
isDarkMode: boolean,
|
||||||
|
): { hooks: { init: (u: any) => void } } => {
|
||||||
|
let tooltip: HTMLDivElement;
|
||||||
|
const tooltipLeftOffset = 10;
|
||||||
|
const tooltipTopOffset = 10;
|
||||||
|
let isMouseOverPlot = false;
|
||||||
|
|
||||||
|
function formatValue(value: string | number | Date): string | number | Date {
|
||||||
|
if (typeof value === 'string' && !Number.isNaN(parseFloat(value))) {
|
||||||
|
return parseFloat(value).toFixed(3);
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value.toFixed(3);
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toLocaleString();
|
||||||
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTooltip(u: any, left: number, top: number): void {
|
||||||
|
const idx = u.posToIdx(left);
|
||||||
|
const xVal = u.data[0][idx];
|
||||||
|
|
||||||
|
if (xVal == null) {
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xDate = new Date(xVal * 1000);
|
||||||
|
const formattedXDate = formatValue(xDate);
|
||||||
|
|
||||||
|
let tooltipContent = `<div class="uplot-tooltip-title">Time: ${formattedXDate}</div>`;
|
||||||
|
|
||||||
|
let mainValue;
|
||||||
|
let upperBand;
|
||||||
|
let lowerBand;
|
||||||
|
|
||||||
|
let color = null;
|
||||||
|
|
||||||
|
// Loop through all series (excluding the x-axis series)
|
||||||
|
for (let i = 1; i < u.series.length; i++) {
|
||||||
|
const series = u.series[i];
|
||||||
|
|
||||||
|
const yVal = u.data[i][idx];
|
||||||
|
const formattedYVal = formatValue(yVal);
|
||||||
|
|
||||||
|
color = generateColor(
|
||||||
|
series.label,
|
||||||
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the round marker for the series
|
||||||
|
const marker = `<span class="uplot-tooltip-marker" style="background-color: ${color};"></span>`;
|
||||||
|
|
||||||
|
if (series.label.toLowerCase().includes('upper band')) {
|
||||||
|
upperBand = formattedYVal;
|
||||||
|
} else if (series.label.toLowerCase().includes('lower band')) {
|
||||||
|
lowerBand = formattedYVal;
|
||||||
|
} else if (series.label.toLowerCase().includes('main series')) {
|
||||||
|
mainValue = formattedYVal;
|
||||||
|
} else {
|
||||||
|
tooltipContent += `
|
||||||
|
<div class="uplot-tooltip-series">
|
||||||
|
${marker}
|
||||||
|
<span class="uplot-tooltip-series-name">${series.label}:</span>
|
||||||
|
<span class="uplot-tooltip-series-value">${formattedYVal}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add main value, upper band, and lower band to the tooltip
|
||||||
|
if (mainValue !== undefined) {
|
||||||
|
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||||
|
tooltipContent += `
|
||||||
|
<div class="uplot-tooltip-series">
|
||||||
|
${marker}
|
||||||
|
<span class="uplot-tooltip-series-name">Main Series:</span>
|
||||||
|
<span class="uplot-tooltip-series-value">${mainValue}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (upperBand !== undefined) {
|
||||||
|
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||||
|
tooltipContent += `
|
||||||
|
<div class="uplot-tooltip-series">
|
||||||
|
${marker}
|
||||||
|
<span class="uplot-tooltip-series-name">Upper Band:</span>
|
||||||
|
<span class="uplot-tooltip-series-value">${upperBand}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (lowerBand !== undefined) {
|
||||||
|
const marker = `<span class="uplot-tooltip-marker"></span>`;
|
||||||
|
tooltipContent += `
|
||||||
|
<div class="uplot-tooltip-series">
|
||||||
|
${marker}
|
||||||
|
<span class="uplot-tooltip-series-name">Lower Band:</span>
|
||||||
|
<span class="uplot-tooltip-series-value">${lowerBand}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.innerHTML = tooltipContent;
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
tooltip.style.left = `${left + tooltipLeftOffset}px`;
|
||||||
|
tooltip.style.top = `${top + tooltipTopOffset}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(u: any): void {
|
||||||
|
tooltip = document.createElement('div');
|
||||||
|
tooltip.className = 'uplot-tooltip';
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
u.over.appendChild(tooltip);
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
u.over.addEventListener('mouseenter', () => {
|
||||||
|
isMouseOverPlot = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
u.over.addEventListener('mouseleave', () => {
|
||||||
|
isMouseOverPlot = false;
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
u.over.addEventListener('mousemove', (e: MouseEvent) => {
|
||||||
|
if (isMouseOverPlot) {
|
||||||
|
const rect = u.over.getBoundingClientRect();
|
||||||
|
const left = e.clientX - rect.left;
|
||||||
|
const top = e.clientY - rect.top;
|
||||||
|
updateTooltip(u, left, top);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
init,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tooltipPlugin;
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const pageTitle = t(routeKey);
|
const pageTitle = t(routeKey);
|
||||||
const renderFullScreen =
|
const renderFullScreen =
|
||||||
pathname === ROUTES.GET_STARTED ||
|
pathname === ROUTES.GET_STARTED ||
|
||||||
|
pathname === ROUTES.ONBOARDING ||
|
||||||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||||
@@ -211,6 +212,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [licenseData, isFetching]);
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// after logging out hide the trial expiry banner
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
setShowTrialExpiryBanner(false);
|
||||||
|
}
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
const handleUpgrade = (): void => {
|
const handleUpgrade = (): void => {
|
||||||
if (role === 'ADMIN') {
|
if (role === 'ADMIN') {
|
||||||
history.push(ROUTES.BILLING);
|
history.push(ROUTES.BILLING);
|
||||||
|
|||||||
@@ -3,25 +3,41 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
|||||||
|
|
||||||
import { OptionType } from './types';
|
import { OptionType } from './types';
|
||||||
|
|
||||||
export const getOptionList = (t: TFunction): OptionType[] => [
|
export const getOptionList = (
|
||||||
{
|
t: TFunction,
|
||||||
title: t('metric_based_alert'),
|
isAnomalyDetectionEnabled: boolean,
|
||||||
selection: AlertTypes.METRICS_BASED_ALERT,
|
): OptionType[] => {
|
||||||
description: t('metric_based_alert_desc'),
|
const optionList: OptionType[] = [
|
||||||
},
|
{
|
||||||
{
|
title: t('metric_based_alert'),
|
||||||
title: t('log_based_alert'),
|
selection: AlertTypes.METRICS_BASED_ALERT,
|
||||||
selection: AlertTypes.LOGS_BASED_ALERT,
|
description: t('metric_based_alert_desc'),
|
||||||
description: t('log_based_alert_desc'),
|
},
|
||||||
},
|
{
|
||||||
{
|
title: t('log_based_alert'),
|
||||||
title: t('traces_based_alert'),
|
selection: AlertTypes.LOGS_BASED_ALERT,
|
||||||
selection: AlertTypes.TRACES_BASED_ALERT,
|
description: t('log_based_alert_desc'),
|
||||||
description: t('traces_based_alert_desc'),
|
},
|
||||||
},
|
{
|
||||||
{
|
title: t('traces_based_alert'),
|
||||||
title: t('exceptions_based_alert'),
|
selection: AlertTypes.TRACES_BASED_ALERT,
|
||||||
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
description: t('traces_based_alert_desc'),
|
||||||
description: t('exceptions_based_alert_desc'),
|
},
|
||||||
},
|
{
|
||||||
];
|
title: t('exceptions_based_alert'),
|
||||||
|
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||||
|
description: t('exceptions_based_alert_desc'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isAnomalyDetectionEnabled) {
|
||||||
|
optionList.unshift({
|
||||||
|
title: t('anomaly_based_alert'),
|
||||||
|
selection: AlertTypes.ANOMALY_BASED_ALERT,
|
||||||
|
description: t('anomaly_based_alert_desc'),
|
||||||
|
isBeta: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionList;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Row, Typography } from 'antd';
|
import { Row, Tag, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -12,11 +14,18 @@ import { OptionType } from './types';
|
|||||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||||
const { t } = useTranslation(['alerts']);
|
const { t } = useTranslation(['alerts']);
|
||||||
|
|
||||||
const optionList = getOptionList(t);
|
const isAnomalyDetectionEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
|
|
||||||
|
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
||||||
|
|
||||||
function handleRedirection(option: AlertTypes): void {
|
function handleRedirection(option: AlertTypes): void {
|
||||||
let url = '';
|
let url = '';
|
||||||
switch (option) {
|
switch (option) {
|
||||||
|
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||||
|
url =
|
||||||
|
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
||||||
|
break;
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
case AlertTypes.METRICS_BASED_ALERT:
|
||||||
url =
|
url =
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
||||||
@@ -52,6 +61,13 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
|||||||
<AlertTypeCard
|
<AlertTypeCard
|
||||||
key={option.selection}
|
key={option.selection}
|
||||||
title={option.title}
|
title={option.title}
|
||||||
|
extra={
|
||||||
|
option.isBeta ? (
|
||||||
|
<Tag bordered={false} color="geekblue">
|
||||||
|
Beta
|
||||||
|
</Tag>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
onSelect(option.selection);
|
onSelect(option.selection);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface OptionType {
|
|||||||
title: string;
|
title: string;
|
||||||
selection: AlertTypes;
|
selection: AlertTypes;
|
||||||
description: string;
|
description: string;
|
||||||
|
isBeta?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import {
|
|||||||
initialQueryPromQLData,
|
initialQueryPromQLData,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
|
defaultAlgorithm,
|
||||||
defaultCompareOp,
|
defaultCompareOp,
|
||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
|
defaultSeasonality,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
@@ -46,6 +49,52 @@ export const alertDefaults: AlertDef = {
|
|||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
matchType: defaultMatchType,
|
matchType: defaultMatchType,
|
||||||
|
algorithm: defaultAlgorithm,
|
||||||
|
seasonality: defaultSeasonality,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
severity: 'warning',
|
||||||
|
},
|
||||||
|
annotations: defaultAnnotations,
|
||||||
|
evalWindow: defaultEvalWindow,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const anamolyAlertDefaults: AlertDef = {
|
||||||
|
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
condition: {
|
||||||
|
compositeQuery: {
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
...initialQueryBuilderFormValuesMap.metrics,
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: 'anomaly',
|
||||||
|
args: [],
|
||||||
|
namedArgs: { z_score_threshold: 3 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
promQueries: { A: initialQueryPromQLData },
|
||||||
|
chQueries: {
|
||||||
|
A: {
|
||||||
|
name: 'A',
|
||||||
|
query: ``,
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
unit: undefined,
|
||||||
|
},
|
||||||
|
op: defaultCompareOp,
|
||||||
|
matchType: defaultMatchType,
|
||||||
|
algorithm: defaultAlgorithm,
|
||||||
|
seasonality: defaultSeasonality,
|
||||||
|
target: 3,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
@@ -56,6 +105,7 @@ export const alertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const logAlertDefaults: AlertDef = {
|
export const logAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.LOGS_BASED_ALERT,
|
alertType: AlertTypes.LOGS_BASED_ALERT,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@@ -86,6 +136,7 @@ export const logAlertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const traceAlertDefaults: AlertDef = {
|
export const traceAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.TRACES_BASED_ALERT,
|
alertType: AlertTypes.TRACES_BASED_ALERT,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@@ -116,6 +167,7 @@ export const traceAlertDefaults: AlertDef = {
|
|||||||
|
|
||||||
export const exceptionAlertDefaults: AlertDef = {
|
export const exceptionAlertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@@ -145,6 +197,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||||
|
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
|
||||||
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
||||||
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
||||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Form, Row } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -13,6 +13,7 @@ import { AlertDef } from 'types/api/alerts/def';
|
|||||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||||
import {
|
import {
|
||||||
alertDefaults,
|
alertDefaults,
|
||||||
|
anamolyAlertDefaults,
|
||||||
exceptionAlertDefaults,
|
exceptionAlertDefaults,
|
||||||
logAlertDefaults,
|
logAlertDefaults,
|
||||||
traceAlertDefaults,
|
traceAlertDefaults,
|
||||||
@@ -24,8 +25,12 @@ function CreateRules(): JSX.Element {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||||
const version = queryParams.get('version');
|
const version = queryParams.get('version');
|
||||||
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
|
const alertTypeFromParams =
|
||||||
|
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
? AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
: queryParams.get(QueryParams.alertType);
|
||||||
|
|
||||||
const compositeQuery = useGetCompositeQueryParam();
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
function getAlertTypeFromDataSource(): AlertTypes | null {
|
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||||
@@ -45,6 +50,7 @@ function CreateRules(): JSX.Element {
|
|||||||
|
|
||||||
const onSelectType = (typ: AlertTypes): void => {
|
const onSelectType = (typ: AlertTypes): void => {
|
||||||
setAlertType(typ);
|
setAlertType(typ);
|
||||||
|
|
||||||
switch (typ) {
|
switch (typ) {
|
||||||
case AlertTypes.LOGS_BASED_ALERT:
|
case AlertTypes.LOGS_BASED_ALERT:
|
||||||
setInitValues(logAlertDefaults);
|
setInitValues(logAlertDefaults);
|
||||||
@@ -55,13 +61,40 @@ function CreateRules(): JSX.Element {
|
|||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||||
setInitValues(exceptionAlertDefaults);
|
setInitValues(exceptionAlertDefaults);
|
||||||
break;
|
break;
|
||||||
|
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||||
|
setInitValues({
|
||||||
|
...anamolyAlertDefaults,
|
||||||
|
version: version || ENTITY_VERSION_V4,
|
||||||
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
setInitValues({
|
setInitValues({
|
||||||
...alertDefaults,
|
...alertDefaults,
|
||||||
version: version || ENTITY_VERSION_V4,
|
version: version || ENTITY_VERSION_V4,
|
||||||
|
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
queryParams.set(QueryParams.alertType, typ);
|
|
||||||
|
queryParams.set(
|
||||||
|
QueryParams.alertType,
|
||||||
|
typ === AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
? AlertTypes.METRICS_BASED_ALERT
|
||||||
|
: typ,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typ === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||||
|
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
) {
|
||||||
|
queryParams.set(
|
||||||
|
QueryParams.ruleType,
|
||||||
|
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||||
|
}
|
||||||
|
|
||||||
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||||
history.replace(generatedUrl);
|
history.replace(generatedUrl);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,18 +7,16 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
|||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<FormAlertRules
|
||||||
<FormAlertRules
|
alertType={
|
||||||
alertType={
|
initialValue.alertType
|
||||||
initialValue.alertType
|
? (initialValue.alertType as AlertTypes)
|
||||||
? (initialValue.alertType as AlertTypes)
|
: AlertTypes.METRICS_BASED_ALERT
|
||||||
: AlertTypes.METRICS_BASED_ALERT
|
}
|
||||||
}
|
formInstance={formInstance}
|
||||||
formInstance={formInstance}
|
initialValue={initialValue}
|
||||||
initialValue={initialValue}
|
ruleId={ruleId}
|
||||||
ruleId={ruleId}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 10px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: rgba(22, 24, 29, 0.6);
|
background: rgba(22, 24, 29, 0.6);
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-slate-500);
|
background: var(--bg-slate-500);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import {
|
|||||||
PanelBottomClose,
|
PanelBottomClose,
|
||||||
Plus,
|
Plus,
|
||||||
X,
|
X,
|
||||||
XCircle,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
@@ -515,7 +514,11 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="explorer-options-container">
|
<div className="explorer-options-container">
|
||||||
{isQueryUpdated && !isExplorerOptionHidden && (
|
{
|
||||||
|
// if a viewName is selected and the explorer options are not hidden then
|
||||||
|
// always show the clear option
|
||||||
|
}
|
||||||
|
{!isExplorerOptionHidden && viewName && (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
isEditDeleteSupported ? '' : 'hide-update',
|
isEditDeleteSupported ? '' : 'hide-update',
|
||||||
@@ -529,18 +532,25 @@ function ExplorerOptions({
|
|||||||
icon={<X size={14} />}
|
icon={<X size={14} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Divider
|
{
|
||||||
type="vertical"
|
// only show the update view option when the query is updated
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
}
|
||||||
/>
|
{isQueryUpdated && (
|
||||||
<Tooltip title="Update this view" placement="top">
|
<>
|
||||||
<Button
|
<Divider
|
||||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
type="vertical"
|
||||||
disabled={isViewUpdating}
|
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||||
onClick={onUpdateQueryHandler}
|
/>
|
||||||
icon={<Disc3 size={14} />}
|
<Tooltip title="Update this view" placement="top">
|
||||||
/>
|
<Button
|
||||||
</Tooltip>
|
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||||
|
disabled={isViewUpdating}
|
||||||
|
onClick={onUpdateQueryHandler}
|
||||||
|
icon={<Disc3 size={14} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isExplorerOptionHidden && (
|
{!isExplorerOptionHidden && (
|
||||||
@@ -564,10 +574,7 @@ function ExplorerOptions({
|
|||||||
}}
|
}}
|
||||||
dropdownStyle={dropdownStyle}
|
dropdownStyle={dropdownStyle}
|
||||||
className="views-dropdown"
|
className="views-dropdown"
|
||||||
allowClear={{
|
allowClear={false}
|
||||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
|
||||||
}}
|
|
||||||
onClear={handleClearSelect}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{viewsData?.data?.data?.map((view) => {
|
{viewsData?.data?.data?.map((view) => {
|
||||||
@@ -662,8 +669,8 @@ function ExplorerOptions({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ExplorerOptionsHideArea
|
<ExplorerOptionsHideArea
|
||||||
|
viewName={viewName}
|
||||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||||
sourcepage={sourcepage}
|
sourcepage={sourcepage}
|
||||||
@@ -672,7 +679,6 @@ function ExplorerOptions({
|
|||||||
onUpdateQueryHandler={onUpdateQueryHandler}
|
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||||
isEditDeleteSupported={isEditDeleteSupported}
|
isEditDeleteSupported={isEditDeleteSupported}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
className="save-view-modal"
|
className="save-view-modal"
|
||||||
title={<span className="title">Save this view</span>}
|
title={<span className="title">Save this view</span>}
|
||||||
@@ -705,7 +711,6 @@ function ExplorerOptions({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
footer={null}
|
footer={null}
|
||||||
onOk={onCancel(false)}
|
onOk={onCancel(false)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { setExplorerToolBarVisibility } from './utils';
|
import { setExplorerToolBarVisibility } from './utils';
|
||||||
|
|
||||||
interface DroppableAreaProps {
|
interface DroppableAreaProps {
|
||||||
|
viewName: string;
|
||||||
isQueryUpdated: boolean;
|
isQueryUpdated: boolean;
|
||||||
isExplorerOptionHidden?: boolean;
|
isExplorerOptionHidden?: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
@@ -20,6 +21,7 @@ interface DroppableAreaProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ExplorerOptionsHideArea({
|
function ExplorerOptionsHideArea({
|
||||||
|
viewName,
|
||||||
isQueryUpdated,
|
isQueryUpdated,
|
||||||
isExplorerOptionHidden,
|
isExplorerOptionHidden,
|
||||||
sourcepage,
|
sourcepage,
|
||||||
@@ -39,7 +41,7 @@ function ExplorerOptionsHideArea({
|
|||||||
<div className="explorer-option-droppable-container">
|
<div className="explorer-option-droppable-container">
|
||||||
{isExplorerOptionHidden && (
|
{isExplorerOptionHidden && (
|
||||||
<>
|
<>
|
||||||
{isQueryUpdated && (
|
{viewName && (
|
||||||
<div className="explorer-actions-btn">
|
<div className="explorer-actions-btn">
|
||||||
<Tooltip title="Clear this view">
|
<Tooltip title="Clear this view">
|
||||||
<Button
|
<Button
|
||||||
@@ -49,7 +51,7 @@ function ExplorerOptionsHideArea({
|
|||||||
icon={<X size={14} color={Color.BG_INK_500} />}
|
icon={<X size={14} color={Color.BG_INK_500} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isEditDeleteSupported && (
|
{isEditDeleteSupported && isQueryUpdated && (
|
||||||
<Tooltip title="Update this View">
|
<Tooltip title="Update this View">
|
||||||
<Button
|
<Button
|
||||||
onClick={onUpdateQueryHandler}
|
onClick={onUpdateQueryHandler}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function BasicInfo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
<StepHeading> {t('alert_form_step4')} </StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('field_severity')}
|
label={t('field_severity')}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.alert-chart-container {
|
||||||
|
height: 57vh;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.threshold-alert-uplot-chart-container {
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-loading-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-error-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import './ChartPreview.styles.scss';
|
||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
@@ -14,6 +18,7 @@ import {
|
|||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import getTimeString from 'lib/getTimeString';
|
import getTimeString from 'lib/getTimeString';
|
||||||
@@ -34,6 +39,7 @@ import { getGraphType } from 'utils/getGraphType';
|
|||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
|
import { AlertDetectionTypes } from '..';
|
||||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||||
import { getThresholdLabel } from './utils';
|
import { getThresholdLabel } from './utils';
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ function ChartPreview({
|
|||||||
selectedInterval,
|
selectedInterval,
|
||||||
minTime,
|
minTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
|
alertDef?.ruleType,
|
||||||
],
|
],
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled: canQuery,
|
enabled: canQuery,
|
||||||
@@ -163,8 +170,6 @@ function ChartPreview({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
|
||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@@ -202,7 +207,10 @@ function ChartPreview({
|
|||||||
id: 'alert_legend_widget',
|
id: 'alert_legend_widget',
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
apiResponse: queryResponse?.data?.payload,
|
apiResponse: queryResponse?.data?.payload,
|
||||||
dimensions: containerDimensions,
|
dimensions: {
|
||||||
|
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
|
||||||
|
width: containerDimensions?.width,
|
||||||
|
},
|
||||||
minTimeScale,
|
minTimeScale,
|
||||||
maxTimeScale,
|
maxTimeScale,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
@@ -245,36 +253,59 @@ function ChartPreview({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||||
|
|
||||||
|
const isAnomalyDetectionAlert =
|
||||||
|
alertDef?.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
|
||||||
|
|
||||||
|
const chartDataAvailable =
|
||||||
|
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
||||||
|
|
||||||
|
const isAnomalyDetectionEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer>
|
<div className="alert-chart-container" ref={graphRef}>
|
||||||
{headline}
|
<ChartContainer>
|
||||||
|
{headline}
|
||||||
|
|
||||||
<div ref={graphRef} style={{ height: '100%' }}>
|
<div className="threshold-alert-uplot-chart-container">
|
||||||
{queryResponse.isLoading && (
|
{queryResponse.isLoading && (
|
||||||
<Spinner size="large" tip="Loading..." height="100%" />
|
<Spinner size="large" tip="Loading..." height="100%" />
|
||||||
)}
|
)}
|
||||||
{(queryResponse?.isError || queryResponse?.error) && (
|
{(queryResponse?.isError || queryResponse?.error) && (
|
||||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||||
<InfoCircleOutlined />{' '}
|
<InfoCircleOutlined />
|
||||||
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
||||||
</FailedMessageContainer>
|
</FailedMessageContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chartData && !queryResponse.isError && !queryResponse.isLoading && (
|
{chartDataAvailable && !isAnomalyDetectionAlert && (
|
||||||
<GridPanelSwitch
|
<GridPanelSwitch
|
||||||
options={options}
|
options={options}
|
||||||
panelType={graphType}
|
panelType={graphType}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
name={name || 'Chart Preview'}
|
name={name || 'Chart Preview'}
|
||||||
panelData={
|
panelData={
|
||||||
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
||||||
}
|
}
|
||||||
query={query || initialQueriesMap.metrics}
|
query={query || initialQueriesMap.metrics}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</ChartContainer>
|
{chartDataAvailable &&
|
||||||
|
isAnomalyDetectionAlert &&
|
||||||
|
isAnomalyDetectionEnabled &&
|
||||||
|
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
|
||||||
|
<AnomalyAlertEvaluationView
|
||||||
|
data={queryResponse?.data?.payload}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.steps-container {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-chart-preview-container {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.alert-type-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.alert-type-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-preview-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-method-container {
|
||||||
|
margin: 24px 0;
|
||||||
|
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-method-description {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-help-btns {
|
.info-help-btns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ function QuerySection({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<div>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(currentTab)}
|
{renderQuerySection(currentTab)}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.rule-definition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './RuleOptions.styles.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Collapse,
|
Collapse,
|
||||||
@@ -18,14 +20,17 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
|
defaultAlgorithm,
|
||||||
defaultCompareOp,
|
defaultCompareOp,
|
||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultFrequency,
|
defaultFrequency,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
|
defaultSeasonality,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
import { AlertDetectionTypes } from '.';
|
||||||
import {
|
import {
|
||||||
FormContainer,
|
FormContainer,
|
||||||
InlineSelect,
|
InlineSelect,
|
||||||
@@ -43,6 +48,8 @@ function RuleOptions({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const { ruleType } = alertDef;
|
||||||
|
|
||||||
const handleMatchOptChange = (value: string | unknown): void => {
|
const handleMatchOptChange = (value: string | unknown): void => {
|
||||||
const m = (value as string) || alertDef.condition?.matchType;
|
const m = (value as string) || alertDef.condition?.matchType;
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
@@ -86,8 +93,19 @@ function RuleOptions({
|
|||||||
>
|
>
|
||||||
<Select.Option value="1">{t('option_above')}</Select.Option>
|
<Select.Option value="1">{t('option_above')}</Select.Option>
|
||||||
<Select.Option value="2">{t('option_below')}</Select.Option>
|
<Select.Option value="2">{t('option_below')}</Select.Option>
|
||||||
<Select.Option value="3">{t('option_equal')}</Select.Option>
|
|
||||||
<Select.Option value="4">{t('option_notequal')}</Select.Option>
|
{/* hide equal and not eqaul in case of analmoy based alert */}
|
||||||
|
|
||||||
|
{ruleType !== 'anomaly_rule' && (
|
||||||
|
<>
|
||||||
|
<Select.Option value="3">{t('option_equal')}</Select.Option>
|
||||||
|
<Select.Option value="4">{t('option_notequal')}</Select.Option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ruleType === 'anomaly_rule' && (
|
||||||
|
<Select.Option value="5">{t('option_above_below')}</Select.Option>
|
||||||
|
)}
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,9 +119,14 @@ function RuleOptions({
|
|||||||
>
|
>
|
||||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
||||||
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
|
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
|
||||||
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
|
|
||||||
<Select.Option value="4">{t('option_intotal')}</Select.Option>
|
{ruleType !== 'anomaly_rule' && (
|
||||||
<Select.Option value="5">{t('option_last')}</Select.Option>
|
<>
|
||||||
|
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
|
||||||
|
<Select.Option value="4">{t('option_intotal')}</Select.Option>
|
||||||
|
<Select.Option value="5">{t('option_last')}</Select.Option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,6 +138,37 @@ function RuleOptions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeAlgorithm = (value: string | unknown): void => {
|
||||||
|
const alg = (value as string) || alertDef.condition.algorithm;
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: {
|
||||||
|
...alertDef.condition,
|
||||||
|
algorithm: alg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeSeasonality = (value: string | unknown): void => {
|
||||||
|
const seasonality = (value as string) || alertDef.condition.seasonality;
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: {
|
||||||
|
...alertDef.condition,
|
||||||
|
seasonality,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeDeviation = (value: number): void => {
|
||||||
|
const target = value || alertDef.condition.target || 3;
|
||||||
|
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: { ...alertDef.condition, target: Number(target) },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderEvalWindows = (): JSX.Element => (
|
const renderEvalWindows = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
@@ -127,8 +181,18 @@ function RuleOptions({
|
|||||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||||
<Select.Option value="15m0s">{t('option_15min')}</Select.Option>
|
<Select.Option value="15m0s">{t('option_15min')}</Select.Option>
|
||||||
<Select.Option value="1h0m0s">{t('option_60min')}</Select.Option>
|
<Select.Option value="1h0m0s">{t('option_60min')}</Select.Option>
|
||||||
|
<Select.Option value="2h0m0s">{t('option_2hours')}</Select.Option>
|
||||||
|
<Select.Option value="3h0m0s">{t('option_3hours')}</Select.Option>
|
||||||
<Select.Option value="4h0m0s">{t('option_4hours')}</Select.Option>
|
<Select.Option value="4h0m0s">{t('option_4hours')}</Select.Option>
|
||||||
|
<Select.Option value="5h0m0s">{t('option_5hours')}</Select.Option>
|
||||||
|
<Select.Option value="6h0m0s">{t('option_6hours')}</Select.Option>
|
||||||
|
<Select.Option value="8h0m0s">{t('option_8hours')}</Select.Option>
|
||||||
|
<Select.Option value="10h0m0s">{t('option_10hours')}</Select.Option>
|
||||||
|
<Select.Option value="12h0m0s">{t('option_12hours')}</Select.Option>
|
||||||
<Select.Option value="24h0m0s">{t('option_24hours')}</Select.Option>
|
<Select.Option value="24h0m0s">{t('option_24hours')}</Select.Option>
|
||||||
|
<Select.Option value="48h0m0s">{t('option_48hours')}</Select.Option>
|
||||||
|
<Select.Option value="72h0m0s">{t('option_72hours')}</Select.Option>
|
||||||
|
<Select.Option value="168h0m0s">{t('option_1week')}</Select.Option>
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,6 +210,54 @@ function RuleOptions({
|
|||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderAlgorithms = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={defaultAlgorithm}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.condition.algorithm}
|
||||||
|
onChange={onChangeAlgorithm}
|
||||||
|
>
|
||||||
|
<Select.Option value="standard">Standard</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDeviationOpts = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={3}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.condition.target}
|
||||||
|
onChange={(value: number | unknown): void => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
onChangeDeviation(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value={1}>1</Select.Option>
|
||||||
|
<Select.Option value={2}>2</Select.Option>
|
||||||
|
<Select.Option value={3}>3</Select.Option>
|
||||||
|
<Select.Option value={4}>4</Select.Option>
|
||||||
|
<Select.Option value={5}>5</Select.Option>
|
||||||
|
<Select.Option value={6}>6</Select.Option>
|
||||||
|
<Select.Option value={7}>7</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSeasonality = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={defaultSeasonality}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.condition.seasonality}
|
||||||
|
onChange={onChangeSeasonality}
|
||||||
|
>
|
||||||
|
<Select.Option value="hourly">Hourly</Select.Option>
|
||||||
|
<Select.Option value="daily">Daily</Select.Option>
|
||||||
|
<Select.Option value="weekly">Weekly</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
@@ -216,6 +328,32 @@ function RuleOptions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderAnomalyRuleOpts = (): JSX.Element => (
|
||||||
|
<Form.Item>
|
||||||
|
<Typography.Text className="rule-definition">
|
||||||
|
{t('text_condition1_anomaly')}
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={queryOptions}
|
||||||
|
placeholder={t('selected_query_placeholder')}
|
||||||
|
value={alertDef.condition.selectedQueryName}
|
||||||
|
onChange={onChangeSelectedQueryName}
|
||||||
|
/>
|
||||||
|
{t('text_condition3')} {renderEvalWindows()}
|
||||||
|
<Typography.Text>is</Typography.Text>
|
||||||
|
{renderDeviationOpts()}
|
||||||
|
<Typography.Text>deviations</Typography.Text>
|
||||||
|
{renderCompareOps()}
|
||||||
|
<Typography.Text>the predicted data</Typography.Text>
|
||||||
|
{renderMatchOpts()}
|
||||||
|
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
|
||||||
|
seasonality
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
|
||||||
const renderFrequency = (): JSX.Element => (
|
const renderFrequency = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
@@ -245,36 +383,45 @@ function RuleOptions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading>{t('alert_form_step2')}</StepHeading>
|
<StepHeading>{t('alert_form_step3')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
{queryCategory === EQueryType.PROM
|
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
|
||||||
? renderPromRuleOptions()
|
{queryCategory !== EQueryType.PROM &&
|
||||||
: renderThresholdRuleOpts()}
|
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||||
|
<>{renderAnomalyRuleOpts()}</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{queryCategory !== EQueryType.PROM &&
|
||||||
|
ruleType === AlertDetectionTypes.THRESHOLD_ALERT &&
|
||||||
|
renderThresholdRuleOpts()}
|
||||||
|
|
||||||
<Space direction="vertical" size="large">
|
<Space direction="vertical" size="large">
|
||||||
<Space direction="horizontal" align="center">
|
{ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||||
<Form.Item noStyle name={['condition', 'target']}>
|
<Space direction="horizontal" align="center">
|
||||||
<InputNumber
|
<Form.Item noStyle name={['condition', 'target']}>
|
||||||
addonBefore={t('field_threshold')}
|
<InputNumber
|
||||||
value={alertDef?.condition?.target}
|
addonBefore={t('field_threshold')}
|
||||||
onChange={onChange}
|
value={alertDef?.condition?.target}
|
||||||
type="number"
|
onChange={onChange}
|
||||||
onWheel={(e): void => e.currentTarget.blur()}
|
type="number"
|
||||||
/>
|
onWheel={(e): void => e.currentTarget.blur()}
|
||||||
</Form.Item>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item noStyle>
|
||||||
|
<Select
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={categorySelectOptions}
|
||||||
|
placeholder={t('field_unit')}
|
||||||
|
value={alertDef.condition.targetUnit}
|
||||||
|
onChange={onChangeAlertUnit}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item noStyle>
|
|
||||||
<Select
|
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
options={categorySelectOptions}
|
|
||||||
placeholder={t('field_unit')}
|
|
||||||
value={alertDef.condition.targetUnit}
|
|
||||||
onChange={onChangeAlertUnit}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
<Collapse>
|
<Collapse>
|
||||||
<Collapse.Panel header={t('More options')} key="1">
|
<Collapse.Panel header={t('More options')} key="1">
|
||||||
<Space direction="vertical" size="large">
|
<Space direction="vertical" size="large">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import './FormAlertRules.styles.scss';
|
|||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
|
||||||
FormInstance,
|
FormInstance,
|
||||||
Modal,
|
Modal,
|
||||||
SelectProps,
|
SelectProps,
|
||||||
@@ -13,8 +12,6 @@ import {
|
|||||||
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 logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
|
||||||
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
|
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@@ -26,17 +23,23 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
|||||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
||||||
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 { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
import useFeatureFlag, {
|
||||||
|
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 { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { BellDot, ExternalLink } from 'lucide-react';
|
||||||
|
import Tabs2 from 'periscope/components/Tabs2';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +47,11 @@ import {
|
|||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
Query,
|
||||||
|
QueryFunctionProps,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
@@ -56,13 +63,29 @@ import {
|
|||||||
ActionButton,
|
ActionButton,
|
||||||
ButtonContainer,
|
ButtonContainer,
|
||||||
MainFormContainer,
|
MainFormContainer,
|
||||||
PanelContainer,
|
|
||||||
StepContainer,
|
StepContainer,
|
||||||
StyledLeftContainer,
|
StepHeading,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import UserGuide from './UserGuide';
|
|
||||||
import { getSelectedQueryOptions } from './utils';
|
import { getSelectedQueryOptions } from './utils';
|
||||||
|
|
||||||
|
export enum AlertDetectionTypes {
|
||||||
|
THRESHOLD_ALERT = 'threshold_rule',
|
||||||
|
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||||
|
[AlertTypes.METRICS_BASED_ALERT]:
|
||||||
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||||
|
[AlertTypes.LOGS_BASED_ALERT]:
|
||||||
|
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||||
|
[AlertTypes.TRACES_BASED_ALERT]:
|
||||||
|
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||||
|
[AlertTypes.EXCEPTIONS_BASED_ALERT]:
|
||||||
|
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||||
|
[AlertTypes.ANOMALY_BASED_ALERT]:
|
||||||
|
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function FormAlertRules({
|
function FormAlertRules({
|
||||||
alertType,
|
alertType,
|
||||||
@@ -79,6 +102,8 @@ function FormAlertRules({
|
|||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
const location = useLocation();
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
// In case of alert the panel types should always be "Graph" only
|
// In case of alert the panel types should always be "Graph" only
|
||||||
const panelType = PANEL_TYPES.TIME_SERIES;
|
const panelType = PANEL_TYPES.TIME_SERIES;
|
||||||
@@ -86,6 +111,7 @@ function FormAlertRules({
|
|||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
|
handleSetQueryData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
@@ -108,6 +134,10 @@ function FormAlertRules({
|
|||||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||||
|
|
||||||
|
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||||
|
|
||||||
|
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||||
setYAxisUnit(currentQuery.unit || '');
|
setYAxisUnit(currentQuery.unit || '');
|
||||||
@@ -138,6 +168,89 @@ function FormAlertRules({
|
|||||||
|
|
||||||
useShareBuilderUrl(sq);
|
useShareBuilderUrl(sq);
|
||||||
|
|
||||||
|
const handleDetectionMethodChange = (value: string): void => {
|
||||||
|
setAlertDef((def) => ({
|
||||||
|
...def,
|
||||||
|
ruleType: value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logEvent(`Alert: Detection method changed`, {
|
||||||
|
detectionMethod: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDetectionMethod(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
|
||||||
|
const anomalyFunction = {
|
||||||
|
name: 'anomaly',
|
||||||
|
args: [],
|
||||||
|
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
||||||
|
};
|
||||||
|
const functions = data.functions || [];
|
||||||
|
|
||||||
|
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
// Add anomaly if not already present
|
||||||
|
if (!functions.some((func) => func.name === 'anomaly')) {
|
||||||
|
functions.push(anomalyFunction);
|
||||||
|
} else {
|
||||||
|
const anomalyFuncIndex = functions.findIndex(
|
||||||
|
(func) => func.name === 'anomaly',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (anomalyFuncIndex !== -1) {
|
||||||
|
const anomalyFunc = {
|
||||||
|
...functions[anomalyFuncIndex],
|
||||||
|
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
|
||||||
|
};
|
||||||
|
functions.splice(anomalyFuncIndex, 1);
|
||||||
|
functions.push(anomalyFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove anomaly if present
|
||||||
|
const index = functions.findIndex((func) => func.name === 'anomaly');
|
||||||
|
if (index !== -1) {
|
||||||
|
functions.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return functions;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ruleType =
|
||||||
|
detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
: AlertDetectionTypes.THRESHOLD_ALERT;
|
||||||
|
|
||||||
|
queryParams.set(QueryParams.ruleType, ruleType);
|
||||||
|
|
||||||
|
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||||
|
|
||||||
|
history.replace(generatedUrl);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [detectionMethod]);
|
||||||
|
|
||||||
|
const updateFunctionsBasedOnAlertType = (): void => {
|
||||||
|
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
|
||||||
|
const queryData = currentQuery.builder.queryData[index];
|
||||||
|
|
||||||
|
const updatedFunctions = updateFunctions(queryData);
|
||||||
|
queryData.functions = updatedFunctions;
|
||||||
|
handleSetQueryData(index, queryData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFunctionsBasedOnAlertType();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
detectionMethod,
|
||||||
|
alertDef.condition.target,
|
||||||
|
currentQuery.builder.queryData.length,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const broadcastToSpecificChannels =
|
const broadcastToSpecificChannels =
|
||||||
(initialValue &&
|
(initialValue &&
|
||||||
@@ -145,10 +258,22 @@ function FormAlertRules({
|
|||||||
initialValue.preferredChannels.length > 0) ||
|
initialValue.preferredChannels.length > 0) ||
|
||||||
isNewRule;
|
isNewRule;
|
||||||
|
|
||||||
|
let ruleType = AlertDetectionTypes.THRESHOLD_ALERT;
|
||||||
|
|
||||||
|
if (initialValue.ruleType) {
|
||||||
|
ruleType = initialValue.ruleType as AlertDetectionTypes;
|
||||||
|
} else if (alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
ruleType = AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
|
||||||
|
}
|
||||||
|
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
...initialValue,
|
...initialValue,
|
||||||
broadcastToAll: !broadcastToSpecificChannels,
|
broadcastToAll: !broadcastToSpecificChannels,
|
||||||
|
ruleType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setDetectionMethod(ruleType);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [initialValue, isNewRule]);
|
}, [initialValue, isNewRule]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -269,7 +394,11 @@ function FormAlertRules({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
|
if (
|
||||||
|
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
|
||||||
|
alertDef.condition?.target !== 0 &&
|
||||||
|
!alertDef.condition?.target
|
||||||
|
) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: t('target_missing'),
|
description: t('target_missing'),
|
||||||
@@ -300,12 +429,15 @@ function FormAlertRules({
|
|||||||
const postableAlert: AlertDef = {
|
const postableAlert: AlertDef = {
|
||||||
...alertDef,
|
...alertDef,
|
||||||
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
||||||
alertType,
|
alertType:
|
||||||
|
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
? AlertTypes.METRICS_BASED_ALERT
|
||||||
|
: alertType,
|
||||||
source: window?.location.toString(),
|
source: window?.location.toString(),
|
||||||
ruleType:
|
ruleType:
|
||||||
currentQuery.queryType === EQueryType.PROM
|
currentQuery.queryType === EQueryType.PROM
|
||||||
? 'promql_rule'
|
? 'promql_rule'
|
||||||
: 'threshold_rule',
|
: alertDef.ruleType,
|
||||||
condition: {
|
condition: {
|
||||||
...alertDef.condition,
|
...alertDef.condition,
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
@@ -322,6 +454,12 @@ function FormAlertRules({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
postableAlert.condition.algorithm = alertDef.condition.algorithm;
|
||||||
|
postableAlert.condition.seasonality = alertDef.condition.seasonality;
|
||||||
|
}
|
||||||
|
|
||||||
return postableAlert;
|
return postableAlert;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -418,6 +556,7 @@ function FormAlertRules({
|
|||||||
queryType: currentQuery.queryType,
|
queryType: currentQuery.queryType,
|
||||||
alertId: postableAlert?.id,
|
alertId: postableAlert?.id,
|
||||||
alertName: postableAlert?.alert,
|
alertName: postableAlert?.alert,
|
||||||
|
ruleType: postableAlert?.ruleType,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
@@ -502,6 +641,7 @@ function FormAlertRules({
|
|||||||
queryType: currentQuery.queryType,
|
queryType: currentQuery.queryType,
|
||||||
status: statusResponse.status,
|
status: statusResponse.status,
|
||||||
statusMessage: statusResponse.message,
|
statusMessage: statusResponse.message,
|
||||||
|
ruleType: postableAlert.ruleType,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||||
@@ -575,6 +715,29 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const isRuleCreated = !ruleId || ruleId === 0;
|
const isRuleCreated = !ruleId || ruleId === 0;
|
||||||
|
|
||||||
|
function handleRedirection(option: AlertTypes): void {
|
||||||
|
let url;
|
||||||
|
if (
|
||||||
|
option === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
|
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
) {
|
||||||
|
url = ALERT_SETUP_GUIDE_URLS[AlertTypes.ANOMALY_BASED_ALERT];
|
||||||
|
} else {
|
||||||
|
url = ALERT_SETUP_GUIDE_URLS[option];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
logEvent('Alert: Check example alert clicked', {
|
||||||
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
|
isNewRule: !ruleId || ruleId === 0,
|
||||||
|
ruleId,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
link: url,
|
||||||
|
});
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRuleCreated) {
|
if (!isRuleCreated) {
|
||||||
logEvent('Alert: Edit page visited', {
|
logEvent('Alert: Edit page visited', {
|
||||||
@@ -585,63 +748,97 @@ function FormAlertRules({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleRedirection(option: AlertTypes): void {
|
const tabs = [
|
||||||
let url = '';
|
{
|
||||||
switch (option) {
|
value: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
label: 'Threshold Alert',
|
||||||
url =
|
},
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
{
|
||||||
break;
|
value: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
case AlertTypes.LOGS_BASED_ALERT:
|
label: 'Anomaly Detection Alert',
|
||||||
url =
|
isBeta: true,
|
||||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
},
|
||||||
break;
|
];
|
||||||
case AlertTypes.TRACES_BASED_ALERT:
|
|
||||||
url =
|
const isAnomalyDetectionEnabled =
|
||||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
break;
|
|
||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
|
||||||
url =
|
|
||||||
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
logEvent('Alert: Check example alert clicked', {
|
|
||||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
|
||||||
isNewRule: !ruleId || ruleId === 0,
|
|
||||||
ruleId,
|
|
||||||
queryType: currentQuery.queryType,
|
|
||||||
link: url,
|
|
||||||
});
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
|
|
||||||
<PanelContainer id="top">
|
<div id="top">
|
||||||
<StyledLeftContainer flex="5 1 600px" md={18}>
|
<div className="overview-header">
|
||||||
<MainFormContainer
|
<div className="alert-type-container">
|
||||||
initialValues={initialValue}
|
{isNewRule && (
|
||||||
layout="vertical"
|
<Typography.Title level={5} className="alert-type-title">
|
||||||
form={formInstance}
|
<BellDot size={14} />
|
||||||
className="main-container"
|
|
||||||
|
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||||
|
'Anomaly Detection Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
|
'Metrics Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
|
||||||
|
'Logs Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
|
||||||
|
'Traces Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
|
||||||
|
'Exceptions Based Alert'}
|
||||||
|
</Typography.Title>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={(): void => handleRedirection(alertDef.alertType as AlertTypes)}
|
||||||
|
icon={<ExternalLink size={14} />}
|
||||||
>
|
>
|
||||||
|
Alert Setup Guide
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MainFormContainer
|
||||||
|
initialValues={initialValue}
|
||||||
|
layout="vertical"
|
||||||
|
form={formInstance}
|
||||||
|
className="main-container"
|
||||||
|
>
|
||||||
|
<div className="chart-preview-container">
|
||||||
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||||
renderQBChartPreview()}
|
renderQBChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.PROM &&
|
{currentQuery.queryType === EQueryType.PROM &&
|
||||||
renderPromAndChQueryChartPreview()}
|
renderPromAndChQueryChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
||||||
renderPromAndChQueryChartPreview()}
|
renderPromAndChQueryChartPreview()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<StepContainer>
|
<StepContainer>
|
||||||
<BuilderUnitsFilter
|
<BuilderUnitsFilter
|
||||||
onChange={onUnitChangeHandler}
|
onChange={onUnitChangeHandler}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
</StepContainer>
|
</StepContainer>
|
||||||
|
|
||||||
|
<div className="steps-container">
|
||||||
|
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
|
isAnomalyDetectionEnabled && (
|
||||||
|
<div className="detection-method-container">
|
||||||
|
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||||
|
|
||||||
|
<Tabs2
|
||||||
|
key={detectionMethod}
|
||||||
|
tabs={tabs}
|
||||||
|
initialSelectedTab={detectionMethod || ''}
|
||||||
|
onSelectTab={handleDetectionMethodChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="detection-method-description">
|
||||||
|
{detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
? t('anomaly_detection_alert_desc')
|
||||||
|
: t('threshold_alert_desc')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<QuerySection
|
<QuerySection
|
||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
@@ -662,79 +859,49 @@ function FormAlertRules({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
<ButtonContainer>
|
</div>
|
||||||
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
<ButtonContainer>
|
||||||
<ActionButton
|
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
||||||
loading={loading || false}
|
|
||||||
type="primary"
|
|
||||||
onClick={onSaveHandler}
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
disabled={
|
|
||||||
isAlertNameMissing ||
|
|
||||||
isAlertAvailableToSave ||
|
|
||||||
!isChannelConfigurationValid ||
|
|
||||||
queryStatus === 'error'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
|
type="primary"
|
||||||
|
onClick={onSaveHandler}
|
||||||
|
icon={<SaveOutlined />}
|
||||||
disabled={
|
disabled={
|
||||||
isAlertNameMissing ||
|
isAlertNameMissing ||
|
||||||
|
isAlertAvailableToSave ||
|
||||||
!isChannelConfigurationValid ||
|
!isChannelConfigurationValid ||
|
||||||
queryStatus === 'error'
|
queryStatus === 'error'
|
||||||
}
|
}
|
||||||
type="default"
|
|
||||||
onClick={onTestRuleHandler}
|
|
||||||
>
|
>
|
||||||
{' '}
|
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||||
{t('button_testrule')}
|
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
</Tooltip>
|
||||||
disabled={loading || false}
|
|
||||||
type="default"
|
<ActionButton
|
||||||
onClick={onCancelHandler}
|
loading={loading || false}
|
||||||
>
|
disabled={
|
||||||
{ruleId === 0 && t('button_cancelchanges')}
|
isAlertNameMissing ||
|
||||||
{ruleId > 0 && t('button_discard')}
|
!isChannelConfigurationValid ||
|
||||||
</ActionButton>
|
queryStatus === 'error'
|
||||||
</ButtonContainer>
|
|
||||||
</MainFormContainer>
|
|
||||||
</StyledLeftContainer>
|
|
||||||
<Col flex="1 1 300px">
|
|
||||||
<UserGuide queryType={currentQuery.queryType} />
|
|
||||||
<div className="info-help-btns">
|
|
||||||
<Button
|
|
||||||
style={{ height: 32 }}
|
|
||||||
onClick={(): void =>
|
|
||||||
handleRedirection(alertDef?.alertType as AlertTypes)
|
|
||||||
}
|
}
|
||||||
className="doc-redirection-btn"
|
type="default"
|
||||||
|
onClick={onTestRuleHandler}
|
||||||
>
|
>
|
||||||
Check an example alert
|
{' '}
|
||||||
</Button>
|
{t('button_testrule')}
|
||||||
<LaunchChatSupport
|
</ActionButton>
|
||||||
attributes={{
|
<ActionButton
|
||||||
alert: alertDef?.alert,
|
disabled={loading || false}
|
||||||
alertType: alertDef?.alertType,
|
type="default"
|
||||||
id: ruleId,
|
onClick={onCancelHandler}
|
||||||
ruleType: alertDef?.ruleType,
|
>
|
||||||
state: (alertDef as any)?.state,
|
{ruleId === 0 && t('button_cancelchanges')}
|
||||||
panelType,
|
{ruleId > 0 && t('button_discard')}
|
||||||
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
|
</ActionButton>
|
||||||
}}
|
</ButtonContainer>
|
||||||
className="facing-issue-btn"
|
</MainFormContainer>
|
||||||
eventName="Alert: Facing Issues in alert"
|
</div>
|
||||||
buttonText="Need help with this alert?"
|
|
||||||
message={alertHelpMessage(alertDef, ruleId)}
|
|
||||||
onHoverText="Click here to get help with this alert"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</PanelContainer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ function LabelSelect({
|
|||||||
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
|
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
|
||||||
send('NEXT');
|
send('NEXT');
|
||||||
}
|
}
|
||||||
|
if (state.value === 'Idle') {
|
||||||
|
send('NEXT');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
value={currentVal as never}
|
value={currentVal as never}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Button, Card, Col, Form, Input, Row, Select, Typography } from 'antd';
|
import { Button, Card, Col, Form, Input, Select, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Item } = Form;
|
const { Item } = Form;
|
||||||
|
|
||||||
export const PanelContainer = styled(Row)`
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledLeftContainer = styled(Col)`
|
export const StyledLeftContainer = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { QueryParams } from 'constants/query';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
@@ -72,16 +73,18 @@ function WidgetHeader({
|
|||||||
tableProcessedDataRef,
|
tableProcessedDataRef,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
}: IWidgetHeaderProps): JSX.Element | null {
|
}: IWidgetHeaderProps): JSX.Element | null {
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
const onEditHandler = useCallback((): void => {
|
const onEditHandler = useCallback((): void => {
|
||||||
const widgetId = widget.id;
|
const widgetId = widget.id;
|
||||||
history.push(
|
urlQuery.set(QueryParams.widgetId, widgetId);
|
||||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
|
urlQuery.set(QueryParams.graphType, widget.panelTypes);
|
||||||
widget.panelTypes
|
urlQuery.set(
|
||||||
}&${QueryParams.compositeQuery}=${encodeURIComponent(
|
QueryParams.compositeQuery,
|
||||||
JSON.stringify(widget.query),
|
encodeURIComponent(JSON.stringify(widget.query)),
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
}, [widget.id, widget.panelTypes, widget.query]);
|
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
|
||||||
|
history.push(generatedUrl);
|
||||||
|
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
|
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
|
||||||
|
|
||||||
|
|||||||
@@ -97,13 +97,19 @@ function GridTableComponent({
|
|||||||
|
|
||||||
const newColumnData = columns.map((e) => ({
|
const newColumnData = columns.map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
render: (text: string): ReactNode => {
|
render: (text: string, ...rest: any): ReactNode => {
|
||||||
const isNumber = !Number.isNaN(Number(text));
|
let textForThreshold = text;
|
||||||
|
if (columnUnits && columnUnits?.[e.title as string]) {
|
||||||
|
textForThreshold = rest[0][`${e.title}_without_unit`];
|
||||||
|
}
|
||||||
|
const isNumber = !Number.isNaN(Number(textForThreshold));
|
||||||
|
|
||||||
if (thresholds && isNumber) {
|
if (thresholds && isNumber) {
|
||||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||||
thresholds,
|
thresholds,
|
||||||
e.title as string,
|
e.title as string,
|
||||||
Number(text),
|
Number(textForThreshold),
|
||||||
|
columnUnits?.[e.title as string],
|
||||||
);
|
);
|
||||||
|
|
||||||
const idx = thresholds.findIndex(
|
const idx = thresholds.findIndex(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
|
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
|
||||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||||
@@ -30,10 +31,39 @@ function evaluateCondition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether a given value meets a specified threshold condition.
|
||||||
|
* It first converts the value to the appropriate unit if a threshold unit is provided,
|
||||||
|
* and then checks the condition using the specified operator.
|
||||||
|
*
|
||||||
|
* @param value - The value to be evaluated.
|
||||||
|
* @param thresholdValue - The threshold value to compare against.
|
||||||
|
* @param thresholdOperator - The operator used for comparison (e.g., '>', '<', '==').
|
||||||
|
* @param thresholdUnit - The unit to which the value should be converted.
|
||||||
|
* @param columnUnit - The current unit of the value.
|
||||||
|
* @returns A boolean indicating whether the value meets the threshold condition.
|
||||||
|
*/
|
||||||
|
function evaluateThresholdWithConvertedValue(
|
||||||
|
value: number,
|
||||||
|
thresholdValue: number,
|
||||||
|
thresholdOperator?: string,
|
||||||
|
thresholdUnit?: string,
|
||||||
|
columnUnit?: string,
|
||||||
|
): boolean {
|
||||||
|
const convertedValue = convertUnit(value, columnUnit, thresholdUnit);
|
||||||
|
|
||||||
|
if (convertedValue) {
|
||||||
|
return evaluateCondition(thresholdOperator, convertedValue, thresholdValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluateCondition(thresholdOperator, value, thresholdValue);
|
||||||
|
}
|
||||||
|
|
||||||
export function findMatchingThreshold(
|
export function findMatchingThreshold(
|
||||||
thresholds: ThresholdProps[],
|
thresholds: ThresholdProps[],
|
||||||
label: string,
|
label: string,
|
||||||
value: number,
|
value: number,
|
||||||
|
columnUnit?: string,
|
||||||
): {
|
): {
|
||||||
threshold: ThresholdProps;
|
threshold: ThresholdProps;
|
||||||
hasMultipleMatches: boolean;
|
hasMultipleMatches: boolean;
|
||||||
@@ -45,10 +75,12 @@ export function findMatchingThreshold(
|
|||||||
if (
|
if (
|
||||||
threshold.thresholdValue !== undefined &&
|
threshold.thresholdValue !== undefined &&
|
||||||
threshold.thresholdTableOptions === label &&
|
threshold.thresholdTableOptions === label &&
|
||||||
evaluateCondition(
|
evaluateThresholdWithConvertedValue(
|
||||||
threshold.thresholdOperator,
|
|
||||||
value,
|
value,
|
||||||
threshold.thresholdValue,
|
threshold?.thresholdValue,
|
||||||
|
threshold.thresholdOperator,
|
||||||
|
threshold.thresholdUnit,
|
||||||
|
columnUnit,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
matchingThresholds.push(threshold);
|
matchingThresholds.push(threshold);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { ColumnsType } from 'antd/es/table/interface';
|
|||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import DropDown from 'components/DropDown/DropDown';
|
import DropDown from 'components/DropDown/DropDown';
|
||||||
import { listAlertMessage } from 'components/LaunchChatSupport/util';
|
|
||||||
import {
|
import {
|
||||||
DynamicColumnsKey,
|
DynamicColumnsKey,
|
||||||
TableDataSource,
|
TableDataSource,
|
||||||
@@ -397,15 +396,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
dynamicColumns={dynamicColumns}
|
dynamicColumns={dynamicColumns}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
pagination={paginationConfig}
|
pagination={paginationConfig}
|
||||||
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',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,17 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
// overridding the request integration style to fix the spacing for dashboard list
|
||||||
|
.request-entity-container {
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integrations-content {
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboards-list-view-content {
|
.dashboards-list-view-content {
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
max-width: 836px;
|
max-width: 836px;
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
|
||||||
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
|
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
|
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
|
||||||
@@ -79,6 +77,7 @@ import { isCloudUser } from 'utils/app';
|
|||||||
|
|
||||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||||
import ImportJSON from './ImportJSON';
|
import ImportJSON from './ImportJSON';
|
||||||
|
import { RequestDashboardBtn } from './RequestDashboardBtn';
|
||||||
import { DeleteButton } from './TableComponents/DeleteButton';
|
import { DeleteButton } from './TableComponents/DeleteButton';
|
||||||
import {
|
import {
|
||||||
DashboardDynamicColumns,
|
DashboardDynamicColumns,
|
||||||
@@ -693,17 +692,14 @@ function DashboardsList(): JSX.Element {
|
|||||||
<Typography.Text className="subtitle">
|
<Typography.Text className="subtitle">
|
||||||
Create and manage dashboards for your workspace.
|
Create and manage dashboards for your workspace.
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<LaunchChatSupport
|
|
||||||
attributes={{
|
|
||||||
screen: 'Dashboard list page',
|
|
||||||
}}
|
|
||||||
eventName="Dashboard: Facing Issues in dashboard"
|
|
||||||
message={dashboardListMessage}
|
|
||||||
buttonText="Need help with dashboards?"
|
|
||||||
onHoverText="Click here to get help with dashboards"
|
|
||||||
intercomMessageDisabled
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{isCloudUser() && (
|
||||||
|
<div className="integrations-container">
|
||||||
|
<div className="integrations-content">
|
||||||
|
<RequestDashboardBtn />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDashboardListLoading ||
|
{isDashboardListLoading ||
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ function ImportJSON({
|
|||||||
|
|
||||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
||||||
|
|
||||||
|
// Add validation for uuid
|
||||||
|
if (dashboardData.uuid !== undefined && dashboardData.uuid.trim() === '') {
|
||||||
|
// silently remove uuid if it is empty
|
||||||
|
delete dashboardData.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
if (dashboardData?.layout) {
|
if (dashboardData?.layout) {
|
||||||
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
||||||
} else {
|
} else {
|
||||||
@@ -123,11 +129,14 @@ function ImportJSON({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
} catch {
|
} catch (error) {
|
||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
setIsFeatureAlert(false);
|
setIsFeatureAlert(false);
|
||||||
|
|
||||||
setIsCreateDashboardError(true);
|
setIsCreateDashboardError(true);
|
||||||
|
notifications.error({
|
||||||
|
message: error instanceof Error ? error.message : t('error_loading_json'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import '../../pages/Integrations/Integrations.styles.scss';
|
||||||
|
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Input, Space, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function RequestDashboardBtn(): JSX.Element {
|
||||||
|
const [
|
||||||
|
isSubmittingRequestForDashboard,
|
||||||
|
setIsSubmittingRequestForDashboard,
|
||||||
|
] = useState(false);
|
||||||
|
|
||||||
|
const [requestedDashboardName, setRequestedDashboardName] = useState('');
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
|
const handleRequestDashboardSubmit = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsSubmittingRequestForDashboard(true);
|
||||||
|
const response = await logEvent('Dashboard Requested', {
|
||||||
|
screen: 'Dashboard list page',
|
||||||
|
dashboard: requestedDashboardName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
notifications.success({
|
||||||
|
message: 'Dashboard Request Submitted',
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDashboard(false);
|
||||||
|
} else {
|
||||||
|
notifications.error({
|
||||||
|
message:
|
||||||
|
response.error ||
|
||||||
|
t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDashboard(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error({
|
||||||
|
message: t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDashboard(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="request-entity-container">
|
||||||
|
<Typography.Text>
|
||||||
|
Can't find the dashboard you need? Request a new Dashboard.
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter dashboard name..."
|
||||||
|
style={{ width: 300, marginBottom: 0 }}
|
||||||
|
value={requestedDashboardName}
|
||||||
|
onChange={(e): void => setRequestedDashboardName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn primary"
|
||||||
|
icon={
|
||||||
|
isSubmittingRequestForDashboard ? (
|
||||||
|
<LoadingOutlined />
|
||||||
|
) : (
|
||||||
|
<Check size={12} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="primary"
|
||||||
|
onClick={handleRequestDashboardSubmit}
|
||||||
|
disabled={
|
||||||
|
isSubmittingRequestForDashboard ||
|
||||||
|
!requestedDashboardName ||
|
||||||
|
requestedDashboardName?.trim().length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -157,6 +157,11 @@ export const getFieldAttributes = (field: string): IFieldAttributes => {
|
|||||||
const stringWithoutPrefix = field.slice('resources_'.length);
|
const stringWithoutPrefix = field.slice('resources_'.length);
|
||||||
const parts = splitOnce(stringWithoutPrefix, '.');
|
const parts = splitOnce(stringWithoutPrefix, '.');
|
||||||
[dataType, newField] = parts;
|
[dataType, newField] = parts;
|
||||||
|
} else if (field.startsWith('scope_string')) {
|
||||||
|
logType = MetricsType.Scope;
|
||||||
|
const stringWithoutPrefix = field.slice('scope_'.length);
|
||||||
|
const parts = splitOnce(stringWithoutPrefix, '.');
|
||||||
|
[dataType, newField] = parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { dataType, newField, logType };
|
return { dataType, newField, logType };
|
||||||
@@ -187,6 +192,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
|||||||
traceId: logData.traceId,
|
traceId: logData.traceId,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
resources: {},
|
resources: {},
|
||||||
|
scope: {},
|
||||||
severity_text: logData.severity_text,
|
severity_text: logData.severity_text,
|
||||||
severity_number: logData.severity_number,
|
severity_number: logData.severity_number,
|
||||||
};
|
};
|
||||||
@@ -198,6 +204,9 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
|||||||
} else if (key.startsWith('resources_')) {
|
} else if (key.startsWith('resources_')) {
|
||||||
outputJson.resources = outputJson.resources || {};
|
outputJson.resources = outputJson.resources || {};
|
||||||
Object.assign(outputJson.resources, logData[key as keyof ILog]);
|
Object.assign(outputJson.resources, logData[key as keyof ILog]);
|
||||||
|
} else if (key.startsWith('scope_string')) {
|
||||||
|
outputJson.scope = outputJson.scope || {};
|
||||||
|
Object.assign(outputJson.scope, logData[key as keyof ILog]);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -2,3 +2,25 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-row-backdrop {
|
||||||
|
&.INFO {
|
||||||
|
background-color: var(--bg-robin-500) 10;
|
||||||
|
}
|
||||||
|
&.WARNING,
|
||||||
|
&.WARN {
|
||||||
|
background-color: var(--bg-amber-500) 10;
|
||||||
|
}
|
||||||
|
&.ERROR {
|
||||||
|
background-color: var(--bg-cherry-500) 10;
|
||||||
|
}
|
||||||
|
&.TRACE {
|
||||||
|
background-color: var(--bg-forest-400) 10;
|
||||||
|
}
|
||||||
|
&.DEBUG {
|
||||||
|
background-color: var(--bg-aqua-500) 10;
|
||||||
|
}
|
||||||
|
&.FATAL {
|
||||||
|
background-color: var(--bg-sakura-500) 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
|
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
@@ -21,6 +22,11 @@ import { TableHeaderCellStyled, TableRowStyled } from './styles';
|
|||||||
import TableRow from './TableRow';
|
import TableRow from './TableRow';
|
||||||
import { InfinityTableProps } from './types';
|
import { InfinityTableProps } from './types';
|
||||||
|
|
||||||
|
interface CustomTableRowProps {
|
||||||
|
activeContextLogId: string;
|
||||||
|
activeLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/function-component-definition
|
// eslint-disable-next-line react/function-component-definition
|
||||||
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
||||||
children,
|
children,
|
||||||
@@ -31,10 +37,17 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
|||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const logType = getLogIndicatorType(props.item);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowStyled
|
<TableRowStyled
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isActiveLog={isHighlighted}
|
$isActiveLog={
|
||||||
|
isHighlighted ||
|
||||||
|
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
|
||||||
|
(context as CustomTableRowProps).activeLogId === props.item.id
|
||||||
|
}
|
||||||
|
$logType={logType}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -66,8 +79,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
...tableViewProps,
|
...tableViewProps,
|
||||||
onClickExpand: onSetActiveLog,
|
onClickExpand: onSetActiveLog,
|
||||||
onOpenLogsContext: handleSetActiveContextLog,
|
onOpenLogsContext: handleSetActiveContextLog,
|
||||||
activeLog,
|
|
||||||
activeContextLog,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||||
@@ -153,7 +164,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
// TODO: fix it in the future
|
// TODO: fix it in the future
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
TableRow: CustomTableRow,
|
TableRow: (props): any =>
|
||||||
|
CustomTableRow({
|
||||||
|
...props,
|
||||||
|
context: {
|
||||||
|
activeContextLogId: activeContextLog?.id,
|
||||||
|
activeLogId: activeLog?.id,
|
||||||
|
},
|
||||||
|
} as any),
|
||||||
}}
|
}}
|
||||||
itemContent={itemContent}
|
itemContent={itemContent}
|
||||||
fixedHeaderContent={tableHeader}
|
fixedHeaderContent={tableHeader}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Color } from '@signozhq/design-tokens';
|
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -37,13 +36,12 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
|||||||
export const TableRowStyled = styled.tr<{
|
export const TableRowStyled = styled.tr<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
|
$logType: string;
|
||||||
}>`
|
}>`
|
||||||
td {
|
td {
|
||||||
${({ $isActiveLog, $isDarkMode }): string =>
|
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? `background-color: ${
|
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
|
||||||
} !important`
|
|
||||||
: ''};
|
: ''};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export enum KeyOperationTableHeader {
|
|||||||
export enum MetricsType {
|
export enum MetricsType {
|
||||||
Tag = 'tag',
|
Tag = 'tag',
|
||||||
Resource = 'resource',
|
Resource = 'resource',
|
||||||
|
Scope = 'scope',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WidgetKeys {
|
export enum WidgetKeys {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
|
||||||
import { dashboardHelpMessage } from 'components/LaunchChatSupport/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_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -47,7 +45,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
|
import {
|
||||||
|
Dashboard,
|
||||||
|
DashboardData,
|
||||||
|
IDashboardVariable,
|
||||||
|
} 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';
|
||||||
@@ -63,6 +65,30 @@ interface DashboardDescriptionProps {
|
|||||||
handle: FullScreenHandle;
|
handle: FullScreenHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeDashboardData(
|
||||||
|
selectedData: DashboardData,
|
||||||
|
): Omit<DashboardData, 'uuid'> {
|
||||||
|
if (!selectedData?.variables) {
|
||||||
|
const { uuid, ...rest } = selectedData;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
const { selectedValue, ...rest } = value;
|
||||||
|
acc[key] = rest;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, IDashboardVariable>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { uuid, ...restData } = selectedData;
|
||||||
|
return {
|
||||||
|
...restData,
|
||||||
|
variables: updatedVariables,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||||
const { handle } = props;
|
const { handle } = props;
|
||||||
@@ -328,18 +354,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
{isDashboardLocked && <LockKeyhole size={14} />}
|
{isDashboardLocked && <LockKeyhole size={14} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="right-section">
|
<div className="right-section">
|
||||||
<LaunchChatSupport
|
|
||||||
attributes={{
|
|
||||||
uuid: selectedDashboard?.uuid,
|
|
||||||
title: updatedTitle,
|
|
||||||
screen: 'Dashboard Details',
|
|
||||||
}}
|
|
||||||
eventName="Dashboard: Facing Issues in dashboard"
|
|
||||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
|
||||||
buttonText="Need help with this dashboard?"
|
|
||||||
onHoverText="Click here to get help with dashboard"
|
|
||||||
intercomMessageDisabled
|
|
||||||
/>
|
|
||||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||||
<Popover
|
<Popover
|
||||||
open={isDashboardSettingsOpen}
|
open={isDashboardSettingsOpen}
|
||||||
@@ -407,7 +421,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
icon={<FileJson size={14} />}
|
icon={<FileJson size={14} />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
downloadObjectAsJson(selectedData, selectedData.title);
|
downloadObjectAsJson(
|
||||||
|
sanitizeDashboardData(selectedData),
|
||||||
|
selectedData.title,
|
||||||
|
);
|
||||||
setIsDashbordSettingsOpen(false);
|
setIsDashbordSettingsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -417,7 +434,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
icon={<ClipboardCopy size={14} />}
|
icon={<ClipboardCopy size={14} />}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setCopy(JSON.stringify(selectedData, null, 2));
|
setCopy(
|
||||||
|
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
|
||||||
|
);
|
||||||
setIsDashbordSettingsOpen(false);
|
setIsDashbordSettingsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParse
|
|||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import { debounce, isArray, isString } from 'lodash-es';
|
import { debounce, isArray, isString } from 'lodash-es';
|
||||||
import map from 'lodash-es/map';
|
import map from 'lodash-es/map';
|
||||||
import { ChangeEvent, memo, useEffect, useMemo, useRef, useState } from 'react';
|
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -257,8 +257,7 @@ function VariableItem({
|
|||||||
if (variableData.name) {
|
if (variableData.name) {
|
||||||
if (
|
if (
|
||||||
value === ALL_SELECT_VALUE ||
|
value === ALL_SELECT_VALUE ||
|
||||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
|
||||||
(Array.isArray(value) && value.length === 0)
|
|
||||||
) {
|
) {
|
||||||
onValueUpdate(variableData.name, variableData.id, optionsData, true);
|
onValueUpdate(variableData.name, variableData.id, optionsData, true);
|
||||||
} else {
|
} else {
|
||||||
@@ -278,25 +277,6 @@ function VariableItem({
|
|||||||
|
|
||||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||||
|
|
||||||
const [variableDropdownOpen, setVariableDropdownOpen] = useState(false);
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent): void => {
|
|
||||||
if (
|
|
||||||
wrapperRef.current &&
|
|
||||||
!wrapperRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setVariableDropdownOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('click', handleClickOutside);
|
|
||||||
return (): void => {
|
|
||||||
window.removeEventListener('click', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectValue =
|
const selectValue =
|
||||||
variableData.allSelected && enableSelectAll
|
variableData.allSelected && enableSelectAll
|
||||||
? 'ALL'
|
? 'ALL'
|
||||||
@@ -343,10 +323,6 @@ function VariableItem({
|
|||||||
Array.isArray(selectedValueStringified) &&
|
Array.isArray(selectedValueStringified) &&
|
||||||
selectedValueStringified.includes(option.toString())
|
selectedValueStringified.includes(option.toString())
|
||||||
) {
|
) {
|
||||||
if (newSelectedValue.length === 0) {
|
|
||||||
handleChange(ALL_SELECT_VALUE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newSelectedValue.length === 1) {
|
if (newSelectedValue.length === 1) {
|
||||||
handleChange(newSelectedValue[0].toString());
|
handleChange(newSelectedValue[0].toString());
|
||||||
return;
|
return;
|
||||||
@@ -431,7 +407,7 @@ function VariableItem({
|
|||||||
<Typography.Text className="variable-name" ellipsis>
|
<Typography.Text className="variable-name" ellipsis>
|
||||||
${variableData.name}
|
${variableData.name}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="variable-value" ref={wrapperRef}>
|
<div className="variable-value">
|
||||||
{variableData.type === 'TEXTBOX' ? (
|
{variableData.type === 'TEXTBOX' ? (
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
@@ -464,10 +440,6 @@ function VariableItem({
|
|||||||
style={SelectItemStyle}
|
style={SelectItemStyle}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
showSearch
|
showSearch
|
||||||
open={variableDropdownOpen}
|
|
||||||
onDropdownVisibleChange={(visible): void =>
|
|
||||||
setVariableDropdownOpen(visible)
|
|
||||||
}
|
|
||||||
data-testid="variable-select"
|
data-testid="variable-select"
|
||||||
className="variable-select"
|
className="variable-select"
|
||||||
popupClassName="dropdown-styles"
|
popupClassName="dropdown-styles"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user