Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8a1a8600e | ||
|
|
45cb1eb38f | ||
|
|
8ebb76bd0c | ||
|
|
309ffa4989 | ||
|
|
d787298600 | ||
|
|
7998d474e2 | ||
|
|
7b8ff5a285 | ||
|
|
cb22aef36f | ||
|
|
cf93712286 | ||
|
|
a906f94b8a | ||
|
|
93b6749920 | ||
|
|
8ab527b174 | ||
|
|
ad163c2b61 | ||
|
|
21f909f4c0 | ||
|
|
b67206dd65 | ||
|
|
ce5afd31fd | ||
|
|
9a184f5740 | ||
|
|
be14f1c32c | ||
|
|
ae37a608f8 | ||
|
|
aaeb579d0d | ||
|
|
1151e8521e | ||
|
|
d779b83715 | ||
|
|
47a41473df | ||
|
|
de370d7f0c | ||
|
|
8a5b26cefe | ||
|
|
2a20b6fc86 | ||
|
|
02ef1744b4 | ||
|
|
2c973adf0b | ||
|
|
f7ff491d35 | ||
|
|
6cd341a887 | ||
|
|
0832bce955 | ||
|
|
62b2462e03 | ||
|
|
152846f554 | ||
|
|
846da08cbd | ||
|
|
17f32e9765 | ||
|
|
48659a2957 | ||
|
|
a2a8a32d1c | ||
|
|
28f2ee2627 | ||
|
|
3b01bb2614 | ||
|
|
622e1765cf | ||
|
|
faaf0a6e73 | ||
|
|
4542a51531 | ||
|
|
191a538430 | ||
|
|
e6ce80213b | ||
|
|
3115b32dcd | ||
|
|
af272a368b | ||
|
|
b336a6cb45 | ||
|
|
b72815ca2f | ||
|
|
ed4a01dea6 | ||
|
|
1914c3b4a0 | ||
|
|
3811e96e23 | ||
|
|
8d16493432 | ||
|
|
db2bfbb887 | ||
|
|
213838a021 | ||
|
|
fd6f9a90e1 | ||
|
|
13f9922c53 | ||
|
|
a654baaa5b | ||
|
|
f766435acc | ||
|
|
d7a65ba689 | ||
|
|
05ce03e67d | ||
|
|
ba6818f487 | ||
|
|
ca53136cbf | ||
|
|
c46bef321c | ||
|
|
ba8f804b26 | ||
|
|
6cc7025e37 | ||
|
|
e62e541fc4 | ||
|
|
2f1ca93eda | ||
|
|
f1c7d72fc5 | ||
|
|
a405307c96 | ||
|
|
c85d48d7fa | ||
|
|
75470f6bb9 | ||
|
|
f75e688b32 | ||
|
|
5f3ca045df | ||
|
|
186632af69 | ||
|
|
fa652be926 | ||
|
|
1e39131c38 | ||
|
|
153e859ac3 | ||
|
|
d1cc29e118 | ||
|
|
972bf94dd0 | ||
|
|
3632208d45 | ||
|
|
cd9768c738 | ||
|
|
f01b9605db | ||
|
|
eec236af50 | ||
|
|
bbff2b459e | ||
|
|
d9535e7a8d | ||
|
|
a82bbe1a72 | ||
|
|
6812f55152 | ||
|
|
83163c17cd | ||
|
|
5ed7c9a46e | ||
|
|
2f323056d0 | ||
|
|
51b583480b | ||
|
|
7b1e2c8b98 | ||
|
|
b87f3bdb50 | ||
|
|
2f5908a3dd | ||
|
|
ca77820e9d | ||
|
|
a4346a2d93 | ||
|
|
44360ecacf | ||
|
|
b675c3cfec | ||
|
|
b23d8da96c | ||
|
|
215ea8d819 | ||
|
|
0c27d5acbc | ||
|
|
435d74c37e |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -32,6 +32,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
make test
|
||||
- name: Build query-service image
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
1
.github/workflows/pr_verify_linked_issue.yml
vendored
1
.github/workflows/pr_verify_linked_issue.yml
vendored
@@ -17,4 +17,3 @@ jobs:
|
||||
uses: hattan/verify-linked-issue-action@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
25
.github/workflows/repo-stats.yml
vendored
25
.github/workflows/repo-stats.yml
vendored
@@ -1,25 +0,0 @@
|
||||
on:
|
||||
schedule:
|
||||
# Run this once per day, towards the end of the day for keeping the most
|
||||
# recent data point most meaningful (hours are interpreted in UTC).
|
||||
- cron: "0 8 * * *"
|
||||
workflow_dispatch: # Allow for running this manually.
|
||||
|
||||
jobs:
|
||||
j1:
|
||||
name: repostats
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: run-ghrs
|
||||
uses: jgehrcke/github-repo-stats@v1.1.0
|
||||
with:
|
||||
# Define the stats repository (the repo to fetch
|
||||
# stats for and to generate the report for).
|
||||
# Remove the parameter when the stats repository
|
||||
# and the data repository are the same.
|
||||
repository: signoz/signoz
|
||||
# Set a GitHub API token that can read the stats
|
||||
# repository, and that can push to the data
|
||||
# repository (which this workflow file lives in),
|
||||
# to store data and the report files.
|
||||
ghtoken: ${{ github.token }}
|
||||
1
.github/workflows/sonar.yml
vendored
1
.github/workflows/sonar.yml
vendored
@@ -24,4 +24,3 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
5
Makefile
5
Makefile
@@ -54,7 +54,7 @@ build-push-frontend:
|
||||
@echo "--> Building and pushing frontend docker image"
|
||||
@echo "------------------"
|
||||
@cd $(FRONTEND_DIRECTORY) && \
|
||||
docker buildx build --file Dockerfile --progress plane --push --platform linux/amd64 \
|
||||
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
|
||||
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
# Steps to build and push docker image of query service
|
||||
@@ -135,3 +135,6 @@ clear-standalone-data:
|
||||
clear-swarm-data:
|
||||
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
||||
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
|
||||
|
||||
test:
|
||||
go test ./pkg/query-service/app/metrics/...
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
##
|
||||
|
||||
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
|
||||
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
|
||||
|
||||
👉 Visualise Metrics, Traces and Logs in a single pane of glass
|
||||
|
||||
@@ -144,6 +144,8 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
|
||||
- SigNoz Logs management are based on ClickHouse, a columnar OLAP datastore which makes aggregate log analytics queries much more efficient
|
||||
- 50% lower resource requirement compared to Elastic during ingestion
|
||||
|
||||
We have published benchmarks comparing Elastic with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
@@ -152,6 +154,8 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
|
||||
- SigNoz supports indexes over high cardinality data and has no limitations on the number of indexes, while Loki reaches max streams with a few indexes added to it.
|
||||
- Searching over a huge volume of data is difficult and slow in Loki compared to SigNoz
|
||||
|
||||
We have published benchmarks comparing Loki with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
|
||||
|
||||
@@ -27,12 +27,6 @@ For x86 chip (amd):
|
||||
docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
For Mac with Apple chip (arm):
|
||||
|
||||
```sh
|
||||
docker-compose -f docker/clickhouse-setup/docker-compose.arm.yaml up -d
|
||||
```
|
||||
|
||||
Open http://localhost:3301 in your favourite browser. In couple of minutes, you should see
|
||||
the data generated from hotrod in SigNoz UI.
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.13.1
|
||||
image: signoz/query-service:0.16.1
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.13.1
|
||||
image: signoz/frontend:0.16.1
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.66.1
|
||||
image: signoz/signoz-otel-collector:0.66.4
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@@ -188,6 +188,7 @@ services:
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
@@ -207,7 +208,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.66.1
|
||||
image: signoz/signoz-otel-collector:0.66.4
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
||||
@@ -86,6 +86,10 @@ processors:
|
||||
default: default
|
||||
- name: deployment.environment
|
||||
default: default
|
||||
# This is added to ensure the uniqueness of the timeseries
|
||||
# Otherwise, identical timeseries produced by multiple replicas of
|
||||
# collectors result in incorrect APM metrics
|
||||
- name: 'signoz.collector.id'
|
||||
# memory_limiter:
|
||||
# # 80% of maximum memory up to 2G
|
||||
# limit_mib: 1500
|
||||
@@ -106,6 +110,7 @@ exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
|
||||
@@ -905,7 +905,8 @@
|
||||
<dictionaries_config>*_dictionary.xml</dictionaries_config>
|
||||
|
||||
<!-- Configuration of user defined executable functions -->
|
||||
<user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>
|
||||
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
|
||||
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
|
||||
|
||||
<!-- Uncomment if you want data to be compressed 30-100% better.
|
||||
Don't do that if you just started using ClickHouse.
|
||||
|
||||
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal file
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<functions>
|
||||
<function>
|
||||
<type>executable</type>
|
||||
<name>histogramQuantile</name>
|
||||
<return_type>Float64</return_type>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>buckets</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>counts</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Float64</type>
|
||||
<name>quantile</name>
|
||||
</argument>
|
||||
<format>CSV</format>
|
||||
<command>./histogramQuantile</command>
|
||||
</function>
|
||||
</functions>
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: otel-collector
|
||||
image: signoz/signoz-otel-collector:0.66.1
|
||||
image: signoz/signoz-otel-collector:0.66.4
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.66.1
|
||||
image: signoz/signoz-otel-collector:0.66.4
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
||||
@@ -97,9 +97,11 @@ services:
|
||||
volumes:
|
||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
|
||||
- ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
- ./data/clickhouse/:/var/lib/clickhouse/
|
||||
- ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
# clickhouse-2:
|
||||
# <<: *clickhouse-defaults
|
||||
@@ -112,9 +114,12 @@ services:
|
||||
# volumes:
|
||||
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
|
||||
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
# - ./data/clickhouse-2/:/var/lib/clickhouse/
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
|
||||
# clickhouse-3:
|
||||
# <<: *clickhouse-defaults
|
||||
@@ -127,9 +132,11 @@ services:
|
||||
# volumes:
|
||||
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
|
||||
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2}
|
||||
@@ -146,7 +153,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.13.1}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.16.1}
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@@ -174,7 +181,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.13.1}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.16.1}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -186,7 +193,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@@ -195,6 +202,7 @@ services:
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
@@ -211,7 +219,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
@@ -224,15 +232,15 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
hotrod:
|
||||
image: jaegertracing/example-hotrod:1.30
|
||||
container_name: hotrod
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
command: ["all"]
|
||||
environment:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
image: jaegertracing/example-hotrod:1.30
|
||||
container_name: hotrod
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
command: ["all"]
|
||||
environment:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
|
||||
@@ -83,6 +83,10 @@ processors:
|
||||
default: default
|
||||
- name: deployment.environment
|
||||
default: default
|
||||
# This is added to ensure the uniqueness of the timeseries
|
||||
# Otherwise, identical timeseries produced by multiple replicas of
|
||||
# collectors result in incorrect APM metrics
|
||||
- name: 'signoz.collector.id'
|
||||
# memory_limiter:
|
||||
# # 80% of maximum memory up to 2G
|
||||
# limit_mib: 1500
|
||||
@@ -115,6 +119,7 @@ exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
|
||||
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
Binary file not shown.
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal file
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NOTE: executable must be built with target OS and architecture set to linux/amd64
|
||||
// env GOOS=linux GOARCH=amd64 go build -o histogramQuantile histogramQuantile.go
|
||||
|
||||
// The following code is adapted from the following source:
|
||||
// https://github.com/prometheus/prometheus/blob/main/promql/quantile.go
|
||||
|
||||
type bucket struct {
|
||||
upperBound float64
|
||||
count float64
|
||||
}
|
||||
|
||||
// buckets implements sort.Interface.
|
||||
type buckets []bucket
|
||||
|
||||
func (b buckets) Len() int { return len(b) }
|
||||
func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }
|
||||
|
||||
// bucketQuantile calculates the quantile 'q' based on the given buckets. The
|
||||
// buckets will be sorted by upperBound by this function (i.e. no sorting
|
||||
// needed before calling this function). The quantile value is interpolated
|
||||
// assuming a linear distribution within a bucket. However, if the quantile
|
||||
// falls into the highest bucket, the upper bound of the 2nd highest bucket is
|
||||
// returned. A natural lower bound of 0 is assumed if the upper bound of the
|
||||
// lowest bucket is greater 0. In that case, interpolation in the lowest bucket
|
||||
// happens linearly between 0 and the upper bound of the lowest bucket.
|
||||
// However, if the lowest bucket has an upper bound less or equal 0, this upper
|
||||
// bound is returned if the quantile falls into the lowest bucket.
|
||||
//
|
||||
// There are a number of special cases (once we have a way to report errors
|
||||
// happening during evaluations of AST functions, we should report those
|
||||
// explicitly):
|
||||
//
|
||||
// If 'buckets' has 0 observations, NaN is returned.
|
||||
//
|
||||
// If 'buckets' has fewer than 2 elements, NaN is returned.
|
||||
//
|
||||
// If the highest bucket is not +Inf, NaN is returned.
|
||||
//
|
||||
// If q==NaN, NaN is returned.
|
||||
//
|
||||
// If q<0, -Inf is returned.
|
||||
//
|
||||
// If q>1, +Inf is returned.
|
||||
func bucketQuantile(q float64, buckets buckets) float64 {
|
||||
if math.IsNaN(q) {
|
||||
return math.NaN()
|
||||
}
|
||||
if q < 0 {
|
||||
return math.Inf(-1)
|
||||
}
|
||||
if q > 1 {
|
||||
return math.Inf(+1)
|
||||
}
|
||||
sort.Sort(buckets)
|
||||
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
buckets = coalesceBuckets(buckets)
|
||||
ensureMonotonic(buckets)
|
||||
|
||||
if len(buckets) < 2 {
|
||||
return math.NaN()
|
||||
}
|
||||
observations := buckets[len(buckets)-1].count
|
||||
if observations == 0 {
|
||||
return math.NaN()
|
||||
}
|
||||
rank := q * observations
|
||||
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
|
||||
|
||||
if b == len(buckets)-1 {
|
||||
return buckets[len(buckets)-2].upperBound
|
||||
}
|
||||
if b == 0 && buckets[0].upperBound <= 0 {
|
||||
return buckets[0].upperBound
|
||||
}
|
||||
var (
|
||||
bucketStart float64
|
||||
bucketEnd = buckets[b].upperBound
|
||||
count = buckets[b].count
|
||||
)
|
||||
if b > 0 {
|
||||
bucketStart = buckets[b-1].upperBound
|
||||
count -= buckets[b-1].count
|
||||
rank -= buckets[b-1].count
|
||||
}
|
||||
return bucketStart + (bucketEnd-bucketStart)*(rank/count)
|
||||
}
|
||||
|
||||
// coalesceBuckets merges buckets with the same upper bound.
|
||||
//
|
||||
// The input buckets must be sorted.
|
||||
func coalesceBuckets(buckets buckets) buckets {
|
||||
last := buckets[0]
|
||||
i := 0
|
||||
for _, b := range buckets[1:] {
|
||||
if b.upperBound == last.upperBound {
|
||||
last.count += b.count
|
||||
} else {
|
||||
buckets[i] = last
|
||||
last = b
|
||||
i++
|
||||
}
|
||||
}
|
||||
buckets[i] = last
|
||||
return buckets[:i+1]
|
||||
}
|
||||
|
||||
// The assumption that bucket counts increase monotonically with increasing
|
||||
// upperBound may be violated during:
|
||||
//
|
||||
// * Recording rule evaluation of histogram_quantile, especially when rate()
|
||||
// has been applied to the underlying bucket timeseries.
|
||||
// * Evaluation of histogram_quantile computed over federated bucket
|
||||
// timeseries, especially when rate() has been applied.
|
||||
//
|
||||
// This is because scraped data is not made available to rule evaluation or
|
||||
// federation atomically, so some buckets are computed with data from the
|
||||
// most recent scrapes, but the other buckets are missing data from the most
|
||||
// recent scrape.
|
||||
//
|
||||
// Monotonicity is usually guaranteed because if a bucket with upper bound
|
||||
// u1 has count c1, then any bucket with a higher upper bound u > u1 must
|
||||
// have counted all c1 observations and perhaps more, so that c >= c1.
|
||||
//
|
||||
// Randomly interspersed partial sampling breaks that guarantee, and rate()
|
||||
// exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
|
||||
// 4 samples but the bucket with le=2000 has a count of 7 from 3 samples. The
|
||||
// monotonicity is broken. It is exacerbated by rate() because under normal
|
||||
// operation, cumulative counting of buckets will cause the bucket counts to
|
||||
// diverge such that small differences from missing samples are not a problem.
|
||||
// rate() removes this divergence.)
|
||||
//
|
||||
// bucketQuantile depends on that monotonicity to do a binary search for the
|
||||
// bucket with the φ-quantile count, so breaking the monotonicity
|
||||
// guarantee causes bucketQuantile() to return undefined (nonsense) results.
|
||||
//
|
||||
// As a somewhat hacky solution until ingestion is atomic per scrape, we
|
||||
// calculate the "envelope" of the histogram buckets, essentially removing
|
||||
// any decreases in the count between successive buckets.
|
||||
|
||||
func ensureMonotonic(buckets buckets) {
|
||||
max := buckets[0].count
|
||||
for i := 1; i < len(buckets); i++ {
|
||||
switch {
|
||||
case buckets[i].count > max:
|
||||
max = buckets[i].count
|
||||
case buckets[i].count < max:
|
||||
buckets[i].count = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End of copied code.
|
||||
|
||||
func readLines() []string {
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
bytes := []byte{}
|
||||
lines := []string{}
|
||||
for {
|
||||
line, isPrefix, err := r.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
bytes = append(bytes, line...)
|
||||
if !isPrefix {
|
||||
str := strings.TrimSpace(string(bytes))
|
||||
if len(str) > 0 {
|
||||
lines = append(lines, str)
|
||||
bytes = []byte{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(bytes) > 0 {
|
||||
lines = append(lines, string(bytes))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func main() {
|
||||
lines := readLines()
|
||||
for _, text := range lines {
|
||||
// Example input
|
||||
// "[1, 2, 4, 8, 16]", "[1, 5, 8, 10, 14]", 0.9"
|
||||
// bounds - counts - quantile
|
||||
parts := strings.Split(text, "\",")
|
||||
|
||||
var bucketNumbers []float64
|
||||
// Strip the ends with square brackets
|
||||
text = parts[0][2 : len(parts[0])-1]
|
||||
// Parse the bucket bounds
|
||||
for _, num := range strings.Split(text, ",") {
|
||||
num = strings.TrimSpace(num)
|
||||
number, err := strconv.ParseFloat(num, 64)
|
||||
if err == nil {
|
||||
bucketNumbers = append(bucketNumbers, number)
|
||||
}
|
||||
}
|
||||
|
||||
var bucketCounts []float64
|
||||
// Strip the ends with square brackets
|
||||
text = parts[1][2 : len(parts[1])-1]
|
||||
// Parse the bucket counts
|
||||
for _, num := range strings.Split(text, ",") {
|
||||
num = strings.TrimSpace(num)
|
||||
number, err := strconv.ParseFloat(num, 64)
|
||||
if err == nil {
|
||||
bucketCounts = append(bucketCounts, number)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the quantile
|
||||
q, err := strconv.ParseFloat(parts[2], 64)
|
||||
var b buckets
|
||||
|
||||
if err == nil {
|
||||
for i := 0; i < len(bucketNumbers); i++ {
|
||||
b = append(b, bucket{upperBound: bucketNumbers[i], count: bucketCounts[i]})
|
||||
}
|
||||
}
|
||||
fmt.Println(bucketQuantile(q, b))
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,11 @@ check_os() {
|
||||
os="centos"
|
||||
package_manager="yum"
|
||||
;;
|
||||
Rocky*)
|
||||
desired_os=1
|
||||
os="centos"
|
||||
package_manager="yum"
|
||||
;;
|
||||
SLES*)
|
||||
desired_os=1
|
||||
os="sles"
|
||||
@@ -223,7 +228,7 @@ wait_for_containers_start() {
|
||||
|
||||
# The while loop is important because for-loops don't work for dynamic values
|
||||
while [[ $timeout -gt 0 ]]; do
|
||||
status_code="$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3301/api/v1/services/list || true)"
|
||||
status_code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3301/api/v1/health?live=1" || true)"
|
||||
if [[ status_code -eq 200 ]]; then
|
||||
break
|
||||
else
|
||||
@@ -511,13 +516,15 @@ else
|
||||
echo ""
|
||||
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
||||
echo ""
|
||||
echo "ℹ️ By default, retention period is set to 7 days for logs and traces, and 30 days for metrics."
|
||||
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
|
||||
|
||||
echo "ℹ️ To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
|
||||
|
||||
echo ""
|
||||
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||
echo ""
|
||||
echo "👉 Need help Getting Started?"
|
||||
echo "👉 Need help in Getting Started?"
|
||||
echo -e "Join us on Slack https://signoz.io/slack"
|
||||
echo ""
|
||||
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||
basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||
@@ -271,42 +272,42 @@ func (lrw *loggingResponseWriter) Flush() {
|
||||
|
||||
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
|
||||
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
|
||||
var requestBody map[string]interface{}
|
||||
|
||||
data := map[string]interface{}{}
|
||||
var postData *model.QueryRangeParamsV2
|
||||
|
||||
if path == pathToExtractBodyFrom && (r.Method == "POST") {
|
||||
bodyBytes, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close() // must close
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
if r.Body != nil {
|
||||
bodyBytes, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
r.Body.Close() // must close
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
json.Unmarshal(bodyBytes, &postData)
|
||||
|
||||
json.Unmarshal(bodyBytes, &requestBody)
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
|
||||
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
|
||||
signozMetricFound := false
|
||||
signozMetricNotFound := false
|
||||
|
||||
if compositeMetricQueryExists {
|
||||
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
|
||||
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
|
||||
if queryTypeExists {
|
||||
data["queryType"] = queryType
|
||||
}
|
||||
panelType, panelTypeExists := compositeMetricQueryMap["panelType"]
|
||||
if panelTypeExists {
|
||||
data["panelType"] = panelType
|
||||
if postData != nil {
|
||||
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
|
||||
|
||||
if postData.CompositeMetricQuery != nil {
|
||||
data["queryType"] = postData.CompositeMetricQuery.QueryType
|
||||
data["panelType"] = postData.CompositeMetricQuery.PanelType
|
||||
}
|
||||
|
||||
data["datasource"] = postData.DataSource
|
||||
}
|
||||
|
||||
datasource, datasourceExists := requestBody["dataSource"]
|
||||
if datasourceExists {
|
||||
data["datasource"] = datasource
|
||||
}
|
||||
|
||||
if !signozMetricFound {
|
||||
if signozMetricNotFound {
|
||||
telemetry.GetInstance().AddActiveMetricsUser()
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
|
||||
}
|
||||
|
||||
@@ -102,9 +102,10 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
|
||||
// eslint rules need to remove
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && npm run commitlint
|
||||
cd frontend && yarn run commitlint --edit $1
|
||||
|
||||
1
frontend/.yarnrc
Normal file
1
frontend/.yarnrc
Normal file
@@ -0,0 +1 @@
|
||||
network-timeout 600000
|
||||
@@ -9,8 +9,9 @@ ARG TARGETARCH
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
# Copy the package.json to install dependencies
|
||||
# Copy the package.json and .yarnrc files prior to install dependencies
|
||||
COPY package.json ./
|
||||
COPY .yarnrc ./
|
||||
|
||||
# Install the dependencies and make the folder
|
||||
RUN CI=1 yarn install
|
||||
|
||||
@@ -25,6 +25,7 @@ const config: Config.InitialOptions = {
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
testEnvironmentOptions: {
|
||||
'jest-playwright': {
|
||||
browsers: ['chromium', 'firefox', 'webkit'],
|
||||
|
||||
@@ -27,16 +27,12 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/colors": "6.0.0",
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@grafana/data": "^8.4.3",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@welldone-software/why-did-you-render": "^6.2.1",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"antd": "4.19.2",
|
||||
"antd": "5.0.5",
|
||||
"axios": "^0.21.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.0",
|
||||
@@ -44,7 +40,7 @@
|
||||
"babel-plugin-named-asset-import": "^0.3.7",
|
||||
"babel-preset-minify": "^0.5.1",
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"chart.js": "^3.4.0",
|
||||
"chart.js": "3.9.1",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
@@ -70,17 +66,18 @@
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"react": "17.0.0",
|
||||
"react-dom": "17.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-force-graph": "^1.41.0",
|
||||
"react-graph-vis": "^1.0.5",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-intersection-observer": "9.4.1",
|
||||
"react-query": "^3.34.19",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use": "^17.3.2",
|
||||
"react-vis": "^1.11.7",
|
||||
"react-virtuoso": "4.0.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"stream": "^0.0.2",
|
||||
@@ -120,7 +117,9 @@
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@playwright/test": "^1.22.0",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/compression-webpack-plugin": "^9.0.0",
|
||||
"@types/copy-webpack-plugin": "^8.0.1",
|
||||
@@ -132,10 +131,11 @@
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/mini-css-extract-plugin": "^2.5.1",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react-grid-layout": "^1.1.2",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-resizable": "3.0.3",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
@@ -145,6 +145,7 @@
|
||||
"@types/webpack-dev-server": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"@welldone-software/why-did-you-render": "6.2.1",
|
||||
"autoprefixer": "^9.0.0",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
@@ -173,7 +174,9 @@
|
||||
"lint-staged": "^12.3.7",
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"react-hooks-testing-library": "0.6.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-resizable": "3.0.4",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
@@ -186,7 +189,7 @@
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0"
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
10
frontend/public/css/antd.dark.min.css
vendored
10
frontend/public/css/antd.dark.min.css
vendored
File diff suppressed because one or more lines are too long
10
frontend/public/css/antd.min.css
vendored
10
frontend/public/css/antd.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { notification } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import loginApi from 'api/user/login';
|
||||
import { Logout } from 'api/utils';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -47,6 +47,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
|
||||
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
|
||||
@@ -106,7 +108,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
} else {
|
||||
Logout();
|
||||
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: response.error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ConfigProvider } from 'antd';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import AppLayout from 'container/AppLayout';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
@@ -9,29 +12,33 @@ import PrivateRoute from './Private';
|
||||
import routes from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<PrivateRoute>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => {
|
||||
return (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
const themeConfig = useThemeConfig();
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</PrivateRoute>
|
||||
</Router>
|
||||
return (
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<NotificationProvider>
|
||||
<Router history={history}>
|
||||
<PrivateRoute>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</PrivateRoute>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -8,9 +8,7 @@ const query = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/variables/query?query=${encodeURIComponent(props.query)}`,
|
||||
);
|
||||
const response = await axios.post(`/variables/query`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -10,9 +10,11 @@ const getSpans = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Key: e.Key,
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const exclude: string[] = [];
|
||||
@@ -35,7 +37,7 @@ const getSpans = async (
|
||||
start: String(props.start),
|
||||
end: String(props.end),
|
||||
function: props.function,
|
||||
groupBy: props.groupBy,
|
||||
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
|
||||
step: props.step,
|
||||
tags: updatedSelectedTags,
|
||||
...nonDuration,
|
||||
|
||||
@@ -28,9 +28,11 @@ const getSpanAggregate = async (
|
||||
});
|
||||
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Key: e.Key,
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const other = Object.fromEntries(props.selectedFilter);
|
||||
|
||||
@@ -11,9 +11,11 @@ const getTagValue = async (
|
||||
const response = await axios.post<PayloadProps>(`/getTagValues`, {
|
||||
start: props.start.toString(),
|
||||
end: props.end.toString(),
|
||||
tagKey: props.tagKey,
|
||||
tagKey: {
|
||||
Key: props.tagKey.Key,
|
||||
Type: props.tagKey.Type,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import generatePicker from 'antd/es/date-picker/generatePicker';
|
||||
import { Dayjs } from 'dayjs';
|
||||
// included in antd
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
|
||||
|
||||
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
|
||||
|
||||
export default DatePicker;
|
||||
@@ -1,8 +1,6 @@
|
||||
import MEditor, { EditorProps } from '@monaco-editor/react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
function Editor({
|
||||
value,
|
||||
@@ -12,7 +10,7 @@ function Editor({
|
||||
height,
|
||||
options,
|
||||
}: MEditorProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
return (
|
||||
<MEditor
|
||||
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
||||
|
||||
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { Chart, ChartTypeRegistry, Plugin } from 'chart.js';
|
||||
import * as ChartHelpers from 'chart.js/helpers';
|
||||
|
||||
// utils
|
||||
import { ChartEventHandler, mergeDefaultOptions } from './utils';
|
||||
|
||||
export const dragSelectPluginId = 'drag-select-plugin';
|
||||
|
||||
type ChartDragHandlers = {
|
||||
mousedown: ChartEventHandler;
|
||||
mousemove: ChartEventHandler;
|
||||
mouseup: ChartEventHandler;
|
||||
globalMouseup: () => void;
|
||||
};
|
||||
|
||||
export type DragSelectPluginOptions = {
|
||||
color?: string;
|
||||
onSelect?: (startValueX: number, endValueX: number) => void;
|
||||
};
|
||||
|
||||
const defaultDragSelectPluginOptions: Required<DragSelectPluginOptions> = {
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
onSelect: () => {},
|
||||
};
|
||||
|
||||
export function createDragSelectPluginOptions(
|
||||
isEnabled: boolean,
|
||||
onSelect?: (start: number, end: number) => void,
|
||||
color?: string,
|
||||
): DragSelectPluginOptions | false {
|
||||
if (!isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
onSelect,
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
function createMousedownHandler(
|
||||
chart: Chart,
|
||||
dragData: DragSelectData,
|
||||
): ChartEventHandler {
|
||||
return (ev): void => {
|
||||
const { left, right } = chart.chartArea;
|
||||
|
||||
let { x: startDragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||
|
||||
if (left > startDragPositionX) {
|
||||
startDragPositionX = left;
|
||||
}
|
||||
|
||||
if (right < startDragPositionX) {
|
||||
startDragPositionX = right;
|
||||
}
|
||||
|
||||
const startValuePositionX = chart.scales.x.getValueForPixel(
|
||||
startDragPositionX,
|
||||
);
|
||||
|
||||
dragData.onDragStart(startDragPositionX, startValuePositionX);
|
||||
};
|
||||
}
|
||||
|
||||
function createMousemoveHandler(
|
||||
chart: Chart,
|
||||
dragData: DragSelectData,
|
||||
): ChartEventHandler {
|
||||
return (ev): void => {
|
||||
if (!dragData.isMouseDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { left, right } = chart.chartArea;
|
||||
|
||||
let { x: dragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||
|
||||
if (left > dragPositionX) {
|
||||
dragPositionX = left;
|
||||
}
|
||||
|
||||
if (right < dragPositionX) {
|
||||
dragPositionX = right;
|
||||
}
|
||||
|
||||
const valuePositionX = chart.scales.x.getValueForPixel(dragPositionX);
|
||||
|
||||
dragData.onDrag(dragPositionX, valuePositionX);
|
||||
chart.update('none');
|
||||
};
|
||||
}
|
||||
|
||||
function createMouseupHandler(
|
||||
chart: Chart,
|
||||
options: DragSelectPluginOptions,
|
||||
dragData: DragSelectData,
|
||||
): ChartEventHandler {
|
||||
return (ev): void => {
|
||||
const { left, right } = chart.chartArea;
|
||||
|
||||
let { x: endRelativePostionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||
|
||||
if (left > endRelativePostionX) {
|
||||
endRelativePostionX = left;
|
||||
}
|
||||
|
||||
if (right < endRelativePostionX) {
|
||||
endRelativePostionX = right;
|
||||
}
|
||||
|
||||
const endValuePositionX = chart.scales.x.getValueForPixel(
|
||||
endRelativePostionX,
|
||||
);
|
||||
|
||||
dragData.onDragEnd(endRelativePostionX, endValuePositionX);
|
||||
|
||||
chart.update('none');
|
||||
|
||||
if (
|
||||
typeof options.onSelect === 'function' &&
|
||||
typeof dragData.startValuePositionX === 'number' &&
|
||||
typeof dragData.endValuePositionX === 'number'
|
||||
) {
|
||||
const start = Math.min(
|
||||
dragData.startValuePositionX,
|
||||
dragData.endValuePositionX,
|
||||
);
|
||||
const end = Math.max(
|
||||
dragData.startValuePositionX,
|
||||
dragData.endValuePositionX,
|
||||
);
|
||||
|
||||
options.onSelect(start, end);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createGlobalMouseupHandler(
|
||||
options: DragSelectPluginOptions,
|
||||
dragData: DragSelectData,
|
||||
): () => void {
|
||||
return (): void => {
|
||||
const { isDragging, endRelativePixelPositionX, endValuePositionX } = dragData;
|
||||
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragData.onDragEnd(
|
||||
endRelativePixelPositionX as number,
|
||||
endValuePositionX as number,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof options.onSelect === 'function' &&
|
||||
typeof dragData.startValuePositionX === 'number' &&
|
||||
typeof dragData.endValuePositionX === 'number'
|
||||
) {
|
||||
const start = Math.min(
|
||||
dragData.startValuePositionX,
|
||||
dragData.endValuePositionX,
|
||||
);
|
||||
const end = Math.max(
|
||||
dragData.startValuePositionX,
|
||||
dragData.endValuePositionX,
|
||||
);
|
||||
|
||||
options.onSelect(start, end);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class DragSelectData {
|
||||
public isDragging = false;
|
||||
|
||||
public isMouseDown = false;
|
||||
|
||||
public startRelativePixelPositionX: number | null = null;
|
||||
|
||||
public startValuePositionX: number | null | undefined = null;
|
||||
|
||||
public endRelativePixelPositionX: number | null = null;
|
||||
|
||||
public endValuePositionX: number | null | undefined = null;
|
||||
|
||||
public initialize(): void {
|
||||
this.isDragging = false;
|
||||
this.isMouseDown = false;
|
||||
this.startRelativePixelPositionX = null;
|
||||
this.startValuePositionX = null;
|
||||
this.endRelativePixelPositionX = null;
|
||||
this.endValuePositionX = null;
|
||||
}
|
||||
|
||||
public onDragStart(
|
||||
startRelativePixelPositionX: number,
|
||||
startValuePositionX: number | undefined,
|
||||
): void {
|
||||
this.isDragging = false;
|
||||
this.isMouseDown = true;
|
||||
this.startRelativePixelPositionX = startRelativePixelPositionX;
|
||||
this.startValuePositionX = startValuePositionX;
|
||||
this.endRelativePixelPositionX = null;
|
||||
this.endValuePositionX = null;
|
||||
}
|
||||
|
||||
public onDrag(
|
||||
endRelativePixelPositionX: number,
|
||||
endValuePositionX: number | undefined,
|
||||
): void {
|
||||
this.isDragging = true;
|
||||
this.endRelativePixelPositionX = endRelativePixelPositionX;
|
||||
this.endValuePositionX = endValuePositionX;
|
||||
}
|
||||
|
||||
public onDragEnd(
|
||||
endRelativePixelPositionX: number,
|
||||
endValuePositionX: number | undefined,
|
||||
): void {
|
||||
if (!this.isDragging) {
|
||||
this.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDragging = false;
|
||||
this.isMouseDown = false;
|
||||
this.endRelativePixelPositionX = endRelativePixelPositionX;
|
||||
this.endValuePositionX = endValuePositionX;
|
||||
}
|
||||
}
|
||||
|
||||
export const createDragSelectPlugin = (): Plugin<
|
||||
keyof ChartTypeRegistry,
|
||||
DragSelectPluginOptions
|
||||
> => {
|
||||
const dragData = new DragSelectData();
|
||||
let pluginOptions: Required<DragSelectPluginOptions>;
|
||||
|
||||
const handlers: ChartDragHandlers = {
|
||||
mousedown: () => {},
|
||||
mousemove: () => {},
|
||||
mouseup: () => {},
|
||||
globalMouseup: () => {},
|
||||
};
|
||||
|
||||
const dragSelectPlugin: Plugin<
|
||||
keyof ChartTypeRegistry,
|
||||
DragSelectPluginOptions
|
||||
> = {
|
||||
id: dragSelectPluginId,
|
||||
start: (chart: Chart, _, passedOptions) => {
|
||||
pluginOptions = mergeDefaultOptions(
|
||||
passedOptions,
|
||||
defaultDragSelectPluginOptions,
|
||||
);
|
||||
|
||||
const { canvas } = chart;
|
||||
|
||||
dragData.initialize();
|
||||
|
||||
const mousedownHandler = createMousedownHandler(chart, dragData);
|
||||
const mousemoveHandler = createMousemoveHandler(chart, dragData);
|
||||
const mouseupHandler = createMouseupHandler(chart, pluginOptions, dragData);
|
||||
const globalMouseupHandler = createGlobalMouseupHandler(
|
||||
pluginOptions,
|
||||
dragData,
|
||||
);
|
||||
|
||||
canvas.addEventListener('mousedown', mousedownHandler, { passive: true });
|
||||
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
|
||||
canvas.addEventListener('mouseup', mouseupHandler, { passive: true });
|
||||
document.addEventListener('mouseup', globalMouseupHandler, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
handlers.mousedown = mousedownHandler;
|
||||
handlers.mousemove = mousemoveHandler;
|
||||
handlers.mouseup = mouseupHandler;
|
||||
handlers.globalMouseup = globalMouseupHandler;
|
||||
},
|
||||
beforeDestroy: (chart: Chart) => {
|
||||
const { canvas } = chart;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.removeEventListener('mousedown', handlers.mousedown);
|
||||
canvas.removeEventListener('mousemove', handlers.mousemove);
|
||||
canvas.removeEventListener('mouseup', handlers.mouseup);
|
||||
document.removeEventListener('mouseup', handlers.globalMouseup);
|
||||
},
|
||||
afterDatasetsDraw: (chart: Chart) => {
|
||||
const {
|
||||
startRelativePixelPositionX,
|
||||
endRelativePixelPositionX,
|
||||
isDragging,
|
||||
} = dragData;
|
||||
|
||||
if (startRelativePixelPositionX && endRelativePixelPositionX && isDragging) {
|
||||
const left = Math.min(
|
||||
startRelativePixelPositionX,
|
||||
endRelativePixelPositionX,
|
||||
);
|
||||
const right = Math.max(
|
||||
startRelativePixelPositionX,
|
||||
endRelativePixelPositionX,
|
||||
);
|
||||
const top = chart.chartArea.top - 5;
|
||||
const bottom = chart.chartArea.bottom + 5;
|
||||
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.fillStyle = pluginOptions.color;
|
||||
chart.ctx.fillRect(left, top, right - left, bottom - top);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return dragSelectPlugin;
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export const emptyGraph = {
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = '1.5rem sans-serif';
|
||||
ctx.fillStyle = `${grey.primary}`;
|
||||
ctx.fillText('No data to display', width / 2, height / 2);
|
||||
ctx.fillText('No data', width / 2, height / 2);
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
|
||||
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Chart, ChartEvent, ChartTypeRegistry, Plugin } from 'chart.js';
|
||||
import * as ChartHelpers from 'chart.js/helpers';
|
||||
|
||||
// utils
|
||||
import { ChartEventHandler, mergeDefaultOptions } from './utils';
|
||||
|
||||
export const intersectionCursorPluginId = 'intersection-cursor-plugin';
|
||||
|
||||
export type IntersectionCursorPluginOptions = {
|
||||
color?: string;
|
||||
dashSize?: number;
|
||||
gapSize?: number;
|
||||
};
|
||||
|
||||
export const defaultIntersectionCursorPluginOptions: Required<IntersectionCursorPluginOptions> = {
|
||||
color: 'white',
|
||||
dashSize: 3,
|
||||
gapSize: 3,
|
||||
};
|
||||
|
||||
export function createIntersectionCursorPluginOptions(
|
||||
isEnabled: boolean,
|
||||
color?: string,
|
||||
dashSize?: number,
|
||||
gapSize?: number,
|
||||
): IntersectionCursorPluginOptions | false {
|
||||
if (!isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
dashSize,
|
||||
gapSize,
|
||||
};
|
||||
}
|
||||
|
||||
function createMousemoveHandler(
|
||||
chart: Chart,
|
||||
cursorData: IntersectionCursorData,
|
||||
): ChartEventHandler {
|
||||
return (ev: ChartEvent | MouseEvent): void => {
|
||||
const { left, right, top, bottom } = chart.chartArea;
|
||||
|
||||
let { x, y } = ChartHelpers.getRelativePosition(ev, chart);
|
||||
|
||||
if (left > x) {
|
||||
x = left;
|
||||
}
|
||||
|
||||
if (right < x) {
|
||||
x = right;
|
||||
}
|
||||
|
||||
if (y < top) {
|
||||
y = top;
|
||||
}
|
||||
|
||||
if (y > bottom) {
|
||||
y = bottom;
|
||||
}
|
||||
|
||||
cursorData.onMouseMove(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
function createMouseoutHandler(
|
||||
cursorData: IntersectionCursorData,
|
||||
): ChartEventHandler {
|
||||
return (): void => {
|
||||
cursorData.onMouseOut();
|
||||
};
|
||||
}
|
||||
|
||||
class IntersectionCursorData {
|
||||
public positionX: number | null | undefined;
|
||||
|
||||
public positionY: number | null | undefined;
|
||||
|
||||
public initialize(): void {
|
||||
this.positionX = null;
|
||||
this.positionY = null;
|
||||
}
|
||||
|
||||
public onMouseMove(x: number | undefined, y: number | undefined): void {
|
||||
this.positionX = x;
|
||||
this.positionY = y;
|
||||
}
|
||||
|
||||
public onMouseOut(): void {
|
||||
this.positionX = null;
|
||||
this.positionY = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const createIntersectionCursorPlugin = (): Plugin<
|
||||
keyof ChartTypeRegistry,
|
||||
IntersectionCursorPluginOptions
|
||||
> => {
|
||||
const cursorData = new IntersectionCursorData();
|
||||
let pluginOptions: Required<IntersectionCursorPluginOptions>;
|
||||
|
||||
let mousemoveHandler: (ev: ChartEvent | MouseEvent) => void;
|
||||
let mouseoutHandler: (ev: ChartEvent | MouseEvent) => void;
|
||||
|
||||
const intersectionCursorPlugin: Plugin<
|
||||
keyof ChartTypeRegistry,
|
||||
IntersectionCursorPluginOptions
|
||||
> = {
|
||||
id: intersectionCursorPluginId,
|
||||
start: (chart: Chart, _, passedOptions) => {
|
||||
const { canvas } = chart;
|
||||
|
||||
cursorData.initialize();
|
||||
pluginOptions = mergeDefaultOptions(
|
||||
passedOptions,
|
||||
defaultIntersectionCursorPluginOptions,
|
||||
);
|
||||
|
||||
mousemoveHandler = createMousemoveHandler(chart, cursorData);
|
||||
mouseoutHandler = createMouseoutHandler(cursorData);
|
||||
|
||||
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
|
||||
canvas.addEventListener('mouseout', mouseoutHandler, { passive: true });
|
||||
},
|
||||
beforeDestroy: (chart: Chart) => {
|
||||
const { canvas } = chart;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.removeEventListener('mousemove', mousemoveHandler);
|
||||
canvas.removeEventListener('mouseout', mouseoutHandler);
|
||||
},
|
||||
afterDatasetsDraw: (chart: Chart) => {
|
||||
const { positionX, positionY } = cursorData;
|
||||
|
||||
const lineDashData = [pluginOptions.dashSize, pluginOptions.gapSize];
|
||||
|
||||
if (typeof positionX === 'number' && typeof positionY === 'number') {
|
||||
const { top, bottom, left, right } = chart.chartArea;
|
||||
|
||||
chart.ctx.beginPath();
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.strokeStyle = pluginOptions.color;
|
||||
chart.ctx.setLineDash(lineDashData);
|
||||
chart.ctx.moveTo(left, positionY);
|
||||
chart.ctx.lineTo(right, positionY);
|
||||
chart.ctx.stroke();
|
||||
|
||||
chart.ctx.beginPath();
|
||||
chart.ctx.setLineDash(lineDashData);
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
chart.ctx.strokeStyle = pluginOptions.color;
|
||||
chart.ctx.moveTo(positionX, top);
|
||||
chart.ctx.lineTo(positionX, bottom);
|
||||
chart.ctx.stroke();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return intersectionCursorPlugin;
|
||||
};
|
||||
@@ -22,87 +22,86 @@ const getOrCreateLegendList = (
|
||||
listContainer.style.height = '100%';
|
||||
listContainer.style.flexWrap = 'wrap';
|
||||
listContainer.style.justifyContent = 'center';
|
||||
listContainer.style.fontSize = '0.75rem';
|
||||
legendContainer?.appendChild(listContainer);
|
||||
}
|
||||
|
||||
return listContainer;
|
||||
};
|
||||
|
||||
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
|
||||
return {
|
||||
id: 'htmlLegend',
|
||||
afterUpdate(chart): void {
|
||||
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
||||
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
id: 'htmlLegend',
|
||||
afterUpdate(chart): void {
|
||||
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
||||
|
||||
// Remove old legend items
|
||||
while (ul.firstChild) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
// Remove old legend items
|
||||
while (ul.firstChild) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
|
||||
// Reuse the built-in legendItems generator
|
||||
const items = get(chart, [
|
||||
'options',
|
||||
'plugins',
|
||||
'legend',
|
||||
'labels',
|
||||
'generateLabels',
|
||||
])
|
||||
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
|
||||
chart,
|
||||
)
|
||||
: null;
|
||||
// Reuse the built-in legendItems generator
|
||||
const items = get(chart, [
|
||||
'options',
|
||||
'plugins',
|
||||
'legend',
|
||||
'labels',
|
||||
'generateLabels',
|
||||
])
|
||||
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
|
||||
chart,
|
||||
)
|
||||
: null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?.forEach((item: Record<any, any>, index: number) => {
|
||||
const li = document.createElement('li');
|
||||
li.style.alignItems = 'center';
|
||||
li.style.cursor = 'pointer';
|
||||
li.style.display = 'flex';
|
||||
li.style.marginLeft = '10px';
|
||||
li.style.marginTop = '5px';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?.forEach((item: Record<any, any>, index: number) => {
|
||||
const li = document.createElement('li');
|
||||
li.style.alignItems = 'center';
|
||||
li.style.cursor = 'pointer';
|
||||
li.style.display = 'flex';
|
||||
li.style.marginLeft = '10px';
|
||||
// li.style.marginTop = '5px';
|
||||
|
||||
li.onclick = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { type } = chart.config;
|
||||
if (type === 'pie' || type === 'doughnut') {
|
||||
// Pie and doughnut charts only have a single dataset and visibility is per item
|
||||
chart.toggleDataVisibility(index);
|
||||
} else {
|
||||
chart.setDatasetVisibility(
|
||||
item.datasetIndex,
|
||||
!chart.isDatasetVisible(item.datasetIndex),
|
||||
);
|
||||
}
|
||||
chart.update();
|
||||
};
|
||||
|
||||
// Color box
|
||||
const boxSpan = document.createElement('span');
|
||||
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
|
||||
boxSpan.style.borderColor = `${item?.strokeStyle}`;
|
||||
boxSpan.style.borderWidth = `${item.lineWidth}px`;
|
||||
boxSpan.style.display = 'inline-block';
|
||||
boxSpan.style.minHeight = '20px';
|
||||
boxSpan.style.marginRight = '10px';
|
||||
boxSpan.style.minWidth = '20px';
|
||||
boxSpan.style.borderRadius = '50%';
|
||||
|
||||
if (item.text) {
|
||||
// Text
|
||||
const textContainer = document.createElement('span');
|
||||
textContainer.style.margin = '0';
|
||||
textContainer.style.padding = '0';
|
||||
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
|
||||
|
||||
const text = document.createTextNode(item.text);
|
||||
textContainer.appendChild(text);
|
||||
|
||||
li.appendChild(boxSpan);
|
||||
li.appendChild(textContainer);
|
||||
ul.appendChild(li);
|
||||
li.onclick = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { type } = chart.config;
|
||||
if (type === 'pie' || type === 'doughnut') {
|
||||
// Pie and doughnut charts only have a single dataset and visibility is per item
|
||||
chart.toggleDataVisibility(index);
|
||||
} else {
|
||||
chart.setDatasetVisibility(
|
||||
item.datasetIndex,
|
||||
!chart.isDatasetVisible(item.datasetIndex),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
chart.update();
|
||||
};
|
||||
|
||||
// Color box
|
||||
const boxSpan = document.createElement('span');
|
||||
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
|
||||
boxSpan.style.borderColor = `${item?.strokeStyle}`;
|
||||
boxSpan.style.borderWidth = `${item.lineWidth}px`;
|
||||
boxSpan.style.display = 'inline-block';
|
||||
boxSpan.style.minHeight = '0.75rem';
|
||||
boxSpan.style.marginRight = '0.5rem';
|
||||
boxSpan.style.minWidth = '0.75rem';
|
||||
boxSpan.style.borderRadius = '50%';
|
||||
|
||||
if (item.text) {
|
||||
// Text
|
||||
const textContainer = document.createElement('span');
|
||||
textContainer.style.margin = '0';
|
||||
textContainer.style.padding = '0';
|
||||
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
|
||||
|
||||
const text = document.createTextNode(item.text);
|
||||
textContainer.appendChild(text);
|
||||
|
||||
li.appendChild(boxSpan);
|
||||
li.appendChild(textContainer);
|
||||
ul.appendChild(li);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ChartEvent } from 'chart.js';
|
||||
|
||||
export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void;
|
||||
|
||||
export function mergeDefaultOptions<T extends Record<string, unknown>>(
|
||||
options: T,
|
||||
defaultOptions: Required<T>,
|
||||
): Required<T> {
|
||||
const sanitizedOptions = { ...options };
|
||||
Object.keys(options).forEach((key) => {
|
||||
if (sanitizedOptions[key as keyof T] === undefined) {
|
||||
delete sanitizedOptions[key as keyof T];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...defaultOptions,
|
||||
...sanitizedOptions,
|
||||
};
|
||||
}
|
||||
8
frontend/src/components/Graph/helpers.ts
Normal file
8
frontend/src/components/Graph/helpers.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
|
||||
export const getAxisLabelColor = (currentTheme: string): string => {
|
||||
if (currentTheme === 'light') {
|
||||
return themeColors.black;
|
||||
}
|
||||
return themeColors.whiteCream;
|
||||
};
|
||||
@@ -23,14 +23,27 @@ import {
|
||||
} 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';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { hasData } from './hasData';
|
||||
import { getAxisLabelColor } from './helpers';
|
||||
import { legend } from './Plugin';
|
||||
import {
|
||||
createDragSelectPlugin,
|
||||
createDragSelectPluginOptions,
|
||||
dragSelectPluginId,
|
||||
DragSelectPluginOptions,
|
||||
} from './Plugin/DragSelect';
|
||||
import { emptyGraph } from './Plugin/EmptyGraph';
|
||||
import {
|
||||
createIntersectionCursorPlugin,
|
||||
createIntersectionCursorPluginOptions,
|
||||
intersectionCursorPluginId,
|
||||
IntersectionCursorPluginOptions,
|
||||
} from './Plugin/IntersectionCursor';
|
||||
import { LegendsContainer } from './styles';
|
||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
@@ -66,9 +79,13 @@ function Graph({
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
}: GraphProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||
|
||||
@@ -92,7 +109,7 @@ function Graph({
|
||||
}
|
||||
|
||||
if (chartRef.current !== null) {
|
||||
const options: ChartOptions = {
|
||||
const options: CustomChartOptions = {
|
||||
animation: {
|
||||
duration: animate ? 200 : 0,
|
||||
},
|
||||
@@ -136,6 +153,10 @@ function Graph({
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const date = dayjs(context[0].parsed.x);
|
||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
||||
},
|
||||
label(context) {
|
||||
let label = context.dataset.label || '';
|
||||
|
||||
@@ -145,10 +166,27 @@ function Graph({
|
||||
if (context.parsed.y !== null) {
|
||||
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
labelTextColor(labelData) {
|
||||
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
return 'rgba(255, 255, 255, 0.75)';
|
||||
},
|
||||
},
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
),
|
||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||
!!onDragSelect,
|
||||
currentTheme === 'dark' ? 'white' : 'black',
|
||||
),
|
||||
},
|
||||
layout: {
|
||||
padding: 0,
|
||||
@@ -178,6 +216,7 @@ function Graph({
|
||||
},
|
||||
},
|
||||
type: 'time',
|
||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
@@ -186,6 +225,7 @@ function Graph({
|
||||
color: getGridColor(),
|
||||
},
|
||||
ticks: {
|
||||
color: getAxisLabelColor(currentTheme),
|
||||
// Include a dollar sign in the ticks
|
||||
callback(value) {
|
||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||
@@ -201,18 +241,50 @@ function Graph({
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
point: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hoverBackgroundColor: (ctx: any) => {
|
||||
if (ctx?.element?.options?.borderColor) {
|
||||
return ctx.element.options.borderColor;
|
||||
}
|
||||
return 'rgba(0,0,0,0.1)';
|
||||
},
|
||||
hoverRadius: 5,
|
||||
},
|
||||
},
|
||||
onClick: (event, element, chart) => {
|
||||
if (onClickHandler) {
|
||||
onClickHandler(event, element, chart, data);
|
||||
}
|
||||
},
|
||||
onHover: (event, _, chart) => {
|
||||
if (event.native) {
|
||||
const interactions = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{
|
||||
intersect: false,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (interactions[0]) {
|
||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
if (!chartHasData) chartPlugins.push(emptyGraph);
|
||||
if (chartHasData) {
|
||||
chartPlugins.push(createIntersectionCursorPlugin());
|
||||
chartPlugins.push(createDragSelectPlugin());
|
||||
} else {
|
||||
chartPlugins.push(emptyGraph);
|
||||
}
|
||||
|
||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||
|
||||
lineChartRef.current = new Chart(chartRef.current, {
|
||||
@@ -235,6 +307,9 @@ function Graph({
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -249,6 +324,13 @@ function Graph({
|
||||
);
|
||||
}
|
||||
|
||||
type CustomChartOptions = ChartOptions & {
|
||||
plugins: {
|
||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||
};
|
||||
};
|
||||
|
||||
interface GraphProps {
|
||||
animate?: boolean;
|
||||
type: ChartType;
|
||||
@@ -261,6 +343,8 @@ interface GraphProps {
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
dragSelectColor?: string;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
@@ -287,6 +371,11 @@ Graph.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
containerHeight: '85%',
|
||||
containerHeight: '90%',
|
||||
onDragSelect: undefined,
|
||||
dragSelectColor: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
||||
export default memo(Graph, (prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data),
|
||||
);
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const LegendsContainer = styled.div`
|
||||
height: 15%;
|
||||
height: 10%;
|
||||
|
||||
* {
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0.5rem;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: ${themeColors.royalGrey};
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: ${themeColors.matterhornGrey};
|
||||
}
|
||||
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Button, Popover } from 'antd';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
@@ -14,7 +16,7 @@ function AddToQueryHOC({
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const generatedQuery = useMemo(
|
||||
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
||||
@@ -31,7 +33,9 @@ function AddToQueryHOC({
|
||||
}
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: updatedQueryString,
|
||||
payload: {
|
||||
searchQueryString: updatedQueryString,
|
||||
},
|
||||
});
|
||||
}, [dispatch, generatedQuery, queryString]);
|
||||
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { Popover } from 'antd';
|
||||
import React from 'react';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
interface CopyClipboardHOCProps {
|
||||
textToCopy: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
function CopyClipboardHOC({
|
||||
textToCopy,
|
||||
children,
|
||||
}: CopyClipboardHOCProps): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const [value, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
useEffect(() => {
|
||||
if (value.value) {
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
}
|
||||
}, [value, notifications]);
|
||||
|
||||
const onClick = useCallback((): void => {
|
||||
setCopy(textToCopy);
|
||||
}, [setCopy, textToCopy]);
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(): void => setCopy(textToCopy)}
|
||||
onKeyDown={(): void => setCopy(textToCopy)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||
@@ -34,4 +33,9 @@ function CopyClipboardHOC({
|
||||
);
|
||||
}
|
||||
|
||||
interface CopyClipboardHOCProps {
|
||||
textToCopy: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default CopyClipboardHOC;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import { map } from 'd3';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -14,7 +15,7 @@ import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import AddToQueryHOC from '../AddToQueryHOC';
|
||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||
import { Container } from './styles';
|
||||
import { Container, LogContainer, Text, TextContainer } from './styles';
|
||||
import { isValidLogField } from './util';
|
||||
|
||||
interface LogFieldProps {
|
||||
@@ -23,21 +24,17 @@ interface LogFieldProps {
|
||||
}
|
||||
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
|
||||
<TextContainer>
|
||||
<Text ellipsis type="secondary">
|
||||
{fieldKey}
|
||||
</Text>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
{': '}
|
||||
{fieldValue}
|
||||
</Typography.Text>
|
||||
</CopyClipboardHOC>
|
||||
</div>
|
||||
</TextContainer>
|
||||
);
|
||||
}
|
||||
function LogSelectedField({
|
||||
@@ -83,6 +80,7 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
dispatch({
|
||||
@@ -93,40 +91,40 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<div>
|
||||
<div>
|
||||
{'{'}
|
||||
<div style={{ marginLeft: '0.5rem' }}>
|
||||
<LogGeneralField
|
||||
fieldKey="log"
|
||||
fieldValue={flattenLogData.body as never}
|
||||
/>
|
||||
{flattenLogData.stream && (
|
||||
<LogContainer>
|
||||
<>
|
||||
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="stream"
|
||||
fieldValue={flattenLogData.stream as never}
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</LogContainer>
|
||||
{'}'}
|
||||
</div>
|
||||
<div>
|
||||
{map(selected, (field) => {
|
||||
return isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
{map(selected, (field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card } from 'antd';
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const fadeInAnimation = keyframes`
|
||||
@@ -16,3 +16,20 @@ export const Container = styled(Card)`
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: ease-in;
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography.Text)`
|
||||
&&& {
|
||||
min-width: 1.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextContainer = styled.div`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
`;
|
||||
|
||||
@@ -11,7 +11,7 @@ function CustomModal({
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
visible={isModalVisible}
|
||||
open={isModalVisible}
|
||||
footer={footer}
|
||||
closable={closable}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
@@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const c = allComponentMap.find((item) => {
|
||||
return item.match(path, currentVersion, userFlags);
|
||||
});
|
||||
const c = allComponentMap.find((item) =>
|
||||
item.match(path, currentVersion, userFlags),
|
||||
);
|
||||
|
||||
if (!c) {
|
||||
return null;
|
||||
|
||||
51
frontend/src/components/ResizeTable/ResizableHeader.tsx
Normal file
51
frontend/src/components/ResizeTable/ResizableHeader.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Resizable, ResizeCallbackData } from 'react-resizable';
|
||||
|
||||
import { enableUserSelectHack } from './config';
|
||||
import { SpanStyle } from './styles';
|
||||
|
||||
function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
|
||||
const { onResize, width, ...restProps } = props;
|
||||
|
||||
const handle = useMemo(
|
||||
() => (
|
||||
<SpanStyle
|
||||
className="react-resizable-handle"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const draggableOpts = useMemo(
|
||||
() => ({
|
||||
enableUserSelectHack,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!width) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <th {...restProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
width={width}
|
||||
height={0}
|
||||
handle={handle}
|
||||
onResize={onResize}
|
||||
draggableOpts={draggableOpts}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<th {...restProps} />
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
interface ResizableHeaderProps {
|
||||
onResize: (e: React.SyntheticEvent<Element>, data: ResizeCallbackData) => void;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export default ResizableHeader;
|
||||
51
frontend/src/components/ResizeTable/ResizeTable.tsx
Normal file
51
frontend/src/components/ResizeTable/ResizeTable.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Table } from 'antd';
|
||||
import type { TableProps } from 'antd/es/table';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { ResizeCallbackData } from 'react-resizable';
|
||||
|
||||
import ResizableHeader from './ResizableHeader';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
||||
const [columnsData, setColumns] = useState<ColumnsType>(columns || []);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(index: number) => (
|
||||
_e: React.SyntheticEvent<Element>,
|
||||
{ size }: ResizeCallbackData,
|
||||
): void => {
|
||||
const newColumns = [...columnsData];
|
||||
newColumns[index] = {
|
||||
...newColumns[index],
|
||||
width: size.width,
|
||||
};
|
||||
setColumns(newColumns);
|
||||
},
|
||||
[columnsData],
|
||||
);
|
||||
|
||||
const mergeColumns = useMemo(
|
||||
() =>
|
||||
columnsData.map((col, index) => ({
|
||||
...col,
|
||||
onHeaderCell: (column: ColumnsType<unknown>[number]): unknown => ({
|
||||
width: column.width,
|
||||
onResize: handleResize(index),
|
||||
}),
|
||||
})),
|
||||
[columnsData, handleResize],
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...restprops}
|
||||
components={{ header: { cell: ResizableHeader } }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
columns={mergeColumns as ColumnsType<any>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizeTable;
|
||||
1
frontend/src/components/ResizeTable/config.ts
Normal file
1
frontend/src/components/ResizeTable/config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const enableUserSelectHack = { enableUserSelectHack: false };
|
||||
4
frontend/src/components/ResizeTable/index.ts
Normal file
4
frontend/src/components/ResizeTable/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ResizableHeader from './ResizableHeader';
|
||||
import ResizeTable from './ResizeTable';
|
||||
|
||||
export { ResizableHeader, ResizeTable };
|
||||
11
frontend/src/components/ResizeTable/styles.ts
Normal file
11
frontend/src/components/ResizeTable/styles.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const SpanStyle = styled.span`
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
`;
|
||||
@@ -4,9 +4,9 @@ import React from 'react';
|
||||
|
||||
import { SpinerStyle } from './styles';
|
||||
|
||||
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
|
||||
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
|
||||
return (
|
||||
<SpinerStyle height={height}>
|
||||
<SpinerStyle height={height} style={style}>
|
||||
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
||||
</SpinerStyle>
|
||||
);
|
||||
@@ -16,11 +16,13 @@ interface SpinnerProps {
|
||||
size?: SpinProps['size'];
|
||||
tip?: SpinProps['tip'];
|
||||
height?: React.CSSProperties['height'];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
Spinner.defaultProps = {
|
||||
size: undefined,
|
||||
tip: undefined,
|
||||
height: undefined,
|
||||
style: {},
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
||||
@@ -6,18 +6,16 @@ import React from 'react';
|
||||
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
||||
return (
|
||||
<Tooltip
|
||||
overlay={(): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||
here
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
overlay={(): JSX.Element => (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||
here
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -8,7 +8,6 @@ export const TextContainer = styled.div<TextContainerProps>`
|
||||
display: flex;
|
||||
|
||||
> button {
|
||||
margin-left: ${({ noButtonMargin }): string => {
|
||||
return noButtonMargin ? '0' : '0.5rem';
|
||||
}}
|
||||
margin-left: ${({ noButtonMargin }): string =>
|
||||
noButtonMargin ? '0' : '0.5rem'}
|
||||
`;
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum LOCALSTORAGE {
|
||||
IS_LOGGED_IN = 'IS_LOGGED_IN',
|
||||
AUTH_TOKEN = 'AUTH_TOKEN',
|
||||
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
|
||||
THEME = 'THEME',
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
|
||||
{
|
||||
label: 'IN',
|
||||
metricValue: '=~',
|
||||
traceValue: 'in',
|
||||
traceValue: 'In',
|
||||
},
|
||||
{
|
||||
label: 'Not IN',
|
||||
metricValue: '!~',
|
||||
traceValue: 'not in',
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
];
|
||||
|
||||
43
frontend/src/constants/theme.ts
Normal file
43
frontend/src/constants/theme.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
const themeColors = {
|
||||
chartcolors: {
|
||||
dodgerBlue: '#2F80ED',
|
||||
mediumOrchid: '#BB6BD9',
|
||||
seaBuckthorn: '#F2994A',
|
||||
seaGreen: '#219653',
|
||||
turquoiseBlue: '#56CCF2',
|
||||
festivalOrange: '#F2C94C',
|
||||
silver: '#BDBDBD',
|
||||
outrageousOrange: '#FF6633',
|
||||
roseBud: '#FFB399',
|
||||
magentaPink: '#FF33FF',
|
||||
canary: '#FFFF99',
|
||||
deepSkyBlue: '#00B3E6',
|
||||
goldTips: '#E6B333',
|
||||
royalBlue: '#3366E6',
|
||||
avocado: '#999966',
|
||||
mintGreen: '#99FF99',
|
||||
chestnut: '#B34D4D',
|
||||
lima: '#80B300',
|
||||
olive: '#809900',
|
||||
beautyBush: '#E6B3B3',
|
||||
danube: '#6680B3',
|
||||
oliveDrab: '#66991A',
|
||||
lavenderRose: '#FF99E6',
|
||||
electricLime: '#CCFF1A',
|
||||
radicalRed: '#FF1A66',
|
||||
harleyOrange: '#E6331A',
|
||||
turquoise: '#33FFCC',
|
||||
gladeGreen: '#66994D',
|
||||
hemlock: '#66664D',
|
||||
vidaLoca: '#4D8000',
|
||||
rust: '#B33300',
|
||||
},
|
||||
errorColor: '#d32f2f',
|
||||
royalGrey: '#888888',
|
||||
matterhornGrey: '#555555',
|
||||
whiteCream: '#ffffffd5',
|
||||
black: '#000000',
|
||||
lightgrey: '#ddd',
|
||||
};
|
||||
|
||||
export { themeColors };
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Button, notification, Table } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,7 +18,7 @@ import Delete from './Delete';
|
||||
|
||||
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const { notifications } = useNotifications();
|
||||
const [channels, setChannels] = useState<Channels[]>(allChannels);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [action] = useComponentPermission(['new_alert_action'], role);
|
||||
@@ -34,11 +36,13 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
title: t('column_channel_name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: t('column_channel_type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -48,6 +52,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (id: string): JSX.Element => (
|
||||
<>
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
@@ -59,13 +64,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Table rowKey="id" dataSource={channels} columns={columns} />
|
||||
</>
|
||||
);
|
||||
return <ResizeTable columns={columns} dataSource={channels} rowKey="id" />;
|
||||
}
|
||||
|
||||
interface AlertChannelsProps {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from 'antd';
|
||||
import { NotificationInstance } from 'antd/lib/notification';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteChannel from 'api/channels/delete';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -3,20 +3,21 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
notification,
|
||||
Space,
|
||||
Table,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||
import getAll from 'api/errors/getAll';
|
||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import ROUTES from 'constants/routes';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
@@ -30,6 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FilterDropdownExtendsProps } from './types';
|
||||
import {
|
||||
extractFilterValues,
|
||||
getDefaultFilterValue,
|
||||
@@ -125,14 +127,15 @@ function AllErrors(): JSX.Element {
|
||||
enabled: !loading,
|
||||
},
|
||||
]);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.error) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: data.error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
}, [data?.error, data?.payload, t]);
|
||||
}, [data?.error, data?.payload, t, notifications]);
|
||||
|
||||
const getDateValue = (value: string): JSX.Element => (
|
||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||
@@ -176,41 +179,45 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const filterDropdownWrapper = useCallback(
|
||||
({ setSelectedKeys, selectedKeys, confirm, placeholder, filterKey }) => {
|
||||
return (
|
||||
<Card size="small">
|
||||
<Space align="start" direction="vertical">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e): void =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
allowClear
|
||||
defaultValue={getDefaultFilterValue(
|
||||
filterKey,
|
||||
getUpdatedServiceName,
|
||||
getUpdatedExceptionType,
|
||||
)}
|
||||
onPressEnter={handleSearch(confirm, selectedKeys[0], filterKey)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSearch(confirm, selectedKeys[0], filterKey)}
|
||||
icon={<SearchOutlined />}
|
||||
size="small"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
placeholder,
|
||||
filterKey,
|
||||
}: FilterDropdownExtendsProps) => (
|
||||
<Card size="small">
|
||||
<Space align="start" direction="vertical">
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e): void =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
allowClear
|
||||
defaultValue={getDefaultFilterValue(
|
||||
filterKey,
|
||||
getUpdatedServiceName,
|
||||
getUpdatedExceptionType,
|
||||
)}
|
||||
onPressEnter={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
|
||||
icon={<SearchOutlined />}
|
||||
size="small"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
),
|
||||
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
|
||||
);
|
||||
|
||||
const onExceptionTypeFilter = useCallback(
|
||||
(value, record: Exception): boolean => {
|
||||
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
|
||||
(value: unknown, record: Exception): boolean => {
|
||||
if (record.exceptionType && typeof value === 'string') {
|
||||
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
@@ -220,7 +227,7 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const onApplicationTypeFilter = useCallback(
|
||||
(value, record: Exception): boolean => {
|
||||
(value: unknown, record: Exception): boolean => {
|
||||
if (record.serviceName && typeof value === 'string') {
|
||||
return record.serviceName.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
@@ -252,6 +259,7 @@ function AllErrors(): JSX.Element {
|
||||
const columns: ColumnsType<Exception> = [
|
||||
{
|
||||
title: 'Exception Type',
|
||||
width: 100,
|
||||
dataIndex: 'exceptionType',
|
||||
key: 'exceptionType',
|
||||
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
|
||||
@@ -277,6 +285,7 @@ function AllErrors(): JSX.Element {
|
||||
title: 'Error Message',
|
||||
dataIndex: 'exceptionMessage',
|
||||
key: 'exceptionMessage',
|
||||
width: 100,
|
||||
render: (value): JSX.Element => (
|
||||
<Tooltip overlay={(): JSX.Element => value}>
|
||||
<Typography.Paragraph
|
||||
@@ -291,6 +300,7 @@ function AllErrors(): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Count',
|
||||
width: 50,
|
||||
dataIndex: 'exceptionCount',
|
||||
key: 'exceptionCount',
|
||||
sorter: true,
|
||||
@@ -303,6 +313,7 @@ function AllErrors(): JSX.Element {
|
||||
{
|
||||
title: 'Last Seen',
|
||||
dataIndex: 'lastSeen',
|
||||
width: 80,
|
||||
key: 'lastSeen',
|
||||
render: getDateValue,
|
||||
sorter: true,
|
||||
@@ -315,6 +326,7 @@ function AllErrors(): JSX.Element {
|
||||
{
|
||||
title: 'First Seen',
|
||||
dataIndex: 'firstSeen',
|
||||
width: 80,
|
||||
key: 'firstSeen',
|
||||
render: getDateValue,
|
||||
sorter: true,
|
||||
@@ -327,6 +339,7 @@ function AllErrors(): JSX.Element {
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
width: 100,
|
||||
key: 'serviceName',
|
||||
sorter: true,
|
||||
defaultSortOrder: getDefaultOrder(
|
||||
@@ -343,7 +356,11 @@ function AllErrors(): JSX.Element {
|
||||
];
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
||||
(paginations, filters, sorter) => {
|
||||
(
|
||||
paginations: TablePaginationConfig,
|
||||
filters: Record<string, FilterValue | null>,
|
||||
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
|
||||
) => {
|
||||
if (!Array.isArray(sorter)) {
|
||||
const { pageSize = 0, current = 0 } = paginations;
|
||||
const { columnKey = '', order } = sorter;
|
||||
@@ -369,10 +386,10 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={data?.payload as Exception[]}
|
||||
columns={columns}
|
||||
rowKey="firstSeen"
|
||||
loading={isLoading || false || errorCountResponse.status === 'loading'}
|
||||
pagination={{
|
||||
|
||||
9
frontend/src/container/AllError/types.ts
Normal file
9
frontend/src/container/AllError/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { FilterDropdownProps } from 'antd/es/table/interface';
|
||||
|
||||
export interface FilterDropdownExtendsProps {
|
||||
placeholder: string;
|
||||
filterKey: string;
|
||||
confirm: FilterDropdownProps['confirm'];
|
||||
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
|
||||
selectedKeys: FilterDropdownProps['selectedKeys'];
|
||||
}
|
||||
@@ -20,15 +20,14 @@ export const urlKey = {
|
||||
serviceName: 'serviceName',
|
||||
};
|
||||
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
||||
return !!(
|
||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
|
||||
!!(
|
||||
orderBy === 'serviceName' ||
|
||||
orderBy === 'exceptionCount' ||
|
||||
orderBy === 'lastSeen' ||
|
||||
orderBy === 'firstSeen' ||
|
||||
orderBy === 'exceptionType'
|
||||
);
|
||||
};
|
||||
|
||||
export const getOrder = (order: string | null): Order => {
|
||||
if (isOrder(order)) {
|
||||
@@ -82,12 +81,9 @@ export const getDefaultOrder = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getNanoSeconds = (date: string): string => {
|
||||
return (
|
||||
Math.floor(new Date(date).getTime() / 1e3).toString() +
|
||||
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0')
|
||||
);
|
||||
};
|
||||
export const getNanoSeconds = (date: string): string =>
|
||||
Math.floor(new Date(date).getTime() / 1e3).toString() +
|
||||
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0');
|
||||
|
||||
export const getUpdatePageSize = (pageSize: string | null): number => {
|
||||
if (pageSize) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { notification } from 'antd';
|
||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
@@ -6,6 +5,7 @@ import getUserVersion from 'api/user/getVersion';
|
||||
import Header from 'container/Header';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { ReactNode, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
@@ -91,6 +91,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const latestVersionCounter = useRef(0);
|
||||
const latestConfigCounter = useRef(0);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getUserLatestVersionResponse.isFetched &&
|
||||
@@ -105,7 +107,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isError: true,
|
||||
},
|
||||
});
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: t('oops_something_went_wrong_version'),
|
||||
});
|
||||
}
|
||||
@@ -123,7 +125,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isError: true,
|
||||
},
|
||||
});
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: t('oops_something_went_wrong_version'),
|
||||
});
|
||||
}
|
||||
@@ -219,6 +221,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getDynamicConfigsResponse.data,
|
||||
getDynamicConfigsResponse.isFetched,
|
||||
getDynamicConfigsResponse.isSuccess,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
||||
@@ -3,9 +3,10 @@ import styled from 'styled-components';
|
||||
|
||||
export const Layout = styled(LayoutComponent)`
|
||||
&&& {
|
||||
min-height: 92vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: calc(100vh - 4rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Menu, Space } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React, { Suspense, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import ErrorLink from './ErrorLink';
|
||||
import LinkContainer from './Link';
|
||||
import { MenuItem } from './styles';
|
||||
|
||||
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||
const sortedConfig = useMemo(
|
||||
@@ -15,10 +14,10 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||
[config.components],
|
||||
);
|
||||
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<Menu.ItemGroup>
|
||||
<Menu>
|
||||
{sortedConfig.map((item) => {
|
||||
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
||||
|
||||
@@ -28,19 +27,19 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||
return (
|
||||
<ErrorLink key={item.text + item.href}>
|
||||
<Suspense fallback={<Spinner height="5vh" />}>
|
||||
<Menu.Item>
|
||||
<MenuItem>
|
||||
<LinkContainer href={item.href}>
|
||||
<Space size="small" align="start">
|
||||
<Component />
|
||||
{item.text}
|
||||
</Space>
|
||||
</LinkContainer>
|
||||
</Menu.Item>
|
||||
</MenuItem>
|
||||
</Suspense>
|
||||
</ErrorLink>
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal file
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Menu } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MenuItem = styled(Menu.Item)`
|
||||
&&& {
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -15,9 +16,8 @@ import HelpToolTip from './Config';
|
||||
function DynamicConfigDropdown({
|
||||
frontendId,
|
||||
}: DynamicConfigDropdownProps): JSX.Element {
|
||||
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
|
||||
|
||||
const config = useMemo(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import createPagerApi from 'api/channels/createPager';
|
||||
import createSlackApi from 'api/channels/createSlack';
|
||||
import createWebhookApi from 'api/channels/createWebhook';
|
||||
@@ -7,6 +7,7 @@ import testSlackApi from 'api/channels/testSlack';
|
||||
import testWebhookApi from 'api/channels/testWebhook';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -58,7 +59,7 @@ function CreateAlertChannels({
|
||||
});
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [testingState, setTestingState] = useState<boolean>(false);
|
||||
const [notifications, NotificationElement] = notification.useNotification();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const [type, setType] = useState<ChannelType>(preType);
|
||||
const onTypeChangeHandler = useCallback(
|
||||
@@ -78,16 +79,17 @@ function CreateAlertChannels({
|
||||
[type, selectedConfig],
|
||||
);
|
||||
|
||||
const prepareSlackRequest = useCallback(() => {
|
||||
return {
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
};
|
||||
}, [selectedConfig]);
|
||||
}),
|
||||
[selectedConfig],
|
||||
);
|
||||
|
||||
const onSlackHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
@@ -335,7 +337,6 @@ function CreateAlertChannels({
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
testingState,
|
||||
NotificationElement,
|
||||
title: t('page_title_create'),
|
||||
initialValue: {
|
||||
type,
|
||||
|
||||
@@ -6,6 +6,16 @@ import {
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
|
||||
const defaultAlertDescription =
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
|
||||
const defaultAlertSummary =
|
||||
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}';
|
||||
|
||||
const defaultAnnotations = {
|
||||
description: defaultAlertDescription,
|
||||
summary: defaultAlertSummary,
|
||||
};
|
||||
|
||||
export const alertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
condition: {
|
||||
@@ -38,9 +48,7 @@ export const alertDefaults: AlertDef = {
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new alert',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
@@ -85,9 +93,7 @@ export const logAlertDefaults: AlertDef = {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/logs`,
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new log-based alert',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
@@ -132,9 +138,7 @@ export const traceAlertDefaults: AlertDef = {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/traces`,
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new trace-based alert',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
@@ -179,8 +183,6 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/exceptions`,
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new exceptions-based alert',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import editPagerApi from 'api/channels/editPager';
|
||||
import editSlackApi from 'api/channels/editSlack';
|
||||
import editWebhookApi from 'api/channels/editWebhook';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
WebhookType,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -36,7 +37,7 @@ function EditAlertChannels({
|
||||
});
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [testingState, setTestingState] = useState<boolean>(false);
|
||||
const [notifications, NotificationElement] = notification.useNotification();
|
||||
const { notifications } = useNotifications();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const [type, setType] = useState<ChannelType>(
|
||||
@@ -47,8 +48,8 @@ function EditAlertChannels({
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
const prepareSlackRequest = useCallback(() => {
|
||||
return {
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
@@ -56,8 +57,9 @@ function EditAlertChannels({
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
id,
|
||||
};
|
||||
}, [id, selectedConfig]);
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onSlackEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
@@ -143,8 +145,8 @@ function EditAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||
|
||||
const preparePagerRequest = useCallback(() => {
|
||||
return {
|
||||
const preparePagerRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig.name || '',
|
||||
routing_key: selectedConfig.routing_key,
|
||||
client: selectedConfig.client,
|
||||
@@ -157,8 +159,9 @@ function EditAlertChannels({
|
||||
details: selectedConfig.details,
|
||||
detailsArray: JSON.parse(selectedConfig.details || '{}'),
|
||||
id,
|
||||
};
|
||||
}, [id, selectedConfig]);
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onPagerEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
@@ -279,7 +282,6 @@ function EditAlertChannels({
|
||||
onSaveHandler,
|
||||
testingState,
|
||||
savingState,
|
||||
NotificationElement,
|
||||
title: t('page_title_edit'),
|
||||
initialValue,
|
||||
editing: true,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
|
||||
import { Button, Divider, Space, Typography } from 'antd';
|
||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||
import Editor from 'components/Editor';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { getNanoSeconds } from 'container/AllError/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
@@ -53,12 +55,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
() => [
|
||||
{
|
||||
title: 'Key',
|
||||
width: 100,
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value',
|
||||
width: 100,
|
||||
key: 'value',
|
||||
},
|
||||
],
|
||||
@@ -77,13 +81,15 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onClickErrorIdHandler = async (
|
||||
id: string,
|
||||
timestamp: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (id.length === 0) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error Id cannot be empty',
|
||||
});
|
||||
return;
|
||||
@@ -95,7 +101,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
}×tamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
@@ -167,7 +173,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
|
||||
<EditorContainer>
|
||||
<Space direction="vertical">
|
||||
<Table tableLayout="fixed" columns={columns} dataSource={data} />
|
||||
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
|
||||
</Space>
|
||||
</EditorContainer>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input, Select } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Input, Select } from 'antd';
|
||||
import { LabelFilterStatement } from 'container/CreateAlertChannels/config';
|
||||
import React from 'react';
|
||||
|
||||
@@ -10,7 +9,7 @@ const { Option } = Select;
|
||||
// point
|
||||
function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
|
||||
return (
|
||||
<FormItem name="label_filter" label="Notify When (Optional)">
|
||||
<Form.Item name="label_filter" label="Notify When (Optional)">
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
defaultValue="Severity"
|
||||
@@ -51,7 +50,7 @@ function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
</Input.Group>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Input } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -11,7 +10,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
const { t } = useTranslation('channels');
|
||||
return (
|
||||
<>
|
||||
<FormItem name="routing_key" label={t('field_pager_routing_key')} required>
|
||||
<Form.Item name="routing_key" label={t('field_pager_routing_key')} required>
|
||||
<Input
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
@@ -20,9 +19,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="description"
|
||||
help={t('help_pager_description')}
|
||||
label={t('field_pager_description')}
|
||||
@@ -38,9 +37,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}
|
||||
placeholder={t('placeholder_pager_description')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="severity"
|
||||
help={t('help_pager_severity')}
|
||||
label={t('field_pager_severity')}
|
||||
@@ -53,9 +52,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="details"
|
||||
help={t('help_pager_details')}
|
||||
label={t('field_pager_details')}
|
||||
@@ -69,9 +68,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="component"
|
||||
help={t('help_pager_component')}
|
||||
label={t('field_pager_component')}
|
||||
@@ -84,9 +83,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="group"
|
||||
help={t('help_pager_group')}
|
||||
label={t('field_pager_group')}
|
||||
@@ -99,9 +98,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="class"
|
||||
help={t('help_pager_class')}
|
||||
label={t('field_pager_class')}
|
||||
@@ -114,8 +113,8 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="client"
|
||||
help={t('help_pager_client')}
|
||||
label={t('field_pager_client')}
|
||||
@@ -128,9 +127,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="client_url"
|
||||
help={t('help_pager_client_url')}
|
||||
label={t('field_pager_client_url')}
|
||||
@@ -143,7 +142,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Input } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -12,7 +11,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormItem name="api_url" label={t('field_webhook_url')}>
|
||||
<Form.Item name="api_url" label={t('field_webhook_url')}>
|
||||
<Input
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
@@ -21,9 +20,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem
|
||||
<Form.Item
|
||||
name="channel"
|
||||
help={t('slack_channel_help')}
|
||||
label={t('field_slack_recipient')}
|
||||
@@ -36,9 +35,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem name="title" label={t('field_slack_title')}>
|
||||
<Form.Item name="title" label={t('field_slack_title')}>
|
||||
<TextArea
|
||||
rows={4}
|
||||
// value={`[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}\n{{- if gt (len .CommonLabels) (len .GroupLabels) -}}\n{{\" \"}}(\n{{- with .CommonLabels.Remove .GroupLabels.Names }}\n {{- range $index, $label := .SortedPairs -}}\n {{ if $index }}, {{ end }}\n {{- $label.Name }}=\"{{ $label.Value -}}\"\n {{- end }}\n{{- end -}}\n)\n{{- end }}`}
|
||||
@@ -49,9 +48,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem name="text" label={t('field_slack_description')}>
|
||||
<Form.Item name="text" label={t('field_slack_description')}>
|
||||
<TextArea
|
||||
onChange={(event): void =>
|
||||
setSelectedConfig((value) => ({
|
||||
@@ -61,7 +60,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
|
||||
}
|
||||
placeholder={t('placeholder_slack_description')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Input } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -10,7 +9,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormItem name="api_url" label={t('field_webhook_url')}>
|
||||
<Form.Item name="api_url" label={t('field_webhook_url')}>
|
||||
<Input
|
||||
onChange={(event): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
@@ -19,8 +18,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={t('field_webhook_username')}
|
||||
help={t('help_webhook_username')}
|
||||
@@ -33,8 +32,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="Password (optional)"
|
||||
help={t('help_webhook_password')}
|
||||
@@ -48,7 +47,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Form, FormInstance, Input, Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Store } from 'antd/lib/form/interface';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
@@ -32,7 +31,6 @@ function FormAlertChannels({
|
||||
onSaveHandler,
|
||||
savingState,
|
||||
testingState,
|
||||
NotificationElement,
|
||||
title,
|
||||
initialValue,
|
||||
editing = false,
|
||||
@@ -54,12 +52,10 @@ function FormAlertChannels({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{NotificationElement}
|
||||
|
||||
<Title level={3}>{title}</Title>
|
||||
|
||||
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
|
||||
<FormItem label={t('field_channel_name')} labelAlign="left" name="name">
|
||||
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
|
||||
<Input
|
||||
disabled={editing}
|
||||
onChange={(event): void => {
|
||||
@@ -69,9 +65,9 @@ function FormAlertChannels({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem label={t('field_channel_type')} labelAlign="left" name="type">
|
||||
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
||||
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
||||
<Option value="slack" key="slack">
|
||||
Slack
|
||||
@@ -83,11 +79,11 @@ function FormAlertChannels({
|
||||
Pagerduty
|
||||
</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem>{renderSettings()}</FormItem>
|
||||
<Form.Item>{renderSettings()}</Form.Item>
|
||||
|
||||
<FormItem>
|
||||
<Form.Item>
|
||||
<Button
|
||||
disabled={savingState}
|
||||
loading={savingState}
|
||||
@@ -110,7 +106,7 @@ function FormAlertChannels({
|
||||
>
|
||||
{t('button_return')}
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
@@ -127,10 +123,6 @@ interface FormAlertChannelsProps {
|
||||
onTestHandler: (props: ChannelType) => void;
|
||||
testingState: boolean;
|
||||
savingState: boolean;
|
||||
NotificationElement: React.ReactElement<
|
||||
unknown,
|
||||
string | React.JSXElementConstructor<unknown>
|
||||
>;
|
||||
title: string;
|
||||
initialValue: Store;
|
||||
// editing indicates if the form is opened in edit mode
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Select } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
@@ -31,7 +30,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
<Form.Item
|
||||
label={t('field_severity')}
|
||||
labelAlign="left"
|
||||
name={['labels', 'severity']}
|
||||
@@ -54,9 +53,9 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
<Option value="warning">{t('option_warning')}</Option>
|
||||
<Option value="info">{t('option_info')}</Option>
|
||||
</SeveritySelect>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
|
||||
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||
<InputSmall
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
@@ -65,8 +64,8 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('field_alert_desc')}
|
||||
labelAlign="left"
|
||||
name={['annotations', 'description']}
|
||||
@@ -82,7 +81,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form.Item>
|
||||
<FormItemMedium label={t('field_labels')}>
|
||||
<LabelSelect
|
||||
onSetLabels={(l: Labels): void => {
|
||||
|
||||
@@ -32,6 +32,8 @@ export const rawQueryToIChQuery = (
|
||||
// ClickHouseQueryBuilder format. The main difference is
|
||||
// use of rawQuery (in ClickHouseQueryBuilder)
|
||||
// and query (in alert builder)
|
||||
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => {
|
||||
return { ...src, name: 'A', rawQuery: src.query };
|
||||
};
|
||||
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => ({
|
||||
...src,
|
||||
name: 'A',
|
||||
rawQuery: src.query,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notification, Select } from 'antd';
|
||||
import { Select } from 'antd';
|
||||
import getChannels from 'api/channels/getAll';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -20,12 +21,14 @@ function ChannelSelect({
|
||||
|
||||
const { loading, payload, error, errorMessage } = useFetch(getChannels);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleChange = (value: string[]): void => {
|
||||
onSelectChannels(value);
|
||||
};
|
||||
|
||||
if (error && errorMessage !== '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: errorMessage,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, notification, Tabs } from 'antd';
|
||||
import { Button, 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 { useNotifications } from 'hooks/useNotifications';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -163,10 +164,11 @@ function QuerySection({
|
||||
...allQueries,
|
||||
});
|
||||
};
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const addMetricQuery = useCallback(() => {
|
||||
if (Object.keys(metricQueries).length > 5) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: t('metric_query_max_limit'),
|
||||
});
|
||||
return;
|
||||
@@ -191,7 +193,7 @@ function QuerySection({
|
||||
expression: queryLabel,
|
||||
};
|
||||
setMetricQueries({ ...queries });
|
||||
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
|
||||
}, [t, getNextQueryLabel, metricQueries, setMetricQueries, notifications]);
|
||||
|
||||
const addFormula = useCallback(() => {
|
||||
// defaulting to F1 as only one formula is supported
|
||||
@@ -211,78 +213,70 @@ function QuerySection({
|
||||
setFormulaQueries({ ...formulas });
|
||||
}, [formulaQueries, setFormulaQueries]);
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => {
|
||||
return (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
};
|
||||
const renderPromqlUI = (): JSX.Element => (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
|
||||
const renderChQueryUI = (): JSX.Element => {
|
||||
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
|
||||
};
|
||||
const renderChQueryUI = (): JSX.Element => (
|
||||
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
|
||||
);
|
||||
|
||||
const renderFormulaButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
const renderFormulaButton = (): JSX.Element => (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
|
||||
const renderQueryButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
const renderQueryButton = (): JSX.Element => (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
|
||||
const renderMetricUI = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{metricQueries &&
|
||||
Object.keys(metricQueries).map((key: string) => {
|
||||
const renderMetricUI = (): JSX.Element => (
|
||||
<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 = metricQueries[key];
|
||||
const current = formulaQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilder
|
||||
<MetricsBuilderFormula
|
||||
key={key}
|
||||
queryIndex={key}
|
||||
queryData={toIMetricsBuilderQuery(current)}
|
||||
selectedGraph="TIME_SERIES"
|
||||
handleQueryChange={handleMetricQueryChange}
|
||||
formulaIndex={key}
|
||||
formulaData={current}
|
||||
handleFormulaChange={handleFormulaChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{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>
|
||||
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||
metricQueries &&
|
||||
Object.keys(metricQueries).length > 0 &&
|
||||
renderFormulaButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
runQuery();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import { Form, Select, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
const FormItem = Form.Item;
|
||||
|
||||
function RuleOptions({
|
||||
alertDef,
|
||||
@@ -38,106 +38,94 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
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) || '';
|
||||
const renderCompareOps = (): JSX.Element => (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
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 renderThresholdMatchOpts = (): JSX.Element => (
|
||||
<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 renderPromMatchOpts = (): JSX.Element => (
|
||||
<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 renderEvalWindows = (): JSX.Element => (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
const renderPromRuleOptions = (): JSX.Element => (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderPromMatchOpts()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -15,154 +15,130 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const renderStep1QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep2QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep1QB = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
const renderStep2QB = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderStep3QB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep3QB = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderGuideForQB = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1QB()}
|
||||
{renderStep2QB()}
|
||||
{renderStep3QB()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep1PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep2PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderGuideForQB = (): JSX.Element => (
|
||||
<>
|
||||
{renderStep1QB()}
|
||||
{renderStep2QB()}
|
||||
{renderStep3QB()}
|
||||
</>
|
||||
);
|
||||
const renderStep1PQL = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
const renderStep2PQL = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderStep3PQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep3PQL = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderGuideForPQL = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1PQL()}
|
||||
{renderStep2PQL()}
|
||||
{renderStep3PQL()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderGuideForPQL = (): JSX.Element => (
|
||||
<>
|
||||
{renderStep1PQL()}
|
||||
{renderStep2PQL()}
|
||||
{renderStep3PQL()}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderStep1CH = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>
|
||||
<Trans
|
||||
i18nKey="user_guide_ch_step1a"
|
||||
t={t}
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
|
||||
<a
|
||||
key={1}
|
||||
target="_blank"
|
||||
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep2CH = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep1CH = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>
|
||||
<Trans
|
||||
i18nKey="user_guide_ch_step1a"
|
||||
t={t}
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
|
||||
<a
|
||||
key={1}
|
||||
target="_blank"
|
||||
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
const renderStep2CH = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderStep3CH = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderStep3CH = (): JSX.Element => (
|
||||
<>
|
||||
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
|
||||
<StyledList>
|
||||
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
|
||||
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderGuideForCH = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{renderStep1CH()}
|
||||
{renderStep2CH()}
|
||||
{renderStep3CH()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const renderGuideForCH = (): JSX.Element => (
|
||||
<>
|
||||
{renderStep1CH()}
|
||||
{renderStep2CH()}
|
||||
{renderStep3CH()}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<Row>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { FormInstance, Modal, notification, Typography } from 'antd';
|
||||
import { Col, FormInstance, Modal, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
MainFormContainer,
|
||||
PanelContainer,
|
||||
StyledLeftContainer,
|
||||
StyledRightContainer,
|
||||
} from './styles';
|
||||
import useDebounce from './useDebounce';
|
||||
import UserGuide from './UserGuide';
|
||||
@@ -191,12 +191,14 @@ function FormAlertRules({
|
||||
});
|
||||
}
|
||||
};
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const validatePromParams = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
if (queryCategory !== EQueryType.PROM) return retval;
|
||||
|
||||
if (!promQueries || Object.keys(promQueries).length === 0) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('promql_required'),
|
||||
});
|
||||
@@ -205,7 +207,7 @@ function FormAlertRules({
|
||||
|
||||
Object.keys(promQueries).forEach((key) => {
|
||||
if (promQueries[key].query === '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('promql_required'),
|
||||
});
|
||||
@@ -214,14 +216,14 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
return retval;
|
||||
}, [t, promQueries, queryCategory]);
|
||||
}, [t, promQueries, queryCategory, notifications]);
|
||||
|
||||
const validateChQueryParams = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
if (queryCategory !== EQueryType.CLICKHOUSE) return retval;
|
||||
|
||||
if (!chQueries || Object.keys(chQueries).length === 0) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('chquery_required'),
|
||||
});
|
||||
@@ -230,7 +232,7 @@ function FormAlertRules({
|
||||
|
||||
Object.keys(chQueries).forEach((key) => {
|
||||
if (chQueries[key].rawQuery === '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('chquery_required'),
|
||||
});
|
||||
@@ -239,14 +241,14 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
return retval;
|
||||
}, [t, chQueries, queryCategory]);
|
||||
}, [t, chQueries, queryCategory, notifications]);
|
||||
|
||||
const validateQBParams = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
if (queryCategory !== EQueryType.QUERY_BUILDER) return true;
|
||||
|
||||
if (!metricQueries || Object.keys(metricQueries).length === 0) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('condition_required'),
|
||||
});
|
||||
@@ -254,7 +256,7 @@ function FormAlertRules({
|
||||
}
|
||||
|
||||
if (!alertDef.condition?.target) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('target_missing'),
|
||||
});
|
||||
@@ -263,7 +265,7 @@ function FormAlertRules({
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
if (metricQueries[key].metricName === '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('metricname_missing', { where: metricQueries[key].name }),
|
||||
});
|
||||
@@ -273,7 +275,7 @@ function FormAlertRules({
|
||||
|
||||
Object.keys(formulaQueries).forEach((key) => {
|
||||
if (formulaQueries[key].expression === '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('expression_missing', formulaQueries[key].name),
|
||||
});
|
||||
@@ -281,11 +283,11 @@ function FormAlertRules({
|
||||
}
|
||||
});
|
||||
return retval;
|
||||
}, [t, alertDef, queryCategory, metricQueries, formulaQueries]);
|
||||
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, notifications]);
|
||||
|
||||
const isFormValid = useCallback((): boolean => {
|
||||
if (!alertDef.alert || alertDef.alert === '') {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('alertname_required'),
|
||||
});
|
||||
@@ -301,7 +303,14 @@ function FormAlertRules({
|
||||
}
|
||||
|
||||
return validateQBParams();
|
||||
}, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
|
||||
}, [
|
||||
t,
|
||||
validateQBParams,
|
||||
validateChQueryParams,
|
||||
alertDef,
|
||||
validatePromParams,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
const preparePostData = (): AlertDef => {
|
||||
const postableAlert: AlertDef = {
|
||||
@@ -349,7 +358,7 @@ function FormAlertRules({
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description:
|
||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||
@@ -362,19 +371,26 @@ function FormAlertRules({
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [t, isFormValid, ruleId, ruleCache, memoizedPreparePostData]);
|
||||
}, [
|
||||
t,
|
||||
isFormValid,
|
||||
ruleId,
|
||||
ruleCache,
|
||||
memoizedPreparePostData,
|
||||
notifications,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
@@ -408,70 +424,64 @@ function FormAlertRules({
|
||||
if (response.statusCode === 200) {
|
||||
const { payload } = response;
|
||||
if (payload?.alertCount === 0) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('no_alerts_found'),
|
||||
});
|
||||
} else {
|
||||
notification.success({
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('rule_test_fired'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [t, isFormValid, memoizedPreparePostData]);
|
||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||
|
||||
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 renderQBChartPreview = (): JSX.Element => (
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderPromChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderChQueryChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
userQueryKey={runQueryId}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
userQueryKey={runQueryId}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
@@ -535,9 +545,9 @@ function FormAlertRules({
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</StyledLeftContainer>
|
||||
<StyledRightContainer flex="1 1 300px">
|
||||
<Col flex="1 1 300px">
|
||||
<UserGuide queryType={queryCategory} />
|
||||
</StyledRightContainer>
|
||||
</Col>
|
||||
</PanelContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,11 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, Input, message, Modal } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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';
|
||||
@@ -29,7 +27,8 @@ function LabelSelect({
|
||||
initialValues,
|
||||
}: LabelSelectProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [currentVal, setCurrentVal] = useState('');
|
||||
const [staging, setStaging] = useState<string[]>([]);
|
||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||
@@ -120,17 +119,15 @@ function LabelSelect({
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
);
|
||||
},
|
||||
(query): JSX.Element => (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
{map(staging, (item) => (
|
||||
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
|
||||
@@ -9,19 +9,15 @@ import {
|
||||
Select,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Item } = Form;
|
||||
|
||||
export const PanelContainer = styled(Row)`
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
export const StyledRightContainer = styled(Col)`
|
||||
&&& {
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledLeftContainer = styled(Col)`
|
||||
&&& {
|
||||
margin-right: 1rem;
|
||||
@@ -99,6 +95,8 @@ export const ThresholdInput = styled(InputNumber)`
|
||||
align-items: center;
|
||||
& > .ant-input-number-group-addon {
|
||||
width: 130px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
& > .ant-input-number {
|
||||
width: 50%;
|
||||
@@ -111,7 +109,7 @@ export const TextareaMedium = styled(TextArea)`
|
||||
width: 70%;
|
||||
`;
|
||||
|
||||
export const FormItemMedium = styled(FormItem)`
|
||||
export const FormItemMedium = styled(Item)`
|
||||
width: 70%;
|
||||
`;
|
||||
|
||||
|
||||
@@ -42,17 +42,15 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
): IMetricsBuilderQuery => ({
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import useThemeMode from 'hooks/useThemeMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React from 'react';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
@@ -14,7 +14,7 @@ interface SpanLengthProps {
|
||||
|
||||
function SpanLength(props: SpanLengthProps): JSX.Element {
|
||||
const { width, leftOffset, bgColor, inMsCount } = props;
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||
return (
|
||||
<SpanWrapper>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
||||
import { Col } from 'antd';
|
||||
import { Col, Typography } from 'antd';
|
||||
import { StyledCol, StyledRow } from 'components/Styled';
|
||||
import { IIntervalUnit } from 'container/TraceDetail/utils';
|
||||
import useThemeMode from 'hooks/useThemeMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { ITraceMetaData } from '..';
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
styles,
|
||||
Wrapper,
|
||||
} from './styles';
|
||||
import { getIconStyles } from './utils';
|
||||
|
||||
function Trace(props: TraceProps): JSX.Element {
|
||||
const {
|
||||
@@ -42,7 +43,8 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
isMissing,
|
||||
} = props;
|
||||
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
|
||||
|
||||
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
|
||||
@@ -111,6 +113,18 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
const width = (value * 1e2) / (globalSpread * 1e6);
|
||||
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
|
||||
|
||||
const iconStyles = useMemo(() => getIconStyles(isDarkMode), [isDarkMode]);
|
||||
|
||||
const icon = useMemo(
|
||||
() =>
|
||||
isOpen ? (
|
||||
<CaretDownFilled style={iconStyles} />
|
||||
) : (
|
||||
<CaretRightFilled style={iconStyles} />
|
||||
),
|
||||
[isOpen, iconStyles],
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
onMouseEnter={onMouseEnterHandler}
|
||||
@@ -136,10 +150,8 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
isDarkMode={isDarkMode}
|
||||
onClick={onClickTreeExpansion}
|
||||
>
|
||||
{totalSpans}
|
||||
<CaretContainer>
|
||||
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
|
||||
</CaretContainer>
|
||||
<Typography>{totalSpans}</Typography>
|
||||
<CaretContainer>{icon}</CaretContainer>
|
||||
</CardComponent>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
|
||||
color: isDarkMode ? 'white' : 'black',
|
||||
});
|
||||
@@ -112,21 +112,19 @@ export const getNodeById = (
|
||||
|
||||
const getSpanWithoutChildren = (
|
||||
span: ITraceTree,
|
||||
): Omit<ITraceTree, 'children'> => {
|
||||
return {
|
||||
id: span.id,
|
||||
name: span.name,
|
||||
parent: span.parent,
|
||||
serviceColour: span.serviceColour,
|
||||
serviceName: span.serviceName,
|
||||
startTime: span.startTime,
|
||||
tags: span.tags,
|
||||
time: span.time,
|
||||
value: span.value,
|
||||
event: span.event,
|
||||
hasError: span.hasError,
|
||||
};
|
||||
};
|
||||
): Omit<ITraceTree, 'children'> => ({
|
||||
id: span.id,
|
||||
name: span.name,
|
||||
parent: span.parent,
|
||||
serviceColour: span.serviceColour,
|
||||
serviceName: span.serviceName,
|
||||
startTime: span.startTime,
|
||||
tags: span.tags,
|
||||
time: span.time,
|
||||
value: span.value,
|
||||
event: span.event,
|
||||
hasError: span.hasError,
|
||||
});
|
||||
|
||||
export const isSpanPresentInSearchString = (
|
||||
searchedString: string,
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Modal,
|
||||
notification,
|
||||
Row,
|
||||
Spin,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
|
||||
import setRetentionApi from 'api/settings/setRetention';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import find from 'lodash-es/find';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -172,6 +163,8 @@ function GeneralSettings({
|
||||
logsTtlValuesPayload.status === 'pending' ? 1000 : null,
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onModalToggleHandler = (type: TTTLType): void => {
|
||||
if (type === 'metrics') setModalMetrics((modal) => !modal);
|
||||
if (type === 'traces') setModalTraces((modal) => !modal);
|
||||
@@ -186,14 +179,14 @@ function GeneralSettings({
|
||||
const onClickSaveHandler = useCallback(
|
||||
(type: TTTLType) => {
|
||||
if (!setRetentionPermission) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: `Sorry you don't have permission to make these changes`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onModalToggleHandler(type);
|
||||
},
|
||||
[setRetentionPermission],
|
||||
[setRetentionPermission, notifications],
|
||||
);
|
||||
|
||||
const s3Enabled = useMemo(
|
||||
@@ -352,7 +345,7 @@ function GeneralSettings({
|
||||
let hasSetTTLFailed = false;
|
||||
if (setTTLResponse.statusCode === 409) {
|
||||
hasSetTTLFailed = true;
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
@@ -390,7 +383,7 @@ function GeneralSettings({
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_failed_message'),
|
||||
placement: 'topRight',
|
||||
@@ -572,7 +565,7 @@ function GeneralSettings({
|
||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
centered
|
||||
visible={category.save.modal}
|
||||
open={category.save.modal}
|
||||
confirmLoading={category.save.apiLoading}
|
||||
>
|
||||
<Typography>
|
||||
@@ -590,20 +583,22 @@ function GeneralSettings({
|
||||
});
|
||||
|
||||
return (
|
||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||
<>
|
||||
{Element}
|
||||
<ErrorTextContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to set retention period`,
|
||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||
}}
|
||||
/>
|
||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||
</ErrorTextContainer>
|
||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||
<ErrorTextContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to set retention period`,
|
||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||
}}
|
||||
/>
|
||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||
</ErrorTextContainer>
|
||||
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
</Col>
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ function GridGraphComponent({
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
}: GridGraphComponentProps): JSX.Element | null {
|
||||
const location = history.location.pathname;
|
||||
|
||||
@@ -38,6 +39,7 @@ function GridGraphComponent({
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -85,6 +87,7 @@ export interface GridGraphComponentProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
GridGraphComponent.defaultProps = {
|
||||
@@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = {
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
staticLine: undefined,
|
||||
onDragSelect: undefined,
|
||||
};
|
||||
|
||||
export default GridGraphComponent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { GraphOnClickHandler } from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
@@ -19,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { NotFoundContainer, TimeContainer } from './styles';
|
||||
import { TimeContainer } from './styles';
|
||||
|
||||
function FullView({
|
||||
widget,
|
||||
@@ -27,6 +27,7 @@ function FullView({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@@ -57,20 +58,23 @@ function FullView({
|
||||
}),
|
||||
);
|
||||
|
||||
const isError = response?.error;
|
||||
const chartDataSet = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: response?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[response],
|
||||
);
|
||||
|
||||
const isLoading = response.isLoading === true;
|
||||
const errorMessage = isError instanceof Error ? isError?.message : '';
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
if (isError || !response?.data?.payload?.data?.result) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>{errorMessage}</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -94,24 +98,15 @@ function FullView({
|
||||
)}
|
||||
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
GRAPH_TYPES: widget.panelTypes,
|
||||
data: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: response.data?.payload?.data?.result
|
||||
? response.data?.payload?.data?.result
|
||||
: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
isStacked: widget.isStacked,
|
||||
opacity: widget.opacity,
|
||||
title: widget.title,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
}}
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -123,12 +118,14 @@ interface FullViewProps {
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
FullView.defaultProps = {
|
||||
fullViewOptions: undefined,
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
onDragSelect: undefined,
|
||||
};
|
||||
|
||||
export default FullView;
|
||||
|
||||
@@ -15,7 +15,7 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getStartAndEndTime from 'lib/getStartAndEndTime';
|
||||
import getStep from 'lib/getStep';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -30,6 +30,7 @@ function FullView({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@@ -80,25 +81,22 @@ function FullView({
|
||||
const queryLength = widget.query.filter((e) => e.query.length !== 0);
|
||||
|
||||
const response = useQueries(
|
||||
queryLength.map((query) => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
queryFn: () => {
|
||||
return getQueryResult({
|
||||
end: queryMinMax.max.toString(),
|
||||
query: query.query,
|
||||
start: queryMinMax.min.toString(),
|
||||
step: `${getStep({
|
||||
start: queryMinMax.min,
|
||||
end: queryMinMax.max,
|
||||
inputFormat: 's',
|
||||
})}`,
|
||||
});
|
||||
},
|
||||
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
|
||||
retryOnMount: false,
|
||||
};
|
||||
}),
|
||||
queryLength.map((query) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
queryFn: () =>
|
||||
getQueryResult({
|
||||
end: queryMinMax.max.toString(),
|
||||
query: query.query,
|
||||
start: queryMinMax.min.toString(),
|
||||
step: `${getStep({
|
||||
start: queryMinMax.min,
|
||||
end: queryMinMax.max,
|
||||
inputFormat: 's',
|
||||
})}`,
|
||||
}),
|
||||
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
|
||||
retryOnMount: false,
|
||||
})),
|
||||
);
|
||||
|
||||
const isError =
|
||||
@@ -117,6 +115,18 @@ function FullView({
|
||||
})),
|
||||
);
|
||||
|
||||
const chartDataSet = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: data.map((e) => ({
|
||||
query: e?.map((e) => e.query).join(' ') || '',
|
||||
queryData: e?.map((e) => e.queryData) || [],
|
||||
legend: e?.map((e) => e.legend).join('') || '',
|
||||
})),
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
@@ -151,22 +161,15 @@ function FullView({
|
||||
)}
|
||||
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
GRAPH_TYPES: widget.panelTypes,
|
||||
data: getChartData({
|
||||
queryData: data.map((e) => ({
|
||||
query: e?.map((e) => e.query).join(' ') || '',
|
||||
queryData: e?.map((e) => e.queryData) || [],
|
||||
legend: e?.map((e) => e.legend).join('') || '',
|
||||
})),
|
||||
}),
|
||||
isStacked: widget.isStacked,
|
||||
opacity: widget.opacity,
|
||||
title: widget.title,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
}}
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -178,12 +181,14 @@ interface FullViewProps {
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
FullView.defaultProps = {
|
||||
fullViewOptions: undefined,
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
onDragSelect: undefined,
|
||||
};
|
||||
|
||||
export default FullView;
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const GraphContainer = styled.div`
|
||||
min-height: 70vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ChartData } from 'chart.js';
|
||||
import Spinner from 'components/Spinner';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useQuery } from 'react-query';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
@@ -20,13 +22,14 @@ import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LayoutProps } from '..';
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView/index.metricsBuilder';
|
||||
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
@@ -35,13 +38,15 @@ function GridCardGraph({
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const [state, setState] = useState<GridCardGraphState>({
|
||||
loading: true,
|
||||
errorMessage: '',
|
||||
error: false,
|
||||
payload: undefined,
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: true,
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@@ -53,113 +58,57 @@ function GridCardGraph({
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const [selectedDashboard] = dashboards;
|
||||
const selectedData = selectedDashboard?.data;
|
||||
const { variables } = selectedData;
|
||||
|
||||
// const getMaxMinTime = GetMaxMinTime({
|
||||
// graphType: widget?.panelTypes,
|
||||
// maxTime,
|
||||
// minTime,
|
||||
// });
|
||||
|
||||
// const { start, end } = GetStartAndEndTime({
|
||||
// type: widget?.timePreferance,
|
||||
// maxTime: getMaxMinTime.maxTime,
|
||||
// minTime: getMaxMinTime.minTime,
|
||||
// });
|
||||
|
||||
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
|
||||
|
||||
// const response = useQueries(
|
||||
// queryLength?.map((query) => {
|
||||
// return {
|
||||
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
// queryFn: () => {
|
||||
// return getQueryResult({
|
||||
// end,
|
||||
// query: query?.query,
|
||||
// start,
|
||||
// step: '60',
|
||||
// });
|
||||
// },
|
||||
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
|
||||
// retryOnMount: false,
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
|
||||
// const isError =
|
||||
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
|
||||
// response.some((e) => e.isError === true);
|
||||
|
||||
// const isLoading = response.some((e) => e.isLoading === true);
|
||||
|
||||
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
|
||||
|
||||
// const data = response.map((responseOfQuery) =>
|
||||
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
|
||||
// query: queryLength[index]?.query,
|
||||
// queryData: e,
|
||||
// legend: queryLength[index]?.legend,
|
||||
// })),
|
||||
// );
|
||||
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
try {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
}));
|
||||
const response = await GetMetricQueryRange({
|
||||
selectedTime: widget.timePreferance,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
});
|
||||
|
||||
const isError = response.error;
|
||||
|
||||
if (isError != null) {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage: isError || 'Something went wrong',
|
||||
loading: false,
|
||||
}));
|
||||
} else {
|
||||
const chartDataSet = getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: response.payload?.data?.result
|
||||
? response.payload?.data?.result
|
||||
: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: chartDataSet,
|
||||
}));
|
||||
const queryResponse = useQuery(
|
||||
[
|
||||
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
|
||||
{
|
||||
widget,
|
||||
maxTime,
|
||||
minTime,
|
||||
globalSelectedInterval,
|
||||
variables,
|
||||
},
|
||||
],
|
||||
() =>
|
||||
GetMetricQueryRange({
|
||||
selectedTime: widget.timePreferance,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString(),
|
||||
loading: false,
|
||||
}));
|
||||
} finally {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
})();
|
||||
}, [widget, maxTime, minTime, globalSelectedInterval]);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[queryResponse],
|
||||
);
|
||||
|
||||
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||
|
||||
const onToggleModal = useCallback(
|
||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||
@@ -177,70 +126,114 @@ function GridCardGraph({
|
||||
onToggleModal(setDeleteModal);
|
||||
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
||||
|
||||
const getModals = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={(): void => onToggleModal(setDeleteModal)}
|
||||
visible={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
const getModals = (): JSX.Element => (
|
||||
<>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={(): void => onToggleModal(setDeleteModal)}
|
||||
open={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
visible={modal}
|
||||
onCancel={(): void => onToggleModal(setModal)}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
onCancel={(): void => onToggleModal(setModal)}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||
|
||||
if (state.error && !isEmptyLayout) {
|
||||
if (queryResponse.isError && !isEmptyLayout) {
|
||||
return (
|
||||
<>
|
||||
<span ref={graphRef}>
|
||||
{getModals()}
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={(): void => onToggleModal(setModal)}
|
||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||
/>
|
||||
|
||||
<ErrorContainer>{state.errorMessage}</ErrorContainer>
|
||||
</>
|
||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||
<>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<GridGraphComponent
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={prevChartDataSetRef}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(state.loading === true || state.payload === undefined) &&
|
||||
!isEmptyLayout
|
||||
) {
|
||||
return <Spinner height="20vh" tip="Loading..." />;
|
||||
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||
<>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<GridGraphComponent
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={prevChartDataSetRef}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Spinner height="20vh" tip="Loading..." />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={graphRef}
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
@@ -255,28 +248,31 @@ function GridCardGraph({
|
||||
}}
|
||||
>
|
||||
{!isEmptyLayout && (
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={(): void => onToggleModal(setModal)}
|
||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||
/>
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEmptyLayout && getModals()}
|
||||
|
||||
{!isEmpty(widget) && !!state.payload && (
|
||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||
<GridGraphComponent
|
||||
{...{
|
||||
GRAPH_TYPES: widget.panelTypes,
|
||||
data: state.payload,
|
||||
isStacked: widget.isStacked,
|
||||
opacity: widget.opacity,
|
||||
title: ' ', // empty title to accommodate absolutely positioned widget header
|
||||
name,
|
||||
yAxisUnit,
|
||||
}}
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
data={chartData}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '} // `empty title to accommodate absolutely positioned widget header
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -285,13 +281,6 @@ function GridCardGraph({
|
||||
);
|
||||
}
|
||||
|
||||
interface GridCardGraphState {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorMessage: string;
|
||||
payload: ChartData | undefined;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
deleteWidget: ({
|
||||
widgetId,
|
||||
@@ -306,8 +295,13 @@ interface GridCardGraphProps extends DispatchProps {
|
||||
layout?: Layout[];
|
||||
// eslint-disable-next-line react/require-default-props
|
||||
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
GridCardGraph.defaultProps = {
|
||||
onDragSelect: undefined,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -27,7 +28,7 @@ function GraphLayout({
|
||||
setLayout,
|
||||
}: GraphLayoutProps): JSX.Element {
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||
['save_layout', 'add_panel'],
|
||||
@@ -71,6 +72,7 @@ function GraphLayout({
|
||||
useCSSTransforms
|
||||
allowOverlap={false}
|
||||
onLayoutChange={onLayoutChangeHandler}
|
||||
draggableHandle=".drag-handle"
|
||||
>
|
||||
{layouts.map(({ Component, ...rest }) => {
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user