mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 04:39:01 +08:00
Merge branch 'SigNoz:develop' into develop
This commit is contained in:
commit
65804f245c
22
.github/workflows/dependency-review.yml
vendored
Normal file
22
.github/workflows/dependency-review.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
with:
|
||||
fail-on-severity: high
|
||||
uses: actions/dependency-review-action@v2
|
6
Makefile
6
Makefile
@ -82,15 +82,9 @@ dev-setup:
|
||||
run-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
|
||||
|
||||
run-arm:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml up -d
|
||||
|
||||
down-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
|
||||
|
||||
down-arm:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml down -v
|
||||
|
||||
clear-standalone-data:
|
||||
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
||||
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"
|
||||
|
@ -86,15 +86,19 @@ services:
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
# - "8889:8889" # Prometheus metrics exposed by the agent
|
||||
# - "13133:13133" # health_check
|
||||
# - "14268:14268" # Jaeger receiver
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "8889:8889" # signoz spanmetrics exposed by the agent
|
||||
# - "9411:9411" # Zipkin port
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "14250:14250" # Jaeger gRPC
|
||||
# - "14268:14268" # Jaeger thrift HTTP
|
||||
# - "55678:55678" # OpenCensus receiver
|
||||
# - "55679:55679" # zpages extension
|
||||
# - "55680:55680" # OTLP gRPC legacy receiver
|
||||
# - "55681:55681" # OTLP HTTP legacy receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
@ -111,6 +115,11 @@ services:
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -1,30 +1,46 @@
|
||||
receivers:
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "localhost:12345"
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
jaeger:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:14250
|
||||
thrift_http:
|
||||
endpoint: 0.0.0.0:14268
|
||||
# thrift_compact:
|
||||
# endpoint: 0.0.0.0:6831
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
scrapers:
|
||||
cpu:
|
||||
load:
|
||||
memory:
|
||||
disk:
|
||||
filesystem:
|
||||
network:
|
||||
cpu: {}
|
||||
load: {}
|
||||
memory: {}
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
timeout: 10s
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
@ -49,9 +65,7 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
@ -60,18 +74,35 @@ exporters:
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages, pprof]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp, hostmetrics]
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
exporters: [prometheus]
|
||||
|
@ -1,17 +1,26 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
http:
|
||||
|
||||
# Data sources: metrics
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"]
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -32,17 +41,26 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages, pprof]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp, prometheus]
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
exporters: [clickhousemetricswrite]
|
||||
|
@ -82,16 +82,20 @@ services:
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
# - "8889:8889" # Prometheus metrics exposed by the agent
|
||||
# - "13133:13133" # health_check
|
||||
# - "14268:14268" # Jaeger receiver
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "8889:8889" # signoz spanmetrics exposed by the agent
|
||||
# - "9411:9411" # Zipkin port
|
||||
# - "13133:13133" # health check extension
|
||||
# - "14250:14250" # Jaeger gRPC
|
||||
# - "14268:14268" # Jaeger thrift HTTP
|
||||
# - "55678:55678" # OpenCensus receiver
|
||||
# - "55679:55679" # zpages extension
|
||||
# - "55680:55680" # OTLP gRPC legacy receiver
|
||||
# - "55681:55681" # OTLP HTTP legacy receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
mem_limit: 2000m
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -103,6 +107,11 @@ services:
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
|
@ -1,25 +1,36 @@
|
||||
receivers:
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: "localhost:12345"
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:4317
|
||||
http:
|
||||
endpoint: 0.0.0.0:4318
|
||||
jaeger:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: 0.0.0.0:14250
|
||||
thrift_http:
|
||||
endpoint: 0.0.0.0:14268
|
||||
# thrift_compact:
|
||||
# endpoint: 0.0.0.0:6831
|
||||
# thrift_binary:
|
||||
# endpoint: 0.0.0.0:6832
|
||||
hostmetrics:
|
||||
collection_interval: 60s
|
||||
scrapers:
|
||||
cpu:
|
||||
load:
|
||||
memory:
|
||||
disk:
|
||||
filesystem:
|
||||
network:
|
||||
cpu: {}
|
||||
load: {}
|
||||
memory: {}
|
||||
disk: {}
|
||||
filesystem: {}
|
||||
network: {}
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -49,9 +60,20 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
override: false
|
||||
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
@ -60,18 +82,30 @@ exporters:
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
prometheus:
|
||||
endpoint: "0.0.0.0:8889"
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- zpages
|
||||
- pprof
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp, hostmetrics]
|
||||
receivers: [otlp]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/hostmetrics:
|
||||
receivers: [hostmetrics]
|
||||
processors: [resourcedetection, batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
|
@ -3,15 +3,28 @@ receivers:
|
||||
protocols:
|
||||
grpc:
|
||||
http:
|
||||
|
||||
# Data sources: metrics
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector internal metrics
|
||||
- job_name: "otel-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets: ["otel-collector:8889"]
|
||||
- targets:
|
||||
- otel-collector:8888
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: "otel-collector-metrics"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
# SigNoz span metrics
|
||||
- job_name: "signozspanmetrics-collector"
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
@ -32,17 +45,29 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
|
||||
extensions:
|
||||
health_check: {}
|
||||
zpages: {}
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
service:
|
||||
extensions: [health_check, zpages]
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- zpages
|
||||
- pprof
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [otlp, prometheus]
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
|
@ -1,123 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<clickhouse>
|
||||
<!-- See also the files in users.d directory where the settings can be overridden. -->
|
||||
|
||||
<!-- Profiles of settings. -->
|
||||
<profiles>
|
||||
<!-- Default settings. -->
|
||||
<default>
|
||||
<!-- Maximum memory usage for processing single query, in bytes. -->
|
||||
<max_memory_usage>10000000000</max_memory_usage>
|
||||
|
||||
<!-- How to choose between replicas during distributed query processing.
|
||||
random - choose random replica from set of replicas with minimum number of errors
|
||||
nearest_hostname - from set of replicas with minimum number of errors, choose replica
|
||||
with minimum number of different symbols between replica's hostname and local hostname
|
||||
(Hamming distance).
|
||||
in_order - first live replica is chosen in specified order.
|
||||
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
|
||||
-->
|
||||
<load_balancing>random</load_balancing>
|
||||
</default>
|
||||
|
||||
<!-- Profile that allows only read queries. -->
|
||||
<readonly>
|
||||
<readonly>1</readonly>
|
||||
</readonly>
|
||||
</profiles>
|
||||
|
||||
<!-- Users and ACL. -->
|
||||
<users>
|
||||
<!-- If user name was not specified, 'default' user is used. -->
|
||||
<default>
|
||||
<!-- See also the files in users.d directory where the password can be overridden.
|
||||
|
||||
Password could be specified in plaintext or in SHA256 (in hex format).
|
||||
|
||||
If you want to specify password in plaintext (not recommended), place it in 'password' element.
|
||||
Example: <password>qwerty</password>.
|
||||
Password could be empty.
|
||||
|
||||
If you want to specify SHA256, place it in 'password_sha256_hex' element.
|
||||
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
|
||||
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
|
||||
|
||||
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
|
||||
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
|
||||
|
||||
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
|
||||
place its name in 'server' element inside 'ldap' element.
|
||||
Example: <ldap><server>my_ldap_server</server></ldap>
|
||||
|
||||
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
|
||||
place 'kerberos' element instead of 'password' (and similar) elements.
|
||||
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
|
||||
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
|
||||
whose initiator's realm matches it.
|
||||
Example: <kerberos />
|
||||
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
|
||||
|
||||
How to generate decent password:
|
||||
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
|
||||
In first line will be password and in second - corresponding SHA256.
|
||||
|
||||
How to generate double SHA1:
|
||||
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
|
||||
In first line will be password and in second - corresponding double SHA1.
|
||||
-->
|
||||
<password></password>
|
||||
|
||||
<!-- List of networks with open access.
|
||||
|
||||
To open access from everywhere, specify:
|
||||
<ip>::/0</ip>
|
||||
|
||||
To open access only from localhost, specify:
|
||||
<ip>::1</ip>
|
||||
<ip>127.0.0.1</ip>
|
||||
|
||||
Each element of list has one of the following forms:
|
||||
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
|
||||
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
|
||||
<host> Hostname. Example: server01.clickhouse.com.
|
||||
To check access, DNS query is performed, and all received addresses compared to peer address.
|
||||
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
|
||||
To check access, DNS PTR query is performed for peer address and then regexp is applied.
|
||||
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
|
||||
Strongly recommended that regexp is ends with $
|
||||
All results of DNS requests are cached till server restart.
|
||||
-->
|
||||
<networks>
|
||||
<ip>::/0</ip>
|
||||
</networks>
|
||||
|
||||
<!-- Settings profile for user. -->
|
||||
<profile>default</profile>
|
||||
|
||||
<!-- Quota for user. -->
|
||||
<quota>default</quota>
|
||||
|
||||
<!-- User can create other users and grant rights to them. -->
|
||||
<!-- <access_management>1</access_management> -->
|
||||
</default>
|
||||
</users>
|
||||
|
||||
<!-- Quotas. -->
|
||||
<quotas>
|
||||
<!-- Name of quota. -->
|
||||
<default>
|
||||
<!-- Limits for time interval. You could specify many intervals with different limits. -->
|
||||
<interval>
|
||||
<!-- Length of interval. -->
|
||||
<duration>3600</duration>
|
||||
|
||||
<!-- No limits. Just calculate resource usage for time interval. -->
|
||||
<queries>0</queries>
|
||||
<errors>0</errors>
|
||||
<result_rows>0</result_rows>
|
||||
<read_rows>0</read_rows>
|
||||
<execution_time>0</execution_time>
|
||||
</interval>
|
||||
</default>
|
||||
</quotas>
|
||||
</clickhouse>
|
@ -13,8 +13,9 @@
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"postinstall": "is-ci || yarn husky:configure",
|
||||
"playwright": "playwright test --config=./playwright.config.ts",
|
||||
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||
"playwright:codegen:local":"playwright codegen http://localhost:3301",
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1"
|
||||
},
|
||||
@ -43,6 +44,7 @@
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"chart.js": "^3.4.0",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
@ -81,6 +83,7 @@
|
||||
"style-loader": "1.3.0",
|
||||
"styled-components": "^5.2.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"timestamp-nano": "^1.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
|
@ -16,6 +16,8 @@ const config: PlaywrightTestConfig = {
|
||||
updateSnapshots: 'all',
|
||||
fullyParallel: false,
|
||||
quiet: true,
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
63
frontend/public/locales/en-GB/rules.json
Normal file
63
frontend/public/locales/en-GB/rules.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info"
|
||||
}
|
63
frontend/public/locales/en/rules.json
Normal file
63
frontend/public/locales/en/rules.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info"
|
||||
}
|
@ -9,7 +9,7 @@ const create = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/rules', {
|
||||
data: props.query,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -14,7 +14,7 @@ const get = async (
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
|
@ -2,14 +2,14 @@ import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/put';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
const put = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/rules/${props.id}`, {
|
||||
data: props.data,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
17
frontend/src/api/alerts/save.ts
Normal file
17
frontend/src/api/alerts/save.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
import create from './create';
|
||||
import put from './put';
|
||||
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && props.id > 0) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
return create({ ...props });
|
||||
};
|
||||
|
||||
export default save;
|
@ -10,9 +10,8 @@ const getAll = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errors?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
`/listErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
@ -10,11 +10,8 @@ const getByErrorType = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errorWithType?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
serviceName: props.serviceName,
|
||||
errorType: props.errorType,
|
||||
`/errorFromGroupID?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
@ -3,17 +3,15 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getById';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getByErrorId';
|
||||
|
||||
const getById = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/errorWithId?${createQueryParams({
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
errorId: props.errorId,
|
||||
`/errorFromErrorID?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
|
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
||||
|
||||
const getErrorCounts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/countErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.message,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getErrorCounts;
|
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getNextPrevId';
|
||||
|
||||
const getErrorCounts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/nextPrevErrorIDs?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.message,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getErrorCounts;
|
@ -1,14 +1,15 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { getVersion } from 'constants/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/user/getVersion';
|
||||
|
||||
const getVersion = async (): Promise<
|
||||
const getVersionApi = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/version`);
|
||||
const response = await axios.get(`/${getVersion}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
@ -21,4 +22,4 @@ const getVersion = async (): Promise<
|
||||
}
|
||||
};
|
||||
|
||||
export default getVersion;
|
||||
export default getVersionApi;
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -50,6 +51,7 @@ Chart.register(
|
||||
SubTitle,
|
||||
BarController,
|
||||
BarElement,
|
||||
annotationPlugin,
|
||||
);
|
||||
|
||||
function Graph({
|
||||
@ -62,6 +64,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
}: GraphProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -99,6 +102,30 @@ function Graph({
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
annotation: staticLine
|
||||
? {
|
||||
annotations: [
|
||||
{
|
||||
type: 'line',
|
||||
yMin: staticLine.yMin,
|
||||
yMax: staticLine.yMax,
|
||||
borderColor: staticLine.borderColor,
|
||||
borderWidth: staticLine.borderWidth,
|
||||
label: {
|
||||
content: staticLine.lineText,
|
||||
enabled: true,
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
borderWidth: 0,
|
||||
position: 'start',
|
||||
backgroundColor: 'transparent',
|
||||
color: staticLine.textColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
title: {
|
||||
display: title !== undefined,
|
||||
text: title,
|
||||
@ -180,6 +207,7 @@ function Graph({
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
@ -205,6 +233,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -229,6 +258,16 @@ interface GraphProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
yMin: number | undefined;
|
||||
yMax: number | undefined;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
lineText: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export type GraphOnClickHandler = (
|
||||
@ -245,5 +284,6 @@ Graph.defaultProps = {
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
3
frontend/src/constants/api.ts
Normal file
3
frontend/src/constants/api.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const getVersion = 'version';
|
||||
|
||||
export { getVersion };
|
@ -1,31 +1,85 @@
|
||||
import { notification, Table, Tooltip, Typography } from 'antd';
|
||||
import { notification, Table, TableProps, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import getAll from 'api/errors/getAll';
|
||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect } from 'react';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Exception } from 'types/api/errors/getAll';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
getDefaultOrder,
|
||||
getNanoSeconds,
|
||||
getOffSet,
|
||||
getOrder,
|
||||
getOrderParams,
|
||||
getUpdatePageSize,
|
||||
urlKey,
|
||||
} from './utils';
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { search, pathname } = useLocation();
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], {
|
||||
queryFn: () =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
}),
|
||||
});
|
||||
const updatedOrder = getOrder(params.get(urlKey.order));
|
||||
const getUpdatedOffset = getOffSet(params.get(urlKey.offset));
|
||||
const getUpdatedParams = getOrderParams(params.get(urlKey.orderParam));
|
||||
const getUpdatedPageSize = getUpdatePageSize(params.get(urlKey.pageSize));
|
||||
|
||||
const updatedPath = useMemo(
|
||||
() =>
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
pageSize: getUpdatedPageSize,
|
||||
})}`,
|
||||
[
|
||||
pathname,
|
||||
updatedOrder,
|
||||
getUpdatedOffset,
|
||||
getUpdatedParams,
|
||||
getUpdatedPageSize,
|
||||
],
|
||||
);
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
order: updatedOrder,
|
||||
limit: getUpdatedPageSize,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
{
|
||||
queryKey: ['getErrorCounts', maxTime, minTime],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.error) {
|
||||
@ -35,11 +89,9 @@ function AllErrors(): JSX.Element {
|
||||
}
|
||||
}, [data?.error, data?.payload, t]);
|
||||
|
||||
const getDateValue = (value: string): JSX.Element => {
|
||||
return (
|
||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||
);
|
||||
};
|
||||
const getDateValue = (value: string): JSX.Element => (
|
||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Exception> = [
|
||||
{
|
||||
@ -49,14 +101,20 @@ function AllErrors(): JSX.Element {
|
||||
render: (value, record): JSX.Element => (
|
||||
<Tooltip overlay={(): JSX.Element => value}>
|
||||
<Link
|
||||
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
|
||||
to={`${ROUTES.ERROR_DETAIL}?groupId=${
|
||||
record.groupID
|
||||
}×tamp=${getNanoSeconds(record.lastSeen)}`}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
),
|
||||
sorter: (a, b): number =>
|
||||
a.exceptionType.charCodeAt(0) - b.exceptionType.charCodeAt(0),
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'exceptionType',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Error Message',
|
||||
@ -78,39 +136,86 @@ function AllErrors(): JSX.Element {
|
||||
title: 'Count',
|
||||
dataIndex: 'exceptionCount',
|
||||
key: 'exceptionCount',
|
||||
sorter: (a, b): number => a.exceptionCount - b.exceptionCount,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'exceptionCount',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Last Seen',
|
||||
dataIndex: 'lastSeen',
|
||||
key: 'lastSeen',
|
||||
render: getDateValue,
|
||||
sorter: (a, b): number =>
|
||||
dayjs(b.lastSeen).isBefore(dayjs(a.lastSeen)) === true ? 1 : 0,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'lastSeen',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'First Seen',
|
||||
dataIndex: 'firstSeen',
|
||||
key: 'firstSeen',
|
||||
render: getDateValue,
|
||||
sorter: (a, b): number =>
|
||||
dayjs(b.firstSeen).isBefore(dayjs(a.firstSeen)) === true ? 1 : 0,
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'firstSeen',
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
sorter: (a, b): number =>
|
||||
a.serviceName.charCodeAt(0) - b.serviceName.charCodeAt(0),
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
getUpdatedParams,
|
||||
updatedOrder,
|
||||
'serviceName',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = (
|
||||
paginations,
|
||||
_,
|
||||
sorter,
|
||||
) => {
|
||||
if (!Array.isArray(sorter)) {
|
||||
const { pageSize = 0, current = 0 } = paginations;
|
||||
const { columnKey = '', order } = sorter;
|
||||
const updatedOrder = order === 'ascend' ? 'ascending' : 'descending';
|
||||
|
||||
history.replace(
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: (current - 1) * pageSize,
|
||||
orderParam: columnKey,
|
||||
pageSize,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
tableLayout="fixed"
|
||||
dataSource={data?.payload as Exception[]}
|
||||
columns={columns}
|
||||
loading={isLoading || false}
|
||||
rowKey="firstSeen"
|
||||
loading={isLoading || false || errorCountResponse.status === 'loading'}
|
||||
pagination={{
|
||||
pageSize: getUpdatedPageSize,
|
||||
responsive: true,
|
||||
current: getUpdatedOffset / 10 + 1,
|
||||
position: ['bottomLeft'],
|
||||
total: errorCountResponse.data?.payload || 0,
|
||||
}}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
109
frontend/src/container/AllError/utils.test.ts
Normal file
109
frontend/src/container/AllError/utils.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Order, OrderBy } from 'types/api/errors/getAll';
|
||||
|
||||
import {
|
||||
getDefaultOrder,
|
||||
getLimit,
|
||||
getOffSet,
|
||||
getOrder,
|
||||
getOrderParams,
|
||||
getUpdatePageSize,
|
||||
isOrder,
|
||||
isOrderParams,
|
||||
} from './utils';
|
||||
|
||||
describe('Error utils', () => {
|
||||
test('Valid OrderBy Params', () => {
|
||||
expect(isOrderParams('serviceName')).toBe(true);
|
||||
expect(isOrderParams('exceptionCount')).toBe(true);
|
||||
expect(isOrderParams('lastSeen')).toBe(true);
|
||||
expect(isOrderParams('firstSeen')).toBe(true);
|
||||
expect(isOrderParams('exceptionType')).toBe(true);
|
||||
});
|
||||
|
||||
test('Invalid OrderBy Params', () => {
|
||||
expect(isOrderParams('invalid')).toBe(false);
|
||||
expect(isOrderParams(null)).toBe(false);
|
||||
expect(isOrderParams('')).toBe(false);
|
||||
});
|
||||
|
||||
test('Valid Order', () => {
|
||||
expect(isOrder('ascending')).toBe(true);
|
||||
expect(isOrder('descending')).toBe(true);
|
||||
});
|
||||
|
||||
test('Invalid Order', () => {
|
||||
expect(isOrder('invalid')).toBe(false);
|
||||
expect(isOrder(null)).toBe(false);
|
||||
expect(isOrder('')).toBe(false);
|
||||
});
|
||||
|
||||
test('Default Order', () => {
|
||||
const OrderBy: OrderBy[] = [
|
||||
'exceptionCount',
|
||||
'exceptionType',
|
||||
'firstSeen',
|
||||
'lastSeen',
|
||||
'serviceName',
|
||||
];
|
||||
|
||||
const order: Order[] = ['ascending', 'descending'];
|
||||
|
||||
const ascOrd = order[0];
|
||||
const desOrd = order[1];
|
||||
|
||||
OrderBy.forEach((order) => {
|
||||
expect(getDefaultOrder(order, ascOrd, order)).toBe('ascend');
|
||||
expect(getDefaultOrder(order, desOrd, order)).toBe('descend');
|
||||
});
|
||||
});
|
||||
|
||||
test('Limit', () => {
|
||||
expect(getLimit(null)).toBe(10);
|
||||
expect(getLimit('')).toBe(10);
|
||||
expect(getLimit('0')).toBe(0);
|
||||
expect(getLimit('1')).toBe(1);
|
||||
expect(getLimit('10')).toBe(10);
|
||||
expect(getLimit('11')).toBe(11);
|
||||
expect(getLimit('100')).toBe(100);
|
||||
expect(getLimit('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Update Page Size', () => {
|
||||
expect(getUpdatePageSize(null)).toBe(10);
|
||||
expect(getUpdatePageSize('')).toBe(10);
|
||||
expect(getUpdatePageSize('0')).toBe(0);
|
||||
expect(getUpdatePageSize('1')).toBe(1);
|
||||
expect(getUpdatePageSize('10')).toBe(10);
|
||||
expect(getUpdatePageSize('11')).toBe(11);
|
||||
expect(getUpdatePageSize('100')).toBe(100);
|
||||
expect(getUpdatePageSize('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Order Params', () => {
|
||||
expect(getOrderParams(null)).toBe('serviceName');
|
||||
expect(getOrderParams('')).toBe('serviceName');
|
||||
expect(getOrderParams('serviceName')).toBe('serviceName');
|
||||
expect(getOrderParams('exceptionCount')).toBe('exceptionCount');
|
||||
expect(getOrderParams('lastSeen')).toBe('lastSeen');
|
||||
expect(getOrderParams('firstSeen')).toBe('firstSeen');
|
||||
expect(getOrderParams('exceptionType')).toBe('exceptionType');
|
||||
});
|
||||
|
||||
test('OffSet', () => {
|
||||
expect(getOffSet(null)).toBe(0);
|
||||
expect(getOffSet('')).toBe(0);
|
||||
expect(getOffSet('0')).toBe(0);
|
||||
expect(getOffSet('1')).toBe(1);
|
||||
expect(getOffSet('10')).toBe(10);
|
||||
expect(getOffSet('11')).toBe(11);
|
||||
expect(getOffSet('100')).toBe(100);
|
||||
expect(getOffSet('101')).toBe(101);
|
||||
});
|
||||
|
||||
test('Order', () => {
|
||||
expect(getOrder(null)).toBe('ascending');
|
||||
expect(getOrder('')).toBe('ascending');
|
||||
expect(getOrder('ascending')).toBe('ascending');
|
||||
expect(getOrder('descending')).toBe('descending');
|
||||
});
|
||||
});
|
89
frontend/src/container/AllError/utils.ts
Normal file
89
frontend/src/container/AllError/utils.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import Timestamp from 'timestamp-nano';
|
||||
import { Order, OrderBy } from 'types/api/errors/getAll';
|
||||
|
||||
export const isOrder = (order: string | null): order is Order =>
|
||||
!!(order === 'ascending' || order === 'descending');
|
||||
|
||||
export const urlKey = {
|
||||
order: 'order',
|
||||
offset: 'offset',
|
||||
orderParam: 'orderParam',
|
||||
pageSize: 'pageSize',
|
||||
};
|
||||
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
||||
return !!(
|
||||
orderBy === 'serviceName' ||
|
||||
orderBy === 'exceptionCount' ||
|
||||
orderBy === 'lastSeen' ||
|
||||
orderBy === 'firstSeen' ||
|
||||
orderBy === 'exceptionType'
|
||||
);
|
||||
};
|
||||
|
||||
export const getOrder = (order: string | null): Order => {
|
||||
if (isOrder(order)) {
|
||||
return order;
|
||||
}
|
||||
return 'ascending';
|
||||
};
|
||||
|
||||
export const getLimit = (limit: string | null): number => {
|
||||
if (limit) {
|
||||
return parseInt(limit, 10);
|
||||
}
|
||||
return 10;
|
||||
};
|
||||
|
||||
export const getOffSet = (offset: string | null): number => {
|
||||
if (offset && typeof offset === 'string') {
|
||||
return parseInt(offset, 10);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const getOrderParams = (order: string | null): OrderBy => {
|
||||
if (isOrderParams(order)) {
|
||||
return order;
|
||||
}
|
||||
return 'serviceName';
|
||||
};
|
||||
|
||||
export const getDefaultOrder = (
|
||||
orderBy: OrderBy,
|
||||
order: Order,
|
||||
data: OrderBy,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): SortOrder | undefined => {
|
||||
if (orderBy === 'exceptionType' && data === 'exceptionType') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'serviceName' && data === 'serviceName') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'exceptionCount' && data === 'exceptionCount') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'lastSeen' && data === 'lastSeen') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
if (orderBy === 'firstSeen' && data === 'firstSeen') {
|
||||
return order === 'ascending' ? 'ascend' : 'descend';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getNanoSeconds = (date: string): number => {
|
||||
return (
|
||||
parseInt((new Date(date).getTime() / 1e3).toString(), 10) * 1e9 +
|
||||
Timestamp.fromString(date).getNano()
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdatePageSize = (pageSize: string | null): number => {
|
||||
if (pageSize) {
|
||||
return parseInt(pageSize, 10);
|
||||
}
|
||||
return 10;
|
||||
};
|
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
function CreateRules({ initialValue }: CreateRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateRulesProps {
|
||||
initialValue: AlertDef;
|
||||
}
|
||||
|
||||
export default CreateRules;
|
@ -1,102 +1,23 @@
|
||||
import { SaveFilled } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import put from 'api/alerts/put';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/get';
|
||||
import { PayloadProps as PutPayloadProps } from 'types/api/alerts/put';
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
function EditRules({ initialData, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(initialData);
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const [editButtonState, setEditButtonState] = useState<State<PutPayloadProps>>(
|
||||
{
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(async () => {
|
||||
try {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
const response = await put({
|
||||
data: value,
|
||||
id: parseInt(ruleId, 10),
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was Edited correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
error: true,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
}, [value, ruleId, notifications]);
|
||||
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={editButtonState.loading || false}
|
||||
disabled={editButtonState.loading || false}
|
||||
icon={<SaveFilled />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={ruleId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditRulesProps {
|
||||
initialData: PayloadProps['data'];
|
||||
ruleId: string;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
@ -1,25 +1,49 @@
|
||||
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
|
||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||
import Editor from 'components/Editor';
|
||||
import { getNanoSeconds } from 'container/AllError/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import history from 'lib/history';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { PayloadProps } from 'types/api/errors/getById';
|
||||
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
|
||||
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
const { idPayload } = props;
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation(['errorDetails', 'common']);
|
||||
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const queryErrorId = params.get('errorId');
|
||||
const serviceName = params.get('serviceName');
|
||||
const errorType = params.get('errorType');
|
||||
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const errorId = params.get(urlKey.errorId);
|
||||
const serviceName = params.get(urlKey.serviceName);
|
||||
const errorType = params.get(urlKey.exceptionType);
|
||||
const timestamp = params.get(urlKey.timestamp);
|
||||
|
||||
const { data: nextPrevData, status: nextPrevStatus } = useQuery(
|
||||
[
|
||||
idPayload.errorId,
|
||||
idPayload.groupID,
|
||||
idPayload.timestamp,
|
||||
errorId,
|
||||
serviceName,
|
||||
errorType,
|
||||
timestamp,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getNextPrevId({
|
||||
errorID: errorId || idPayload.errorId,
|
||||
groupID: idPayload.groupID,
|
||||
timestamp: timestamp || getNanoSeconds(idPayload.timestamp).toString(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const errorDetail = idPayload;
|
||||
|
||||
@ -48,34 +72,32 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
'errorId',
|
||||
'timestamp',
|
||||
'exceptionMessage',
|
||||
'newerErrorId',
|
||||
'olderErrorId',
|
||||
'exceptionEscaped',
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const onClickErrorIdHandler = async (id: string): Promise<void> => {
|
||||
const onClickErrorIdHandler = async (
|
||||
id: string,
|
||||
timespamp: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (id.length === 0) {
|
||||
notification.error({
|
||||
message: 'Error Id cannot be empty',
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}?errorId=${id}&serviceName=${serviceName}&errorType=${errorType}`,
|
||||
history.replace(
|
||||
`${history.location.pathname}?&groupId=${
|
||||
idPayload.groupID
|
||||
}×tamp=${getNanoSeconds(timespamp)}&errorId=${id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -106,25 +128,25 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
<div>
|
||||
<Space align="end" direction="horizontal">
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
errorDetail.olderErrorId.length === 0 ||
|
||||
queryErrorId === errorDetail.olderErrorId
|
||||
}
|
||||
loading={nextPrevStatus === 'loading'}
|
||||
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
|
||||
onClick={(): Promise<void> =>
|
||||
onClickErrorIdHandler(errorDetail.olderErrorId)
|
||||
onClickErrorIdHandler(
|
||||
nextPrevData?.payload?.prevErrorID || '',
|
||||
nextPrevData?.payload?.prevTimestamp || '',
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('older')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
errorDetail.newerErrorId.length === 0 ||
|
||||
queryErrorId === errorDetail.newerErrorId
|
||||
}
|
||||
loading={nextPrevStatus === 'loading'}
|
||||
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
|
||||
onClick={(): Promise<void> =>
|
||||
onClickErrorIdHandler(errorDetail.newerErrorId)
|
||||
onClickErrorIdHandler(
|
||||
nextPrevData?.payload?.nextErrorID || '',
|
||||
nextPrevData?.payload?.nextTimestamp || '',
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('newer')}
|
||||
@ -153,7 +175,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface ErrorDetailsProps {
|
||||
idPayload: PayloadProps;
|
||||
idPayload: GetByErrorTypeAndServicePayload;
|
||||
}
|
||||
|
||||
export default ErrorDetails;
|
||||
|
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { Select } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
|
||||
import LabelSelect from './labels';
|
||||
import {
|
||||
FormContainer,
|
||||
InputSmall,
|
||||
SeveritySelect,
|
||||
StepHeading,
|
||||
TextareaMedium,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface BasicInfoProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
}
|
||||
|
||||
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={t('field_severity')}
|
||||
labelAlign="left"
|
||||
name={['labels', 'severity']}
|
||||
>
|
||||
<SeveritySelect
|
||||
defaultValue="critical"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...alertDef.labels,
|
||||
severity: s,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="critical">{t('option_critical')}</Option>
|
||||
<Option value="error">{t('option_error')}</Option>
|
||||
<Option value="warning">{t('option_warning')}</Option>
|
||||
<Option value="info">{t('option_info')}</Option>
|
||||
</SeveritySelect>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||
<InputSmall
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
alert: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={t('field_alert_desc')}
|
||||
labelAlign="left"
|
||||
name={['annotations', 'description']}
|
||||
>
|
||||
<TextareaMedium
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
annotations: {
|
||||
...alertDef.annotations,
|
||||
description: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('field_labels')}>
|
||||
<LabelSelect
|
||||
onSetLabels={(l: Labels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...l,
|
||||
},
|
||||
});
|
||||
}}
|
||||
initialValues={alertDef.labels}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BasicInfo;
|
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { Query } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | undefined;
|
||||
graphType?: GRAPH_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time;
|
||||
headline?: JSX.Element;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = 'TIME_SERIES',
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
selectedInterval = '5min',
|
||||
headline,
|
||||
threshold,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('rules');
|
||||
const staticLine: StaticLineProps | undefined =
|
||||
threshold && threshold > 0
|
||||
? {
|
||||
yMin: threshold,
|
||||
yMax: threshold,
|
||||
borderColor: '#f14',
|
||||
borderWidth: 1,
|
||||
lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`,
|
||||
textColor: '#f14',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const queryKey = JSON.stringify(query);
|
||||
const queryResponse = useQuery({
|
||||
queryKey: ['chartPreview', queryKey, selectedInterval],
|
||||
queryFn: () =>
|
||||
GetMetricQueryRange({
|
||||
query: query || {
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: {
|
||||
formulas: [],
|
||||
queryBuilder: [],
|
||||
},
|
||||
clickHouse: [],
|
||||
},
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
}),
|
||||
enabled:
|
||||
query != null &&
|
||||
(query.queryType !== EQueryType.PROM ||
|
||||
(query.promQL?.length > 0 && query.promQL[0].query !== '')),
|
||||
});
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result
|
||||
? queryResponse?.data?.payload?.data?.result
|
||||
: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
{headline}
|
||||
{(queryResponse?.data?.error || queryResponse?.isError) && (
|
||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||
<InfoCircleOutlined />{' '}
|
||||
{queryResponse?.data?.error ||
|
||||
queryResponse?.error ||
|
||||
t('preview_chart_unexpected_error')}
|
||||
</FailedMessageContainer>
|
||||
)}
|
||||
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridGraphComponent
|
||||
title={name}
|
||||
data={chartDataSet}
|
||||
isStacked
|
||||
GRAPH_TYPES={graphType || 'TIME_SERIES'}
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ChartPreview.defaultProps = {
|
||||
graphType: 'TIME_SERIES',
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
selectedInterval: '5min',
|
||||
headline: undefined,
|
||||
threshold: 0,
|
||||
};
|
||||
|
||||
export default ChartPreview;
|
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 55vh;
|
||||
`;
|
||||
|
||||
export const FailedMessageContainer = styled(Tooltip)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
`;
|
||||
|
||||
export const ChartContainer = styled(Card)`
|
||||
border-radius: 4px;
|
||||
&&& {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 1.5rem 0;
|
||||
height: 57vh;
|
||||
/* padding-bottom: 2rem; */
|
||||
}
|
||||
`;
|
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
|
||||
import { IPromQLQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/types';
|
||||
import React from 'react';
|
||||
import { IPromQueries } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
function PromqlSection({
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: PromqlSectionProps): JSX.Element {
|
||||
const handlePromQLQueryChange = ({
|
||||
query,
|
||||
legend,
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
let promQuery = promQueries.A;
|
||||
|
||||
// todo(amol): how to remove query, make it null?
|
||||
if (query) promQuery.query = query;
|
||||
if (legend) promQuery.legend = legend;
|
||||
if (toggleDelete) {
|
||||
promQuery = {
|
||||
query: '',
|
||||
legend: '',
|
||||
name: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
setPromQueries({
|
||||
A: {
|
||||
...promQuery,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<PromQLQueryBuilder
|
||||
key="A"
|
||||
queryIndex="A"
|
||||
queryData={{ ...promQueries?.A, name: 'A' }}
|
||||
handleQueryChange={handlePromQLQueryChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface PromqlSectionProps {
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default PromqlSection;
|
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { notification, Tabs } from 'antd';
|
||||
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
|
||||
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
|
||||
import {
|
||||
IQueryBuilderFormulaHandleChange,
|
||||
IQueryBuilderQueryHandleChange,
|
||||
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PromqlSection from './PromqlSection';
|
||||
import { FormContainer, QueryButton, StepHeading } from './styles';
|
||||
import { toIMetricsBuilderQuery } from './utils';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
function QuerySection({
|
||||
queryCategory,
|
||||
setQueryCategory,
|
||||
metricQueries,
|
||||
setMetricQueries,
|
||||
formulaQueries,
|
||||
setFormulaQueries,
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: QuerySectionProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
const handleQueryCategoryChange = (s: string): void => {
|
||||
if (
|
||||
parseInt(s, 10) === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
setPromQueries({
|
||||
A: {
|
||||
query: '',
|
||||
stats: '',
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setQueryCategory(parseInt(s, 10));
|
||||
};
|
||||
|
||||
const getNextQueryLabel = useCallback((): string => {
|
||||
let maxAscii = 0;
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
const n = key.charCodeAt(0);
|
||||
if (n > maxAscii) {
|
||||
maxAscii = n - 64;
|
||||
}
|
||||
});
|
||||
|
||||
return String.fromCharCode(64 + maxAscii + 1);
|
||||
}, [metricQueries]);
|
||||
|
||||
const handleFormulaChange = ({
|
||||
formulaIndex,
|
||||
expression,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderFormulaHandleChange): void => {
|
||||
const allFormulas = formulaQueries;
|
||||
const current = allFormulas[formulaIndex];
|
||||
if (expression) {
|
||||
current.expression = expression;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allFormulas[formulaIndex];
|
||||
} else {
|
||||
allFormulas[formulaIndex] = current;
|
||||
}
|
||||
|
||||
setFormulaQueries({
|
||||
...allFormulas,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMetricQueryChange = ({
|
||||
queryIndex,
|
||||
aggregateFunction,
|
||||
metricName,
|
||||
tagFilters,
|
||||
groupBy,
|
||||
legend,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries = metricQueries;
|
||||
const current = metricQueries[queryIndex];
|
||||
if (aggregateFunction) {
|
||||
current.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
if (metricName) {
|
||||
current.metricName = metricName;
|
||||
}
|
||||
|
||||
if (tagFilters && current.tagFilters) {
|
||||
current.tagFilters.items = tagFilters;
|
||||
}
|
||||
|
||||
if (legend) {
|
||||
current.legend = legend;
|
||||
}
|
||||
|
||||
if (groupBy) {
|
||||
current.groupBy = groupBy;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allQueries[queryIndex];
|
||||
} else {
|
||||
allQueries[queryIndex] = current;
|
||||
}
|
||||
|
||||
setMetricQueries({
|
||||
...allQueries,
|
||||
});
|
||||
};
|
||||
|
||||
const addMetricQuery = useCallback(() => {
|
||||
if (Object.keys(metricQueries).length > 5) {
|
||||
notification.error({
|
||||
message: t('metric_query_max_limit'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryLabel = getNextQueryLabel();
|
||||
|
||||
const queries = metricQueries;
|
||||
queries[queryLabel] = {
|
||||
name: queryLabel,
|
||||
queryName: queryLabel,
|
||||
metricName: '',
|
||||
formulaOnly: false,
|
||||
aggregateOperator: EAggregateOperator.NOOP,
|
||||
legend: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
expression: queryLabel,
|
||||
};
|
||||
setMetricQueries({ ...queries });
|
||||
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
|
||||
|
||||
const addFormula = useCallback(() => {
|
||||
// defaulting to F1 as only one formula is supported
|
||||
// in alert definition
|
||||
const queryLabel = 'F1';
|
||||
|
||||
const formulas = formulaQueries;
|
||||
formulas[queryLabel] = {
|
||||
queryName: queryLabel,
|
||||
name: queryLabel,
|
||||
formulaOnly: true,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
setFormulaQueries({ ...formulas });
|
||||
}, [formulaQueries, setFormulaQueries]);
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => {
|
||||
return (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormulaButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderQueryButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMetricUI = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{metricQueries &&
|
||||
Object.keys(metricQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = metricQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilder
|
||||
key={key}
|
||||
queryIndex={key}
|
||||
queryData={toIMetricsBuilderQuery(current)}
|
||||
selectedGraph="TIME_SERIES"
|
||||
handleQueryChange={handleMetricQueryChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{queryCategory !== EQueryType.PROM && renderQueryButton()}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
{formulaQueries &&
|
||||
Object.keys(formulaQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = formulaQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilderFormula
|
||||
key={key}
|
||||
formulaIndex={key}
|
||||
formulaData={current}
|
||||
handleFormulaChange={handleFormulaChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||
metricQueries &&
|
||||
Object.keys(metricQueries).length > 0 &&
|
||||
renderFormulaButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
<FormContainer>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
|
||||
activeKey={queryCategory.toString()}
|
||||
onChange={handleQueryCategoryChange}
|
||||
>
|
||||
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
|
||||
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
|
||||
</Tabs>
|
||||
</div>
|
||||
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuerySectionProps {
|
||||
queryCategory: EQueryType;
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
metricQueries: IMetricQueries;
|
||||
setMetricQueries: (b: IMetricQueries) => void;
|
||||
formulaQueries: IFormulaQueries;
|
||||
setFormulaQueries: (b: IFormulaQueries) => void;
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
175
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
175
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultCompareOp,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
FormContainer,
|
||||
InlineSelect,
|
||||
StepHeading,
|
||||
ThresholdInput,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function RuleOptions({
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
queryCategory,
|
||||
}: RuleOptionsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
const handleMatchOptChange = (value: string | unknown): void => {
|
||||
const m = (value as string) || alertDef.condition?.matchType;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: m,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderCompareOps = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const newOp = (value as string) || '';
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
op: newOp,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="1">{t('option_above')}</Option>
|
||||
<Option value="2">{t('option_below')}</Option>
|
||||
<Option value="3">{t('option_equal')}</Option>
|
||||
<Option value="4">{t('option_notequal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
<Option value="2">{t('option_allthetimes')}</Option>
|
||||
<Option value="3">{t('option_onaverage')}</Option>
|
||||
<Option value="4">{t('option_intotal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultEvalWindow}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const ew = (value as string) || alertDef.evalWindow;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
evalWindow: ew,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Option value="5m0s">{t('option_5min')}</Option>
|
||||
<Option value="10m0s">{t('option_10min')}</Option>
|
||||
<Option value="15m0s">{t('option_15min')}</Option>
|
||||
<Option value="60m0s">{t('option_60min')}</Option>
|
||||
<Option value="4h0m0s">{t('option_4hours')}</Option>
|
||||
<Option value="24h0m0s">{t('option_24hours')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdRuleOpts = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
const renderPromRuleOptions = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderPromMatchOpts()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading>{t('alert_form_step2')}</StepHeading>
|
||||
<FormContainer>
|
||||
{queryCategory === EQueryType.PROM
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ThresholdInput
|
||||
controls={false}
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={(value: number | unknown): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
target: (value as number) || undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface RuleOptionsProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
queryCategory: EQueryType;
|
||||
}
|
||||
export default RuleOptions;
|
366
frontend/src/container/FormAlertRules/index.tsx
Normal file
366
frontend/src/container/FormAlertRules/index.tsx
Normal file
@ -0,0 +1,366 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { FormInstance, Modal, notification, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { Query as StagedQuery } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
import ChartPreview from './ChartPreview';
|
||||
import QuerySection from './QuerySection';
|
||||
import RuleOptions from './RuleOptions';
|
||||
import { ActionButton, ButtonContainer, MainFormContainer } from './styles';
|
||||
import useDebounce from './useDebounce';
|
||||
import {
|
||||
prepareBuilderQueries,
|
||||
prepareStagedQuery,
|
||||
toChartInterval,
|
||||
toFormulaQueries,
|
||||
toMetricQueries,
|
||||
} from './utils';
|
||||
|
||||
function FormAlertRules({
|
||||
formInstance,
|
||||
initialValue,
|
||||
ruleId,
|
||||
}: FormAlertRuleProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
|
||||
// initQuery contains initial query when component was mounted
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
|
||||
const [queryCategory, setQueryCategory] = useState<EQueryType>(
|
||||
initQuery?.queryType,
|
||||
);
|
||||
|
||||
// local state to handle metric queries
|
||||
const [metricQueries, setMetricQueries] = useState<IMetricQueries>(
|
||||
toMetricQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle formula queries
|
||||
const [formulaQueries, setFormulaQueries] = useState<IFormulaQueries>(
|
||||
toFormulaQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle promql queries
|
||||
const [promQueries, setPromQueries] = useState<IPromQueries>({
|
||||
...initQuery?.promQueries,
|
||||
});
|
||||
|
||||
// staged query is used to display chart preview
|
||||
const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
|
||||
const debouncedStagedQuery = useDebounce(stagedQuery, 500);
|
||||
|
||||
// this use effect initiates staged query and
|
||||
// other queries based on server data.
|
||||
// useful when fetching of initial values (from api)
|
||||
// is delayed
|
||||
useEffect(() => {
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
const typ = initQuery?.queryType;
|
||||
|
||||
// extract metric query from builderQueries
|
||||
const mq = toMetricQueries(initQuery?.builderQueries);
|
||||
|
||||
// extract formula query from builderQueries
|
||||
const fq = toFormulaQueries(initQuery?.builderQueries);
|
||||
|
||||
// prepare staged query
|
||||
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries);
|
||||
const pq = initQuery?.promQueries;
|
||||
|
||||
setQueryCategory(typ);
|
||||
setMetricQueries(mq);
|
||||
setFormulaQueries(fq);
|
||||
setPromQueries(pq);
|
||||
setStagedQuery(sq);
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
// this useEffect updates staging query when
|
||||
// any of its sub-parameters changes
|
||||
useEffect(() => {
|
||||
// prepare staged query
|
||||
const sq: StagedQuery = prepareStagedQuery(
|
||||
queryCategory,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
);
|
||||
setStagedQuery(sq);
|
||||
}, [queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, []);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
setQueryCategory(val);
|
||||
if (val === EQueryType.PROM) {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
|
||||
if (!alertDef.alert || alertDef.alert === '') {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('alertname_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
queryCategory === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('promql_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
|
||||
Object.keys(metricQueries).length === 0
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('condition_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
if (metricQueries[key].metricName === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('metricname_missing', { where: metricQueries[key].name }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(formulaQueries).forEach((key) => {
|
||||
if (formulaQueries[key].expression === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('expression_missing', formulaQueries[key].name),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return retval;
|
||||
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const saveRule = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postableAlert: AlertDef = {
|
||||
...alertDef,
|
||||
source: window?.location.toString(),
|
||||
ruleType:
|
||||
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
compositeMetricQuery: {
|
||||
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
|
||||
promQueries,
|
||||
queryType: queryCategory,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiReq =
|
||||
ruleId && ruleId > 0
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: 'Success',
|
||||
description:
|
||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||
});
|
||||
console.log('invalidting cache');
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries(['ruleId', ruleId]);
|
||||
|
||||
setTimeout(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('save alert api failed:', e);
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [
|
||||
t,
|
||||
isFormValid,
|
||||
queryCategory,
|
||||
ruleId,
|
||||
alertDef,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
ruleCache,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
<Typography.Text>
|
||||
{' '}
|
||||
{t('confirm_save_content_part1')} <QueryTypeTag queryType={queryCategory} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
);
|
||||
Modal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk() {
|
||||
saveRule();
|
||||
},
|
||||
});
|
||||
}, [t, saveRule, queryCategory]);
|
||||
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
<MainFormContainer
|
||||
initialValues={initialValue}
|
||||
layout="vertical"
|
||||
form={formInstance}
|
||||
>
|
||||
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
|
||||
{queryCategory === EQueryType.PROM && renderPromChartPreview()}
|
||||
<QuerySection
|
||||
queryCategory={queryCategory}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
metricQueries={metricQueries}
|
||||
setMetricQueries={setMetricQueries}
|
||||
formulaQueries={formulaQueries}
|
||||
setFormulaQueries={setFormulaQueries}
|
||||
promQueries={promQueries}
|
||||
setPromQueries={setPromQueries}
|
||||
/>
|
||||
|
||||
<RuleOptions
|
||||
queryCategory={queryCategory}
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
/>
|
||||
|
||||
{renderBasicInfo()}
|
||||
<ButtonContainer>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={loading || false}
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
>
|
||||
{ruleId === 0 && t('button_cancelchanges')}
|
||||
{ruleId > 0 && t('button_discard')}
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormAlertRuleProps {
|
||||
formInstance: FormInstance;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default FormAlertRules;
|
@ -0,0 +1,49 @@
|
||||
import { createMachine } from 'xstate';
|
||||
|
||||
export const ResourceAttributesFilterMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
|
||||
createMachine({
|
||||
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
|
||||
initial: 'Idle',
|
||||
states: {
|
||||
LabelKey: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
LabelValue: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: ['onValidateQuery'],
|
||||
},
|
||||
onBlur: {
|
||||
actions: ['onValidateQuery'],
|
||||
// target: 'Idle',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Idle: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelKey',
|
||||
description: 'Enter a label key',
|
||||
target: 'LabelKey',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: 'Label Key Values',
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
eventsCausingActions: {
|
||||
onSelectLabelValue: 'NEXT' | 'onBlur';
|
||||
onValidateQuery: 'NEXT' | 'onBlur';
|
||||
onSelectLabelKey: 'NEXT';
|
||||
};
|
||||
internalEvents: {
|
||||
'xstate.init': { type: 'xstate.init' };
|
||||
};
|
||||
invokeSrcNameMap: {};
|
||||
missingImplementations: {
|
||||
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
|
||||
services: never;
|
||||
guards: never;
|
||||
delays: never;
|
||||
};
|
||||
eventsCausingServices: {};
|
||||
eventsCausingGuards: {};
|
||||
eventsCausingDelays: {};
|
||||
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
|
||||
tags: never;
|
||||
}
|
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryChipContainer, QueryChipItem } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
interface QueryChipProps {
|
||||
queryData: ILabelRecord;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function QueryChip({
|
||||
queryData,
|
||||
onRemove,
|
||||
}: QueryChipProps): JSX.Element {
|
||||
const { key, value } = queryData;
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
<QueryChipItem
|
||||
closable={key !== 'severity' && key !== 'description'}
|
||||
onClose={(): void => onRemove(key)}
|
||||
>
|
||||
{key}: {value}
|
||||
</QueryChipItem>
|
||||
</QueryChipContainer>
|
||||
);
|
||||
}
|
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import {
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, Input, message, Modal } from 'antd';
|
||||
import { map } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
||||
import QueryChip from './QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
import { createQuery, flattenLabels, prepareLabels } from './utils';
|
||||
|
||||
interface LabelSelectProps {
|
||||
onSetLabels: (q: Labels) => void;
|
||||
initialValues: Labels | undefined;
|
||||
}
|
||||
|
||||
function LabelSelect({
|
||||
onSetLabels,
|
||||
initialValues,
|
||||
}: LabelSelectProps): JSX.Element | null {
|
||||
const { t } = useTranslation('rules');
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [currentVal, setCurrentVal] = useState('');
|
||||
const [staging, setStaging] = useState<string[]>([]);
|
||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||
initialValues ? flattenLabels(initialValues) : [],
|
||||
);
|
||||
|
||||
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
|
||||
onSetLabels(prepareLabels(updatedRecs, initialValues));
|
||||
setQueries(updatedRecs);
|
||||
};
|
||||
|
||||
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
|
||||
actions: {
|
||||
onSelectLabelKey: () => {},
|
||||
onSelectLabelValue: () => {
|
||||
if (currentVal !== '') {
|
||||
setStaging((prevState) => [...prevState, currentVal]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setCurrentVal('');
|
||||
},
|
||||
onValidateQuery: (): void => {
|
||||
if (currentVal === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedQuery = createQuery([...staging, currentVal]);
|
||||
|
||||
if (generatedQuery) {
|
||||
dispatchChanges([...queries, generatedQuery]);
|
||||
setStaging([]);
|
||||
setCurrentVal('');
|
||||
send('RESET');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleFocus = (): void => {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = useCallback((): void => {
|
||||
if (staging.length === 1 && staging[0] !== undefined) {
|
||||
send('onBlur');
|
||||
}
|
||||
}, [send, staging]);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlur();
|
||||
}, [handleBlur]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentVal(e.target?.value);
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
dispatchChanges(queries.filter((queryData) => queryData.key !== key));
|
||||
};
|
||||
|
||||
const handleClearAll = (): void => {
|
||||
Modal.confirm({
|
||||
title: 'Confirm',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('remove_label_confirm'),
|
||||
onOk() {
|
||||
send('RESET');
|
||||
dispatchChanges([]);
|
||||
setStaging([]);
|
||||
message.success(t('remove_label_success'));
|
||||
},
|
||||
okText: t('button_yes'),
|
||||
cancelText: t('button_no'),
|
||||
});
|
||||
};
|
||||
const renderPlaceholder = useCallback((): string => {
|
||||
if (state.value === 'LabelKey') return 'Enter a label key then press ENTER.';
|
||||
if (state.value === 'LabelValue')
|
||||
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
|
||||
return t('placeholder_label_key_pair');
|
||||
}, [t, state, staging]);
|
||||
return (
|
||||
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
|
||||
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleChange}
|
||||
onKeyUp={(e): void => {
|
||||
if (e.key === 'Enter' || e.code === 'Enter') {
|
||||
send('NEXT');
|
||||
}
|
||||
}}
|
||||
bordered={false}
|
||||
value={currentVal as never}
|
||||
style={{ flex: 1 }}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
{queries.length || staging.length || currentVal ? (
|
||||
<Button
|
||||
onClick={handleClearAll}
|
||||
icon={<CloseCircleFilled />}
|
||||
type="text"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SearchContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default LabelSelect;
|
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Tag } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface SearchContainerProps {
|
||||
isDarkMode: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const SearchContainer = styled.div<SearchContainerProps>`
|
||||
width: 70%;
|
||||
border-radisu: 4px;
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.2rem;
|
||||
border: 1px solid #ccc5;
|
||||
${({ disabled }): string => (disabled ? `cursor: not-allowed;` : '')}
|
||||
`;
|
||||
|
||||
export const QueryChipContainer = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
&:hover {
|
||||
& > * {
|
||||
background: ${grey.primary}44;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryChipItem = styled(Tag)`
|
||||
margin-right: 0.1rem;
|
||||
`;
|
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ILabelRecord {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
const hiddenLabels = ['severity', 'description'];
|
||||
|
||||
export const createQuery = (
|
||||
selectedItems: Array<string | string[]> = [],
|
||||
): ILabelRecord | null => {
|
||||
if (selectedItems.length === 2) {
|
||||
return {
|
||||
key: selectedItems[0] as string,
|
||||
value: selectedItems[1] as string,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const flattenLabels = (labels: Labels): ILabelRecord[] => {
|
||||
const recs: ILabelRecord[] = [];
|
||||
|
||||
Object.keys(labels).forEach((key) => {
|
||||
if (!hiddenLabels.includes(key)) {
|
||||
recs.push({
|
||||
key,
|
||||
value: labels[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return recs;
|
||||
};
|
||||
|
||||
export const prepareLabels = (
|
||||
recs: ILabelRecord[],
|
||||
alertLabels: Labels | undefined,
|
||||
): Labels => {
|
||||
const labels: Labels = {};
|
||||
|
||||
recs.forEach((rec) => {
|
||||
if (!hiddenLabels.includes(rec.key)) {
|
||||
labels[rec.key] = rec.value;
|
||||
}
|
||||
});
|
||||
if (alertLabels) {
|
||||
Object.keys(alertLabels).forEach((key) => {
|
||||
if (hiddenLabels.includes(key)) {
|
||||
labels[key] = alertLabels[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
90
frontend/src/container/FormAlertRules/styles.ts
Normal file
90
frontend/src/container/FormAlertRules/styles.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Button, Card, Form, Input, InputNumber, Select } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MainFormContainer = styled(Form)`
|
||||
max-width: 900px;
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionButton = styled(Button)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
export const QueryButton = styled(Button)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryContainer = styled(Card)`
|
||||
&&& {
|
||||
margin-top: 1rem;
|
||||
min-height: 23.5%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const StepHeading = styled.p`
|
||||
margin-top: 1rem;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const InlineSelect = styled(Select)`
|
||||
display: inline-block;
|
||||
width: 10% !important;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
`;
|
||||
|
||||
export const SeveritySelect = styled(Select)`
|
||||
width: 15% !important;
|
||||
`;
|
||||
|
||||
export const InputSmall = styled(Input)`
|
||||
width: 40% !important;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.div`
|
||||
padding: 2em;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #141414;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #303030;
|
||||
`;
|
||||
|
||||
export const ThresholdInput = styled(InputNumber)`
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > .ant-input-number-group-addon {
|
||||
width: 130px;
|
||||
}
|
||||
& > .ant-input-number {
|
||||
width: 50%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextareaMedium = styled(TextArea)`
|
||||
width: 70%;
|
||||
`;
|
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// see https://github.com/tannerlinsley/react-query/issues/293
|
||||
// see https://usehooks.com/useDebounce/
|
||||
export default function useDebounce(value, delay) {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay] // Only re-call effect if value or delay changes
|
||||
);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
136
frontend/src/container/FormAlertRules/utils.ts
Normal file
136
frontend/src/container/FormAlertRules/utils.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
IBuilderQueries,
|
||||
IFormulaQueries,
|
||||
IFormulaQuery,
|
||||
IMetricQueries,
|
||||
IMetricQuery,
|
||||
IPromQueries,
|
||||
IPromQuery,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
IMetricsBuilderQuery,
|
||||
Query as IStagedQuery,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const toFormulaQueries = (b: IBuilderQueries): IFormulaQueries => {
|
||||
const f: IFormulaQueries = {};
|
||||
if (!b) return f;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key === 'F1') {
|
||||
f[key] = b[key] as IFormulaQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
||||
const m: IMetricQueries = {};
|
||||
if (!b) return m;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key !== 'F1') {
|
||||
m[key] = b[key] as IMetricQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
export const toIMetricsBuilderQuery = (
|
||||
q: IMetricQuery,
|
||||
): IMetricsBuilderQuery => {
|
||||
return {
|
||||
name: q.name,
|
||||
metricName: q.metricName,
|
||||
tagFilters: q.tagFilters,
|
||||
groupBy: q.groupBy,
|
||||
aggregateOperator: q.aggregateOperator,
|
||||
disabled: q.disabled,
|
||||
legend: q.legend,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareBuilderQueries = (
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
): IBuilderQueries => {
|
||||
if (!m) return {};
|
||||
const b: IBuilderQueries = {
|
||||
...m,
|
||||
};
|
||||
|
||||
Object.keys(f).forEach((key) => {
|
||||
b[key] = {
|
||||
...f[key],
|
||||
aggregateOperator: undefined,
|
||||
metricName: '',
|
||||
};
|
||||
});
|
||||
return b;
|
||||
};
|
||||
|
||||
export const prepareStagedQuery = (
|
||||
t: EQueryType,
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
p: IPromQueries,
|
||||
): IStagedQuery => {
|
||||
const qbList: IMetricQuery[] = [];
|
||||
const formulaList: IFormulaQuery[] = [];
|
||||
const promList: IPromQuery[] = [];
|
||||
|
||||
// convert map[string]IMetricQuery to IMetricQuery[]
|
||||
if (m) {
|
||||
Object.keys(m).forEach((key) => {
|
||||
qbList.push(m[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IFormulaQuery to IFormulaQuery[]
|
||||
if (f) {
|
||||
Object.keys(f).forEach((key) => {
|
||||
formulaList.push(f[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IPromQuery to IPromQuery[]
|
||||
if (p) {
|
||||
Object.keys(p).forEach((key) => {
|
||||
promList.push({ ...p[key], name: key });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
queryType: t,
|
||||
promQL: promList,
|
||||
metricsBuilder: {
|
||||
formulas: formulaList,
|
||||
queryBuilder: qbList,
|
||||
},
|
||||
clickHouse: [],
|
||||
};
|
||||
};
|
||||
|
||||
// toChartInterval converts eval window to chart selection time interval
|
||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
switch (evalWindow) {
|
||||
case '5m0s':
|
||||
return '5min';
|
||||
case '10m0s':
|
||||
return '10min';
|
||||
case '15m0s':
|
||||
return '15min';
|
||||
case '30m0s':
|
||||
return '30min';
|
||||
case '60m0s':
|
||||
return '30min';
|
||||
case '4h0m0s':
|
||||
return '4hr';
|
||||
case '24h0m0s':
|
||||
return '1day';
|
||||
default:
|
||||
return '5min';
|
||||
}
|
||||
};
|
@ -10,7 +10,7 @@ function SpanNameComponent({
|
||||
<Container title={`${name} ${serviceName}`}>
|
||||
<SpanWrapper>
|
||||
<Span ellipsis>{name}</Span>
|
||||
<Service>{serviceName}</Service>
|
||||
<Service ellipsis>{serviceName}</Service>
|
||||
</SpanWrapper>
|
||||
</Container>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export const Span = styled(Typography.Paragraph)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const Service = styled(Typography)`
|
||||
export const Service = styled(Typography.Paragraph)`
|
||||
&&& {
|
||||
color: #acacac;
|
||||
font-size: 0.75rem;
|
||||
|
@ -41,8 +41,9 @@ export const CardContainer = styled.li<{ isMissing?: boolean }>`
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
z-index: 2;
|
||||
${({ isMissing }): string =>
|
||||
isMissing ? `border: 1px dashed ${volcano[6]};` : ''}
|
||||
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ChartData } from 'chart.js';
|
||||
import Graph, { GraphOnClickHandler } from 'components/Graph';
|
||||
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import ValueGraph from 'components/ValueGraph';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
@ -18,6 +18,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}: GridGraphComponentProps): JSX.Element | null {
|
||||
const location = history.location.pathname;
|
||||
|
||||
@ -36,6 +37,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -82,6 +84,7 @@ export interface GridGraphComponentProps {
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
}
|
||||
|
||||
GridGraphComponent.defaultProps = {
|
||||
@ -90,6 +93,7 @@ GridGraphComponent.defaultProps = {
|
||||
isStacked: undefined,
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
|
||||
export default GridGraphComponent;
|
||||
|
@ -64,9 +64,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Alert Name',
|
||||
dataIndex: 'name',
|
||||
dataIndex: 'alert',
|
||||
key: 'name',
|
||||
sorter: (a, b): number => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
||||
render: (value, record): JSX.Element => (
|
||||
<Typography.Link onClick={(): void => onEditHandler(record.id.toString())}>
|
||||
{value}
|
||||
</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
@ -83,7 +88,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
title: 'Labels',
|
||||
dataIndex: 'labels',
|
||||
key: 'tags',
|
||||
align: 'center',
|
||||
@ -100,7 +105,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return (
|
||||
<Tag key={e} color="magenta">
|
||||
{e}
|
||||
{e}: {value[e]}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
|
@ -25,7 +25,7 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_db_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[1m])) by (db_system)`,
|
||||
query: `sum(rate(signoz_db_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (db_system)`,
|
||||
legend: '{{db_system}}',
|
||||
},
|
||||
])}
|
||||
|
@ -14,7 +14,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
const { resourceAttributePromQLQuery } = useSelector<AppState, MetricReducer>(
|
||||
(state) => state.metrics,
|
||||
);
|
||||
const legend = '{{http_url}}';
|
||||
const legend = '{{address}}';
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -28,7 +28,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."${resourceAttributePromQLQuery}}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[1m])) by (http_url)) < 1000 OR vector(0)`,
|
||||
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR vector(0)) by (address))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address)) < 1000 OR vector(0)`,
|
||||
legend: 'External Call Error Percentage',
|
||||
},
|
||||
])}
|
||||
@ -68,7 +68,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url)`,
|
||||
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address)`,
|
||||
legend,
|
||||
},
|
||||
])}
|
||||
@ -87,7 +87,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `(sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url))/(sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (http_url))`,
|
||||
query: `(sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address))/(sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[5m])) by (address))`,
|
||||
legend,
|
||||
},
|
||||
])}
|
||||
|
@ -193,7 +193,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[2m]))`,
|
||||
query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[5m]))`,
|
||||
legend: 'Requests',
|
||||
},
|
||||
])}
|
||||
@ -227,7 +227,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
||||
}}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[1m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."${resourceAttributePromQLQuery}}[1m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[1m]))) < 1000 OR vector(0)`,
|
||||
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[5m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."${resourceAttributePromQLQuery}}[5m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"${resourceAttributePromQLQuery}}[5m]))) < 1000 OR vector(0)`,
|
||||
legend: 'Error Percentage',
|
||||
},
|
||||
])}
|
||||
|
@ -56,7 +56,7 @@ function Metrics(): JSX.Element {
|
||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Error Rate (in %)',
|
||||
title: 'Error Rate (% of requests)',
|
||||
dataIndex: 'errorRate',
|
||||
key: 'errorRate',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
|
||||
|
@ -29,7 +29,7 @@ function PromQLQueryContainer({
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (query !== undefined) currentIndexQuery.query = query;
|
||||
if (legend !== undefined) currentIndexQuery.legend = legend;
|
||||
|
||||
@ -37,7 +37,7 @@ function PromQLQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { IPromQLQueryHandleChange } from './types';
|
||||
|
||||
interface IPromQLQueryBuilderProps {
|
||||
queryData: IPromQLQuery;
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IPromQLQuery } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IPromQLQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
query?: IPromQLQuery['query'];
|
||||
legend?: IPromQLQuery['legend'];
|
||||
toggleDisable?: IPromQLQuery['disabled'];
|
||||
|
@ -9,7 +9,7 @@ const { TextArea } = Input;
|
||||
|
||||
interface IMetricsBuilderFormulaProps {
|
||||
formulaData: IMetricsBuilderFormula;
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
|
||||
}
|
||||
function MetricsBuilderFormula({
|
||||
|
@ -50,7 +50,7 @@ function QueryBuilderQueryContainer({
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries =
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (aggregateFunction) {
|
||||
currentIndexQuery.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
@ -78,7 +78,7 @@ function QueryBuilderQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
@ -92,7 +92,7 @@ function QueryBuilderQueryContainer({
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
|
||||
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
|
||||
];
|
||||
const currentIndexFormula = allFormulas[formulaIndex];
|
||||
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||
|
||||
if (expression) {
|
||||
currentIndexFormula.expression = expression;
|
||||
@ -103,7 +103,7 @@ function QueryBuilderQueryContainer({
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
allFormulas.splice(formulaIndex, 1);
|
||||
allFormulas.splice(formulaIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ import { IQueryBuilderQueryHandleChange } from './types';
|
||||
const { Option } = Select;
|
||||
|
||||
interface IMetricsBuilderProps {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
selectedGraph: GRAPH_TYPES;
|
||||
queryData: IMetricsBuilderQuery;
|
||||
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IQueryBuilderQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
|
||||
metricName?: IMetricsBuilderQuery['metricName'];
|
||||
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
|
||||
@ -16,7 +16,7 @@ export interface IQueryBuilderQueryHandleChange {
|
||||
}
|
||||
|
||||
export interface IQueryBuilderFormulaHandleChange {
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
expression?: IMetricsBuilderFormula['expression'];
|
||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||
toggleDelete?: boolean;
|
||||
|
@ -1,20 +1,24 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
type FiveMin = '5min';
|
||||
type TenMin = '10min';
|
||||
type FifteenMin = '15min';
|
||||
type ThirtyMin = '30min';
|
||||
type OneMin = '1min';
|
||||
type SixHour = '6hr';
|
||||
type OneHour = '1hr';
|
||||
type FourHour = '4hr';
|
||||
type OneDay = '1day';
|
||||
type OneWeek = '1week';
|
||||
type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
| FourHour
|
||||
| SixHour
|
||||
| OneHour
|
||||
| Custom
|
||||
|
@ -19,6 +19,9 @@ const routesToSkip = [
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.ALERTS_NEW,
|
||||
ROUTES.EDIT_ALERTS,
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
];
|
||||
|
||||
function TopNav(): JSX.Element | null {
|
||||
|
@ -9,7 +9,9 @@ export const AllTraceFilterEnum: TraceFilterEnum[] = [
|
||||
'serviceName',
|
||||
'operation',
|
||||
'component',
|
||||
'httpCode',
|
||||
'rpcMethod',
|
||||
'responseStatusCode',
|
||||
// 'httpCode',
|
||||
'httpHost',
|
||||
'httpMethod',
|
||||
'httpRoute',
|
||||
|
@ -38,6 +38,14 @@ export const groupBy: Dropdown[] = [
|
||||
displayValue: 'HTTP status code',
|
||||
key: 'httpCode',
|
||||
},
|
||||
{
|
||||
displayValue: 'RPC Method',
|
||||
key: 'rpcMethod',
|
||||
},
|
||||
{
|
||||
displayValue: 'Status Code',
|
||||
key: 'responseStatusCode',
|
||||
},
|
||||
{
|
||||
displayValue: 'Database name',
|
||||
key: 'dbName',
|
||||
|
@ -28,11 +28,10 @@ function MissingSpansMessage(): JSX.Element {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '1rem 0',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: volcano[6], fontSize: '1.4rem', marginRight: '0.3rem' }}
|
||||
/>{' '}
|
||||
<InfoCircleOutlined style={{ color: volcano[6], marginRight: '0.3rem' }} />{' '}
|
||||
This trace has missing spans
|
||||
</div>
|
||||
</Popover>
|
||||
|
@ -1,6 +1,6 @@
|
||||
const createQueryParams = (params: { [x: string]: string }): string =>
|
||||
const createQueryParams = (params: { [x: string]: string | number }): string =>
|
||||
Object.keys(params)
|
||||
.map((k) => `${k}=${encodeURI(params[k])}`)
|
||||
.map((k) => `${k}=${encodeURI(String(params[k]))}`)
|
||||
.join('&');
|
||||
|
||||
export default createQueryParams;
|
||||
|
@ -13,6 +13,9 @@ const GetMinMax = (
|
||||
if (interval === '1min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '10min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '15min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
@ -33,8 +36,9 @@ const GetMinMax = (
|
||||
// one week = one day * 7
|
||||
const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '6hr') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime();
|
||||
} else if (['4hr', '6hr'].includes(interval)) {
|
||||
const h = parseInt(interval.replace('hr', ''), 10);
|
||||
const minTimeAgo = getMinAgo({ minutes: h * 60 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === 'custom') {
|
||||
maxTime = (dateTimeRange || [])[1] || 0;
|
||||
|
@ -1,109 +1,9 @@
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import createAlertsApi from 'api/alerts/create';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps as CreateAlertPayloadProps } from 'types/api/alerts/create';
|
||||
import CreateAlertRule from 'container/CreateAlertRule';
|
||||
import React from 'react';
|
||||
import { alertDefaults } from 'types/api/alerts/create';
|
||||
|
||||
import { ButtonContainer, Title } from './styles';
|
||||
|
||||
function CreateAlert(): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(
|
||||
`\n alert: High RPS\n expr: sum(rate(signoz_latency_count{span_kind="SPAN_KIND_SERVER"}[2m])) by (service_name) > 100\n for: 0m\n labels:\n severity: warning\n annotations:\n summary: High RPS of Applications\n description: "RPS is > 100\n\t\t\t VALUE = {{ $value }}\n\t\t\t LABELS = {{ $labels }}"\n `,
|
||||
);
|
||||
|
||||
const [newAlertState, setNewAlertState] = useState<
|
||||
State<CreateAlertPayloadProps>
|
||||
>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
payload: undefined,
|
||||
success: false,
|
||||
});
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
|
||||
const defaultError =
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io';
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
if (value.length === 0) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
description: `Oops! We didn't catch that. Please make sure the alert settings are not empty or try again`,
|
||||
message: 'Error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await createAlertsApi({
|
||||
query: value,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was saved correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 3000);
|
||||
} else {
|
||||
notifications.error({
|
||||
description: response.error || defaultError,
|
||||
message: 'Error',
|
||||
});
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || defaultError,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: defaultError,
|
||||
});
|
||||
}
|
||||
}, [notifications, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Title>Create New Alert</Title>
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={newAlertState.loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
function CreateAlertPage(): JSX.Element {
|
||||
return <CreateAlertRule initialValue={alertDefaults} />;
|
||||
}
|
||||
|
||||
export default CreateAlert;
|
||||
export default CreateAlertPage;
|
||||
|
@ -47,7 +47,12 @@ function EditRules(): JSX.Element {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
return <EditRulesContainer ruleId={ruleId} initialData={data.payload.data} />;
|
||||
return (
|
||||
<EditRulesContainer
|
||||
ruleId={parseInt(ruleId, 10)}
|
||||
initialValue={data.payload.data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
@ -4,107 +4,87 @@ import getById from 'api/errors/getById';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ErrorDetailsContainer from 'container/ErrorDetails';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Redirect, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps } from 'types/api/errors/getById';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { urlKey } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ErrorDetails(): JSX.Element {
|
||||
const { t } = useTranslation(['common']);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||
|
||||
const groupId = params.get(urlKey.groupId);
|
||||
const errorId = params.get(urlKey.errorId);
|
||||
const timestamp = params.get(urlKey.timestamp);
|
||||
|
||||
const errorId = params.get('errorId');
|
||||
const errorType = params.get('errorType');
|
||||
const serviceName = params.get('serviceName');
|
||||
const defaultError = t('something_went_wrong');
|
||||
|
||||
const { data, status } = useQuery(
|
||||
[
|
||||
'errorByType',
|
||||
errorType,
|
||||
'serviceName',
|
||||
serviceName,
|
||||
maxTime,
|
||||
minTime,
|
||||
errorId,
|
||||
],
|
||||
{
|
||||
queryFn: () =>
|
||||
getByErrorType({
|
||||
end: maxTime,
|
||||
errorType: errorType || '',
|
||||
serviceName: serviceName || '',
|
||||
start: minTime,
|
||||
}),
|
||||
enabled: errorId === null && errorType !== null && serviceName !== null,
|
||||
cacheTime: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const { status: ErrorIdStatus, data: errorIdPayload } = useQuery(
|
||||
[
|
||||
'errorByType',
|
||||
errorType,
|
||||
'serviceName',
|
||||
serviceName,
|
||||
maxTime,
|
||||
minTime,
|
||||
'errorId',
|
||||
errorId,
|
||||
],
|
||||
const { data: IdData, status: IdStatus } = useQuery(
|
||||
[errorId, timestamp, groupId],
|
||||
{
|
||||
queryFn: () =>
|
||||
getById({
|
||||
end: maxTime,
|
||||
errorId: errorId || data?.payload?.errorId || '',
|
||||
start: minTime,
|
||||
errorID: errorId || '',
|
||||
groupID: groupId || '',
|
||||
timestamp: timestamp || '',
|
||||
}),
|
||||
enabled:
|
||||
(errorId !== null || status === 'success') &&
|
||||
errorType !== null &&
|
||||
serviceName !== null,
|
||||
cacheTime: 5000,
|
||||
errorId !== null &&
|
||||
groupId !== null &&
|
||||
timestamp !== null &&
|
||||
errorId.length !== 0 &&
|
||||
groupId.length !== 0 &&
|
||||
timestamp.length !== 0,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, status } = useQuery([maxTime, minTime, groupId], {
|
||||
queryFn: () =>
|
||||
getByErrorType({
|
||||
groupID: groupId || '',
|
||||
timestamp: timestamp || '',
|
||||
}),
|
||||
enabled: !!groupId && IdStatus !== 'success',
|
||||
});
|
||||
|
||||
// if errorType and serviceName is null redirecting to the ALL_ERROR page not now
|
||||
if (errorType === null || serviceName === null) {
|
||||
if (groupId === null || timestamp === null) {
|
||||
return <Redirect to={ROUTES.ALL_ERROR} />;
|
||||
}
|
||||
|
||||
// when the api is in loading state
|
||||
if (status === 'loading' || ErrorIdStatus === 'loading') {
|
||||
if (status === 'loading' || IdStatus === 'loading') {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
// if any error occurred while loading
|
||||
if (status === 'error' || ErrorIdStatus === 'error') {
|
||||
return (
|
||||
<Typography>
|
||||
{data?.error || errorIdPayload?.error || defaultError}
|
||||
</Typography>
|
||||
);
|
||||
if (status === 'error' || IdStatus === 'error') {
|
||||
return <Typography>{data?.error || defaultError}</Typography>;
|
||||
}
|
||||
|
||||
const idPayload = data?.payload || IdData?.payload;
|
||||
|
||||
// if API is successfully but there is an error
|
||||
if (
|
||||
(status === 'success' && data?.statusCode >= 400) ||
|
||||
(ErrorIdStatus === 'success' && errorIdPayload.statusCode >= 400)
|
||||
(IdStatus === 'success' && IdData.statusCode >= 400) ||
|
||||
idPayload === null ||
|
||||
idPayload === undefined
|
||||
) {
|
||||
return <Typography>{data?.error || defaultError}</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorDetailsContainer idPayload={errorIdPayload?.payload as PayloadProps} />
|
||||
);
|
||||
return <ErrorDetailsContainer idPayload={idPayload} />;
|
||||
}
|
||||
|
||||
export interface ErrorDetailsParams {
|
||||
|
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const urlKey = {
|
||||
serviceName: 'serviceName',
|
||||
exceptionType: 'exceptionType',
|
||||
groupId: 'groupId',
|
||||
lastSeen: 'lastSeen',
|
||||
errorId: 'errorId',
|
||||
timestamp: 'timestamp',
|
||||
};
|
@ -262,12 +262,13 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
setState(updateValue, setConfirmPassword);
|
||||
}}
|
||||
required
|
||||
id="UpdatePassword"
|
||||
id="confirmPassword"
|
||||
/>
|
||||
|
||||
{confirmPasswordError && (
|
||||
<Typography.Paragraph
|
||||
italic
|
||||
id="password-confirm-error"
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
@ -340,6 +341,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
||||
!organizationName ||
|
||||
!password ||
|
||||
!confirmPassword ||
|
||||
!firstName ||
|
||||
confirmPasswordError ||
|
||||
isPasswordPolicyError
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ const initialValue: TraceReducer = {
|
||||
['httpMethod', INITIAL_FILTER_VALUE],
|
||||
['httpUrl', INITIAL_FILTER_VALUE],
|
||||
['operation', INITIAL_FILTER_VALUE],
|
||||
['rpcMethod', INITIAL_FILTER_VALUE],
|
||||
['responseStatusCode', INITIAL_FILTER_VALUE],
|
||||
['serviceName', INITIAL_FILTER_VALUE],
|
||||
['status', INITIAL_FILTER_VALUE],
|
||||
]),
|
||||
|
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
IMetricsBuilderFormula,
|
||||
IMetricsBuilderQuery,
|
||||
IPromQLQuery,
|
||||
IQueryBuilderTagFilters,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export interface ICompositeMetricQuery {
|
||||
builderQueries: IBuilderQueries;
|
||||
promQueries: IPromQueries;
|
||||
queryType: EQueryType;
|
||||
}
|
||||
|
||||
export interface IPromQueries {
|
||||
[key: string]: IPromQuery;
|
||||
}
|
||||
|
||||
export interface IPromQuery extends IPromQLQuery {
|
||||
stats?: '';
|
||||
}
|
||||
|
||||
export interface IBuilderQueries {
|
||||
[key: string]: IBuilderQuery;
|
||||
}
|
||||
|
||||
// IBuilderQuery combines IMetricQuery and IFormulaQuery
|
||||
// for api calls
|
||||
export interface IBuilderQuery
|
||||
extends Omit<
|
||||
IMetricQuery,
|
||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||
>,
|
||||
Omit<IFormulaQuery, 'expression'> {
|
||||
aggregateOperator: EAggregateOperator | undefined;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
legend?: string;
|
||||
metricName: string | null;
|
||||
groupBy?: string[];
|
||||
expression?: string;
|
||||
tagFilters?: IQueryBuilderTagFilters;
|
||||
toggleDisable?: boolean;
|
||||
toggleDelete?: boolean;
|
||||
}
|
||||
|
||||
export interface IFormulaQueries {
|
||||
[key: string]: IFormulaQuery;
|
||||
}
|
||||
|
||||
export interface IFormulaQuery extends IMetricsBuilderFormula {
|
||||
formulaOnly: boolean;
|
||||
queryName: string;
|
||||
}
|
||||
|
||||
export interface IMetricQueries {
|
||||
[key: string]: IMetricQuery;
|
||||
}
|
||||
|
||||
export interface IMetricQuery extends IMetricsBuilderQuery {
|
||||
formulaOnly: boolean;
|
||||
expression?: string;
|
||||
queryName: string;
|
||||
}
|
@ -1,8 +1,48 @@
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def';
|
||||
|
||||
export interface Props {
|
||||
query: string;
|
||||
data: AlertDef;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const alertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeMetricQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
name: 'A',
|
||||
formulaOnly: false,
|
||||
metricName: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
aggregateOperator: 1,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
toggleDisable: false,
|
||||
toggleDelete: false,
|
||||
},
|
||||
},
|
||||
promQueries: {},
|
||||
queryType: 1,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new alert',
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
32
frontend/src/types/api/alerts/def.ts
Normal file
32
frontend/src/types/api/alerts/def.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
// default match type for threshold
|
||||
export const defaultMatchType = '1';
|
||||
|
||||
// default eval window
|
||||
export const defaultEvalWindow = '5m0s';
|
||||
|
||||
// default compare op: above
|
||||
export const defaultCompareOp = '1';
|
||||
|
||||
export interface AlertDef {
|
||||
id?: number;
|
||||
alert?: string;
|
||||
ruleType?: string;
|
||||
condition: RuleCondition;
|
||||
labels?: Labels;
|
||||
annotations?: Labels;
|
||||
evalWindow?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
compositeMetricQuery: ICompositeMetricQuery;
|
||||
op?: string | undefined;
|
||||
target?: number | undefined;
|
||||
matchType?: string | undefined;
|
||||
}
|
||||
|
||||
export interface Labels {
|
||||
[key: string]: string;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Alerts } from './getAll';
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
id: AlertDef['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
data: string;
|
||||
data: AlertDef;
|
||||
};
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { PayloadProps as DeletePayloadProps } from './delete';
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export type PayloadProps = DeletePayloadProps;
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
data: DeletePayloadProps['data'];
|
||||
}
|
17
frontend/src/types/api/alerts/queryType.ts
Normal file
17
frontend/src/types/api/alerts/queryType.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type QueryType = 1 | 2 | 3;
|
||||
|
||||
export const QUERY_BUILDER: QueryType = 1;
|
||||
export const PROMQL: QueryType = 3;
|
||||
|
||||
export const resolveQueryCategoryName = (s: number): string => {
|
||||
switch (s) {
|
||||
case 1:
|
||||
return 'Query Builder';
|
||||
case 2:
|
||||
return 'Clickhouse Query';
|
||||
case 3:
|
||||
return 'PromQL';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
11
frontend/src/types/api/alerts/save.ts
Normal file
11
frontend/src/types/api/alerts/save.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export type PayloadProps = {
|
||||
status: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id?: number;
|
||||
data: AlertDef;
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
export type Order = 'ascending' | 'descending';
|
||||
export type OrderBy =
|
||||
| 'serviceName'
|
||||
| 'exceptionCount'
|
||||
| 'lastSeen'
|
||||
| 'firstSeen'
|
||||
| 'exceptionType';
|
||||
|
||||
export interface Props {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['maxTime'];
|
||||
order?: Order;
|
||||
orderParam?: OrderBy;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface Exception {
|
||||
@ -12,6 +24,7 @@ export interface Exception {
|
||||
lastSeen: string;
|
||||
firstSeen: string;
|
||||
serviceName: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = Exception[];
|
||||
|
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { PayloadProps as Prop } from './getByErrorTypeAndService';
|
||||
|
||||
export interface Props {
|
||||
groupID: string;
|
||||
errorID: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = Prop;
|
@ -1,10 +1,6 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
export interface Props {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['maxTime'];
|
||||
serviceName: string;
|
||||
errorType: string;
|
||||
timestamp: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
@ -16,7 +12,6 @@ export interface PayloadProps {
|
||||
timestamp: string;
|
||||
spanID: string;
|
||||
traceID: string;
|
||||
serviceName: Props['serviceName'];
|
||||
newerErrorId: string;
|
||||
olderErrorId: string;
|
||||
serviceName: string;
|
||||
groupID: string;
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
import { PayloadProps as Payload } from './getByErrorTypeAndService';
|
||||
|
||||
export type PayloadProps = Payload;
|
||||
|
||||
export type Props = {
|
||||
start: GlobalTime['minTime'];
|
||||
end: GlobalTime['minTime'];
|
||||
errorId: string;
|
||||
};
|
||||
|
||||
export type PayloadProps = number;
|
13
frontend/src/types/api/errors/getNextPrevId.ts
Normal file
13
frontend/src/types/api/errors/getNextPrevId.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export type Props = {
|
||||
errorID: string;
|
||||
timestamp: string;
|
||||
groupID: string;
|
||||
};
|
||||
|
||||
export type PayloadProps = {
|
||||
prevErrorID: string;
|
||||
nextErrorID: string;
|
||||
groupID: string;
|
||||
nextTimestamp: string;
|
||||
prevTimestamp: string;
|
||||
};
|
@ -18,10 +18,10 @@ export type Span = [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string | string[],
|
||||
string | string[],
|
||||
string | string[],
|
||||
Record<string, unknown>[],
|
||||
string[],
|
||||
string[],
|
||||
string[],
|
||||
string[],
|
||||
boolean,
|
||||
];
|
||||
|
||||
|
@ -69,7 +69,9 @@ export type TraceFilterEnum =
|
||||
| 'httpUrl'
|
||||
| 'operation'
|
||||
| 'serviceName'
|
||||
| 'status';
|
||||
| 'status'
|
||||
| 'responseStatusCode'
|
||||
| 'rpcMethod';
|
||||
|
||||
export const AllPanelHeading: {
|
||||
key: TraceFilterEnum;
|
||||
@ -107,6 +109,14 @@ export const AllPanelHeading: {
|
||||
key: 'operation',
|
||||
displayValue: 'Operation',
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
displayValue: 'Status Code',
|
||||
},
|
||||
{
|
||||
key: 'rpcMethod',
|
||||
displayValue: 'RPC Method',
|
||||
},
|
||||
{
|
||||
key: 'serviceName',
|
||||
displayValue: 'Service Name',
|
||||
|
@ -0,0 +1,211 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`utils/spanToTree should return a single tree on valid trace data 1`] = `
|
||||
Object {
|
||||
"missingSpanTree": Array [],
|
||||
"spanTree": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"event": Array [
|
||||
Object {
|
||||
"attributeMap": Object {
|
||||
"event": "HTTP request received S3",
|
||||
"level": "info",
|
||||
"method": "GET",
|
||||
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
|
||||
},
|
||||
"timeUnixNano": 1657275433246142000,
|
||||
},
|
||||
],
|
||||
"hasError": false,
|
||||
"id": "span_3",
|
||||
"isProcessed": true,
|
||||
"name": "HTTP GET SPAN 3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"RefType": "CHILD_OF",
|
||||
"SpanId": "span_2",
|
||||
"TraceId": "0000000000000000span_1",
|
||||
},
|
||||
],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"startTime": 1657275433246,
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span3",
|
||||
"value": "span_3",
|
||||
},
|
||||
],
|
||||
"time": 683273000,
|
||||
"value": 683273000,
|
||||
},
|
||||
],
|
||||
"event": Array [
|
||||
Object {
|
||||
"attributeMap": Object {
|
||||
"event": "HTTP request received S2",
|
||||
"level": "info",
|
||||
"method": "GET",
|
||||
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
|
||||
},
|
||||
"timeUnixNano": 1657275433246142000,
|
||||
},
|
||||
],
|
||||
"hasError": false,
|
||||
"id": "span_2",
|
||||
"isProcessed": true,
|
||||
"name": "HTTP GET SPAN 2",
|
||||
"references": Array [
|
||||
Object {
|
||||
"RefType": "CHILD_OF",
|
||||
"SpanId": "span_1",
|
||||
"TraceId": "0000000000000000span_1",
|
||||
},
|
||||
],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"startTime": 1657275433246,
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span2",
|
||||
"value": "span_2",
|
||||
},
|
||||
],
|
||||
"time": 683273000,
|
||||
"value": 683273000,
|
||||
},
|
||||
],
|
||||
"event": Array [
|
||||
Object {
|
||||
"attributeMap": Object {
|
||||
"event": "HTTP request received S1",
|
||||
"level": "info",
|
||||
"method": "GET",
|
||||
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
|
||||
},
|
||||
"timeUnixNano": 1657275433246142000,
|
||||
},
|
||||
],
|
||||
"hasError": false,
|
||||
"id": "span_1",
|
||||
"name": "HTTP GET SPAN 1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"RefType": "CHILD_OF",
|
||||
"SpanId": "",
|
||||
"TraceId": "0000000000000000span_1",
|
||||
},
|
||||
],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"startTime": 1657275433246,
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span1",
|
||||
"value": "span_1",
|
||||
},
|
||||
],
|
||||
"time": 683273000,
|
||||
"value": 683273000,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`utils/spanToTree should return a single tree on valid trace data 2`] = `
|
||||
Object {
|
||||
"missingSpanTree": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"event": Array [
|
||||
Object {
|
||||
"attributeMap": Object {
|
||||
"event": "HTTP request received S3",
|
||||
"level": "info",
|
||||
"method": "GET",
|
||||
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
|
||||
},
|
||||
"timeUnixNano": 1657275433246142000,
|
||||
},
|
||||
],
|
||||
"hasError": false,
|
||||
"id": "span_3",
|
||||
"isProcessed": true,
|
||||
"name": "HTTP GET SPAN 3",
|
||||
"references": Array [
|
||||
Object {
|
||||
"RefType": "CHILD_OF",
|
||||
"SpanId": "span_2",
|
||||
"TraceId": "0000000000000000span_1",
|
||||
},
|
||||
],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"startTime": 1657275433246,
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span3",
|
||||
"value": "span_3",
|
||||
},
|
||||
],
|
||||
"time": 683273000,
|
||||
"value": 683273000,
|
||||
},
|
||||
],
|
||||
"id": "span_2",
|
||||
"isMissing": true,
|
||||
"name": "Missing Span (span_2)",
|
||||
"serviceColour": "",
|
||||
"serviceName": "",
|
||||
"startTime": null,
|
||||
"tags": Array [],
|
||||
"time": null,
|
||||
"value": null,
|
||||
},
|
||||
],
|
||||
"spanTree": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"event": Array [
|
||||
Object {
|
||||
"attributeMap": Object {
|
||||
"event": "HTTP request received S1",
|
||||
"level": "info",
|
||||
"method": "GET",
|
||||
"url": "/dispatch?customer=392&nonse=0.015296363321630757",
|
||||
},
|
||||
"timeUnixNano": 1657275433246142000,
|
||||
},
|
||||
],
|
||||
"hasError": false,
|
||||
"id": "span_1",
|
||||
"name": "HTTP GET SPAN 1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"RefType": "CHILD_OF",
|
||||
"SpanId": "",
|
||||
"TraceId": "0000000000000000span_1",
|
||||
},
|
||||
],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"startTime": 1657275433246,
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span1",
|
||||
"value": "span_1",
|
||||
},
|
||||
],
|
||||
"time": 683273000,
|
||||
"value": 683273000,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
21
frontend/src/utils/__tests__/spanToTree.test.ts
Normal file
21
frontend/src/utils/__tests__/spanToTree.test.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { TraceData } from '../fixtures/TraceData';
|
||||
import { spanToTreeUtil } from '../spanToTree';
|
||||
|
||||
describe('utils/spanToTree', () => {
|
||||
test('should return a single tree on valid trace data', () => {
|
||||
const spanTree = spanToTreeUtil(TraceData);
|
||||
expect(spanTree.spanTree.length).toBe(1);
|
||||
expect(spanTree.missingSpanTree.length).toBe(0);
|
||||
expect(spanTree).toMatchSnapshot();
|
||||
});
|
||||
test('should return a single tree on valid trace data', () => {
|
||||
const MissingTraceData = [...TraceData];
|
||||
MissingTraceData.splice(1, 1);
|
||||
|
||||
const spanTree = spanToTreeUtil(MissingTraceData);
|
||||
|
||||
expect(spanTree.spanTree.length).toBe(1);
|
||||
expect(spanTree.missingSpanTree.length).toBe(1);
|
||||
expect(spanTree).toMatchSnapshot();
|
||||
});
|
||||
});
|
52
frontend/src/utils/fixtures/TraceData.ts
Normal file
52
frontend/src/utils/fixtures/TraceData.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Span } from 'types/api/trace/getTraceItem';
|
||||
|
||||
export const TraceData: Span[] = [
|
||||
[
|
||||
1657275433246,
|
||||
'span_1',
|
||||
'0000000000000000span_1',
|
||||
'frontend',
|
||||
'HTTP GET SPAN 1',
|
||||
'2',
|
||||
'683273000',
|
||||
['host.name.span1'],
|
||||
['span_1'],
|
||||
['{TraceId=0000000000000000span_1, SpanId=, RefType=CHILD_OF}'],
|
||||
[
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S1","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
],
|
||||
[
|
||||
1657275433246,
|
||||
'span_2',
|
||||
'0000000000000000span_1',
|
||||
'frontend',
|
||||
'HTTP GET SPAN 2',
|
||||
'2',
|
||||
'683273000',
|
||||
['host.name.span2'],
|
||||
['span_2'],
|
||||
['{TraceId=0000000000000000span_1, SpanId=span_1, RefType=CHILD_OF}'],
|
||||
[
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S2","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
],
|
||||
[
|
||||
1657275433246,
|
||||
'span_3',
|
||||
'0000000000000000span_1',
|
||||
'frontend',
|
||||
'HTTP GET SPAN 3',
|
||||
'2',
|
||||
'683273000',
|
||||
['host.name.span3'],
|
||||
['span_3'],
|
||||
['{TraceId=0000000000000000span_1, SpanId=span_2, RefType=CHILD_OF}'],
|
||||
[
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S3","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
],
|
||||
];
|
@ -109,6 +109,12 @@ export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
|
||||
const missingSpanTree: ITraceTree[] = [];
|
||||
const referencedTraceIds: string[] = Array.from(traceIdSet);
|
||||
Object.keys(spanMap).forEach((spanId) => {
|
||||
const isRoot = spanMap[spanId].references?.some((refs) => refs.SpanId === '');
|
||||
if (isRoot) {
|
||||
spanTree.push(spanMap[spanId]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const traceId of referencedTraceIds) {
|
||||
if (traceId.includes(spanId)) {
|
||||
spanTree.push(spanMap[spanId]);
|
||||
|
38
frontend/tests/auth.json
Normal file
38
frontend/tests/auth.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3301",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "isSideBarCollapsed",
|
||||
"value": "false"
|
||||
},
|
||||
{
|
||||
"name": "metricsTimeDurations",
|
||||
"value": "{}"
|
||||
},
|
||||
{
|
||||
"name": "i18nextLng",
|
||||
"value": "en-US"
|
||||
},
|
||||
{
|
||||
"name": "reactQueryDevtoolsSortFn",
|
||||
"value": "\"Status > Last Updated\""
|
||||
},
|
||||
{
|
||||
"name": "AUTH_TOKEN",
|
||||
"value": "authtoken"
|
||||
},
|
||||
{
|
||||
"name": "IS_LOGGED_IN",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "REFRESH_AUTH_TOKEN",
|
||||
"value": "refreshJwt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
7
frontend/tests/fixtures/api/login/200.json
vendored
Normal file
7
frontend/tests/fixtures/api/login/200.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"accessJwt": "authtoken",
|
||||
"accessJwtExpiry": 1656609177,
|
||||
"refreshJwt": "refreshJwt",
|
||||
"refreshJwtExpiry": 1659199377,
|
||||
"userId": "34917776-514b-4b95-a4f5-1a5cc06e34b6"
|
||||
}
|
3
frontend/tests/fixtures/api/organisation/201.json
vendored
Normal file
3
frontend/tests/fixtures/api/organisation/201.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"data": "org updated successfully"
|
||||
}
|
1
frontend/tests/fixtures/api/register/200.json
vendored
Normal file
1
frontend/tests/fixtures/api/register/200.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ "data": "user registered successfully" }
|
5
frontend/tests/fixtures/api/register/401.json
vendored
Normal file
5
frontend/tests/fixtures/api/register/401.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": "error",
|
||||
"errorType": "unauthorized",
|
||||
"error": "You are not allowed to create an account. Please ask your admin to send an invite link"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user